github.com/hashicorp/nomad/api@v0.0.0-20240306165712-3193ac204f65/csi.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package api
     5  
     6  import (
     7  	"fmt"
     8  	"net/url"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  )
    13  
    14  // CSIVolumes is used to access Container Storage Interface (CSI) endpoints.
    15  type CSIVolumes struct {
    16  	client *Client
    17  }
    18  
    19  // CSIVolumes returns a handle on the CSIVolumes endpoint.
    20  func (c *Client) CSIVolumes() *CSIVolumes {
    21  	return &CSIVolumes{client: c}
    22  }
    23  
    24  // List returns all CSI volumes.
    25  func (v *CSIVolumes) List(q *QueryOptions) ([]*CSIVolumeListStub, *QueryMeta, error) {
    26  	var resp []*CSIVolumeListStub
    27  	qm, err := v.client.query("/v1/volumes?type=csi", &resp, q)
    28  	if err != nil {
    29  		return nil, nil, err
    30  	}
    31  	sort.Sort(CSIVolumeIndexSort(resp))
    32  	return resp, qm, nil
    33  }
    34  
    35  // ListExternal returns all CSI volumes, as understood by the external storage
    36  // provider. These volumes may or may not be currently registered with Nomad.
    37  // The response is paginated by the plugin and accepts the
    38  // QueryOptions.PerPage and QueryOptions.NextToken fields.
    39  func (v *CSIVolumes) ListExternal(pluginID string, q *QueryOptions) (*CSIVolumeListExternalResponse, *QueryMeta, error) {
    40  	var resp *CSIVolumeListExternalResponse
    41  
    42  	qp := url.Values{}
    43  	qp.Set("plugin_id", pluginID)
    44  	if q.NextToken != "" {
    45  		qp.Set("next_token", q.NextToken)
    46  	}
    47  	if q.PerPage != 0 {
    48  		qp.Set("per_page", fmt.Sprint(q.PerPage))
    49  	}
    50  
    51  	qm, err := v.client.query("/v1/volumes/external?"+qp.Encode(), &resp, q)
    52  	if err != nil {
    53  		return nil, nil, err
    54  	}
    55  
    56  	sort.Sort(CSIVolumeExternalStubSort(resp.Volumes))
    57  	return resp, qm, nil
    58  }
    59  
    60  // PluginList returns all CSI volumes for the specified plugin id
    61  func (v *CSIVolumes) PluginList(pluginID string) ([]*CSIVolumeListStub, *QueryMeta, error) {
    62  	return v.List(&QueryOptions{Prefix: pluginID})
    63  }
    64  
    65  // Info is used to retrieve a single CSIVolume
    66  func (v *CSIVolumes) Info(id string, q *QueryOptions) (*CSIVolume, *QueryMeta, error) {
    67  	var resp CSIVolume
    68  	qm, err := v.client.query("/v1/volume/csi/"+id, &resp, q)
    69  	if err != nil {
    70  		return nil, nil, err
    71  	}
    72  
    73  	return &resp, qm, nil
    74  }
    75  
    76  // Register registers a single CSIVolume with Nomad. The volume must already
    77  // exist in the external storage provider.
    78  func (v *CSIVolumes) Register(vol *CSIVolume, w *WriteOptions) (*WriteMeta, error) {
    79  	req := CSIVolumeRegisterRequest{
    80  		Volumes: []*CSIVolume{vol},
    81  	}
    82  	meta, err := v.client.put("/v1/volume/csi/"+vol.ID, req, nil, w)
    83  	return meta, err
    84  }
    85  
    86  // Deregister deregisters a single CSIVolume from Nomad. The volume will not be deleted from the external storage provider.
    87  func (v *CSIVolumes) Deregister(id string, force bool, w *WriteOptions) error {
    88  	_, err := v.client.delete(fmt.Sprintf("/v1/volume/csi/%v?force=%t", url.PathEscape(id), force), nil, nil, w)
    89  	return err
    90  }
    91  
    92  // Create creates a single CSIVolume in an external storage provider and
    93  // registers it with Nomad. You do not need to call Register if this call is
    94  // successful.
    95  func (v *CSIVolumes) Create(vol *CSIVolume, w *WriteOptions) ([]*CSIVolume, *WriteMeta, error) {
    96  	req := CSIVolumeCreateRequest{
    97  		Volumes: []*CSIVolume{vol},
    98  	}
    99  
   100  	resp := &CSIVolumeCreateResponse{}
   101  	meta, err := v.client.put(fmt.Sprintf("/v1/volume/csi/%v/create", vol.ID), req, resp, w)
   102  	return resp.Volumes, meta, err
   103  }
   104  
   105  // DEPRECATED: will be removed in Nomad 1.4.0
   106  // Delete deletes a CSI volume from an external storage provider. The ID
   107  // passed as an argument here is for the storage provider's ID, so a volume
   108  // that's already been deregistered can be deleted.
   109  func (v *CSIVolumes) Delete(externalVolID string, w *WriteOptions) error {
   110  	_, err := v.client.delete(fmt.Sprintf("/v1/volume/csi/%v/delete", url.PathEscape(externalVolID)), nil, nil, w)
   111  	return err
   112  }
   113  
   114  // DeleteOpts deletes a CSI volume from an external storage
   115  // provider. The ID passed in the request is for the storage
   116  // provider's ID, so a volume that's already been deregistered can be
   117  // deleted.
   118  func (v *CSIVolumes) DeleteOpts(req *CSIVolumeDeleteRequest, w *WriteOptions) error {
   119  	if w == nil {
   120  		w = &WriteOptions{}
   121  	}
   122  	w.SetHeadersFromCSISecrets(req.Secrets)
   123  	_, err := v.client.delete(fmt.Sprintf("/v1/volume/csi/%v/delete", url.PathEscape(req.ExternalVolumeID)), nil, nil, w)
   124  	return err
   125  }
   126  
   127  // Detach causes Nomad to attempt to detach a CSI volume from a client
   128  // node. This is used in the case that the node is temporarily lost and the
   129  // allocations are unable to drop their claims automatically.
   130  func (v *CSIVolumes) Detach(volID, nodeID string, w *WriteOptions) error {
   131  	_, err := v.client.delete(fmt.Sprintf("/v1/volume/csi/%v/detach?node=%v", url.PathEscape(volID), nodeID), nil, nil, w)
   132  	return err
   133  }
   134  
   135  // CreateSnapshot snapshots an external storage volume.
   136  func (v *CSIVolumes) CreateSnapshot(snap *CSISnapshot, w *WriteOptions) (*CSISnapshotCreateResponse, *WriteMeta, error) {
   137  	req := &CSISnapshotCreateRequest{
   138  		Snapshots: []*CSISnapshot{snap},
   139  	}
   140  	if w == nil {
   141  		w = &WriteOptions{}
   142  	}
   143  	w.SetHeadersFromCSISecrets(snap.Secrets)
   144  	resp := &CSISnapshotCreateResponse{}
   145  	meta, err := v.client.put("/v1/volumes/snapshot", req, resp, w)
   146  	return resp, meta, err
   147  }
   148  
   149  // DeleteSnapshot deletes an external storage volume snapshot.
   150  func (v *CSIVolumes) DeleteSnapshot(snap *CSISnapshot, w *WriteOptions) error {
   151  	qp := url.Values{}
   152  	qp.Set("snapshot_id", snap.ID)
   153  	qp.Set("plugin_id", snap.PluginID)
   154  	if w == nil {
   155  		w = &WriteOptions{}
   156  	}
   157  	w.SetHeadersFromCSISecrets(snap.Secrets)
   158  	_, err := v.client.delete("/v1/volumes/snapshot?"+qp.Encode(), nil, nil, w)
   159  	return err
   160  }
   161  
   162  // ListSnapshotsOpts lists external storage volume snapshots.
   163  func (v *CSIVolumes) ListSnapshotsOpts(req *CSISnapshotListRequest) (*CSISnapshotListResponse, *QueryMeta, error) {
   164  	var resp *CSISnapshotListResponse
   165  
   166  	qp := url.Values{}
   167  	if req.PluginID != "" {
   168  		qp.Set("plugin_id", req.PluginID)
   169  	}
   170  	if req.NextToken != "" {
   171  		qp.Set("next_token", req.NextToken)
   172  	}
   173  	if req.PerPage != 0 {
   174  		qp.Set("per_page", fmt.Sprint(req.PerPage))
   175  	}
   176  	req.QueryOptions.SetHeadersFromCSISecrets(req.Secrets)
   177  
   178  	qm, err := v.client.query("/v1/volumes/snapshot?"+qp.Encode(), &resp, &req.QueryOptions)
   179  	if err != nil {
   180  		return nil, nil, err
   181  	}
   182  
   183  	sort.Sort(CSISnapshotSort(resp.Snapshots))
   184  	return resp, qm, nil
   185  }
   186  
   187  // DEPRECATED: will be removed in Nomad 1.4.0
   188  // ListSnapshots lists external storage volume snapshots.
   189  func (v *CSIVolumes) ListSnapshots(pluginID string, secrets string, q *QueryOptions) (*CSISnapshotListResponse, *QueryMeta, error) {
   190  	var resp *CSISnapshotListResponse
   191  
   192  	qp := url.Values{}
   193  	if pluginID != "" {
   194  		qp.Set("plugin_id", pluginID)
   195  	}
   196  	if q.NextToken != "" {
   197  		qp.Set("next_token", q.NextToken)
   198  	}
   199  	if q.PerPage != 0 {
   200  		qp.Set("per_page", fmt.Sprint(q.PerPage))
   201  	}
   202  
   203  	qm, err := v.client.query("/v1/volumes/snapshot?"+qp.Encode(), &resp, q)
   204  	if err != nil {
   205  		return nil, nil, err
   206  	}
   207  
   208  	sort.Sort(CSISnapshotSort(resp.Snapshots))
   209  	return resp, qm, nil
   210  }
   211  
   212  // CSIVolumeAttachmentMode chooses the type of storage api that will be used to
   213  // interact with the device. (Duplicated in nomad/structs/csi.go)
   214  type CSIVolumeAttachmentMode string
   215  
   216  const (
   217  	CSIVolumeAttachmentModeUnknown     CSIVolumeAttachmentMode = ""
   218  	CSIVolumeAttachmentModeBlockDevice CSIVolumeAttachmentMode = "block-device"
   219  	CSIVolumeAttachmentModeFilesystem  CSIVolumeAttachmentMode = "file-system"
   220  )
   221  
   222  // CSIVolumeAccessMode indicates how a volume should be used in a storage topology
   223  // e.g whether the provider should make the volume available concurrently. (Duplicated in nomad/structs/csi.go)
   224  type CSIVolumeAccessMode string
   225  
   226  const (
   227  	CSIVolumeAccessModeUnknown               CSIVolumeAccessMode = ""
   228  	CSIVolumeAccessModeSingleNodeReader      CSIVolumeAccessMode = "single-node-reader-only"
   229  	CSIVolumeAccessModeSingleNodeWriter      CSIVolumeAccessMode = "single-node-writer"
   230  	CSIVolumeAccessModeMultiNodeReader       CSIVolumeAccessMode = "multi-node-reader-only"
   231  	CSIVolumeAccessModeMultiNodeSingleWriter CSIVolumeAccessMode = "multi-node-single-writer"
   232  	CSIVolumeAccessModeMultiNodeMultiWriter  CSIVolumeAccessMode = "multi-node-multi-writer"
   233  )
   234  
   235  const (
   236  	CSIVolumeTypeHost = "host"
   237  	CSIVolumeTypeCSI  = "csi"
   238  )
   239  
   240  // CSIMountOptions contain optional additional configuration that can be used
   241  // when specifying that a Volume should be used with VolumeAccessTypeMount.
   242  type CSIMountOptions struct {
   243  	// FSType is an optional field that allows an operator to specify the type
   244  	// of the filesystem.
   245  	FSType string `hcl:"fs_type,optional"`
   246  
   247  	// MountFlags contains additional options that may be used when mounting the
   248  	// volume by the plugin. This may contain sensitive data and should not be
   249  	// leaked.
   250  	MountFlags []string `hcl:"mount_flags,optional"`
   251  
   252  	ExtraKeysHCL []string `hcl1:",unusedKeys" json:"-"` // report unexpected keys
   253  }
   254  
   255  func (o *CSIMountOptions) Merge(p *CSIMountOptions) {
   256  	if p == nil {
   257  		return
   258  	}
   259  	if p.FSType != "" {
   260  		o.FSType = p.FSType
   261  	}
   262  	if p.MountFlags != nil {
   263  		o.MountFlags = p.MountFlags
   264  	}
   265  }
   266  
   267  // CSISecrets contain optional additional credentials that may be needed by
   268  // the storage provider. These values will be redacted when reported in the
   269  // API or in Nomad's logs.
   270  type CSISecrets map[string]string
   271  
   272  func (q *QueryOptions) SetHeadersFromCSISecrets(secrets CSISecrets) {
   273  	pairs := []string{}
   274  	for k, v := range secrets {
   275  		pairs = append(pairs, fmt.Sprintf("%v=%v", k, v))
   276  	}
   277  	if q.Headers == nil {
   278  		q.Headers = map[string]string{}
   279  	}
   280  	q.Headers["X-Nomad-CSI-Secrets"] = strings.Join(pairs, ",")
   281  }
   282  
   283  func (w *WriteOptions) SetHeadersFromCSISecrets(secrets CSISecrets) {
   284  	pairs := []string{}
   285  	for k, v := range secrets {
   286  		pairs = append(pairs, fmt.Sprintf("%v=%v", k, v))
   287  	}
   288  	if w.Headers == nil {
   289  		w.Headers = map[string]string{}
   290  	}
   291  	w.Headers["X-Nomad-CSI-Secrets"] = strings.Join(pairs, ",")
   292  }
   293  
   294  // CSIVolume is used for serialization, see also nomad/structs/csi.go
   295  type CSIVolume struct {
   296  	ID         string
   297  	Name       string
   298  	ExternalID string `mapstructure:"external_id" hcl:"external_id"`
   299  	Namespace  string
   300  
   301  	// RequestedTopologies are the topologies submitted as options to
   302  	// the storage provider at the time the volume was created. After
   303  	// volumes are created, this field is ignored.
   304  	RequestedTopologies *CSITopologyRequest `hcl:"topology_request"`
   305  
   306  	// Topologies are the topologies returned by the storage provider,
   307  	// based on the RequestedTopologies and what the storage provider
   308  	// could support. This value cannot be set by the user.
   309  	Topologies []*CSITopology
   310  
   311  	AccessMode     CSIVolumeAccessMode     `hcl:"access_mode"`
   312  	AttachmentMode CSIVolumeAttachmentMode `hcl:"attachment_mode"`
   313  	MountOptions   *CSIMountOptions        `hcl:"mount_options"`
   314  	Secrets        CSISecrets              `mapstructure:"secrets" hcl:"secrets"`
   315  	Parameters     map[string]string       `mapstructure:"parameters" hcl:"parameters"`
   316  	Context        map[string]string       `mapstructure:"context" hcl:"context"`
   317  	Capacity       int64                   `hcl:"-"`
   318  
   319  	// These fields are used as part of the volume creation request
   320  	RequestedCapacityMin  int64                  `hcl:"capacity_min"`
   321  	RequestedCapacityMax  int64                  `hcl:"capacity_max"`
   322  	RequestedCapabilities []*CSIVolumeCapability `hcl:"capability"`
   323  	CloneID               string                 `mapstructure:"clone_id" hcl:"clone_id"`
   324  	SnapshotID            string                 `mapstructure:"snapshot_id" hcl:"snapshot_id"`
   325  
   326  	// ReadAllocs is a map of allocation IDs for tracking reader claim status.
   327  	// The Allocation value will always be nil; clients can populate this data
   328  	// by iterating over the Allocations field.
   329  	ReadAllocs map[string]*Allocation
   330  
   331  	// WriteAllocs is a map of allocation IDs for tracking writer claim
   332  	// status. The Allocation value will always be nil; clients can populate
   333  	// this data by iterating over the Allocations field.
   334  	WriteAllocs map[string]*Allocation
   335  
   336  	// Allocations is a combined list of readers and writers
   337  	Allocations []*AllocationListStub
   338  
   339  	// Schedulable is true if all the denormalized plugin health fields are true
   340  	Schedulable         bool
   341  	PluginID            string `mapstructure:"plugin_id" hcl:"plugin_id"`
   342  	Provider            string
   343  	ProviderVersion     string
   344  	ControllerRequired  bool
   345  	ControllersHealthy  int
   346  	ControllersExpected int
   347  	NodesHealthy        int
   348  	NodesExpected       int
   349  	ResourceExhausted   time.Time
   350  
   351  	CreateIndex uint64
   352  	ModifyIndex uint64
   353  
   354  	// ExtraKeysHCL is used by the hcl parser to report unexpected keys
   355  	ExtraKeysHCL []string `hcl1:",unusedKeys" json:"-"`
   356  }
   357  
   358  // CSIVolumeCapability is a requested attachment and access mode for a
   359  // volume
   360  type CSIVolumeCapability struct {
   361  	AccessMode     CSIVolumeAccessMode     `mapstructure:"access_mode" hcl:"access_mode"`
   362  	AttachmentMode CSIVolumeAttachmentMode `mapstructure:"attachment_mode" hcl:"attachment_mode"`
   363  }
   364  
   365  // CSIVolumeIndexSort is a helper used for sorting volume stubs by creation
   366  // time.
   367  type CSIVolumeIndexSort []*CSIVolumeListStub
   368  
   369  func (v CSIVolumeIndexSort) Len() int {
   370  	return len(v)
   371  }
   372  
   373  func (v CSIVolumeIndexSort) Less(i, j int) bool {
   374  	return v[i].CreateIndex > v[j].CreateIndex
   375  }
   376  
   377  func (v CSIVolumeIndexSort) Swap(i, j int) {
   378  	v[i], v[j] = v[j], v[i]
   379  }
   380  
   381  // CSIVolumeListStub omits allocations. See also nomad/structs/csi.go
   382  type CSIVolumeListStub struct {
   383  	ID                  string
   384  	Namespace           string
   385  	Name                string
   386  	ExternalID          string
   387  	Topologies          []*CSITopology
   388  	AccessMode          CSIVolumeAccessMode
   389  	AttachmentMode      CSIVolumeAttachmentMode
   390  	CurrentReaders      int
   391  	CurrentWriters      int
   392  	Schedulable         bool
   393  	PluginID            string
   394  	Provider            string
   395  	ControllerRequired  bool
   396  	ControllersHealthy  int
   397  	ControllersExpected int
   398  	NodesHealthy        int
   399  	NodesExpected       int
   400  	ResourceExhausted   time.Time
   401  
   402  	CreateIndex uint64
   403  	ModifyIndex uint64
   404  }
   405  
   406  type CSIVolumeListExternalResponse struct {
   407  	Volumes   []*CSIVolumeExternalStub
   408  	NextToken string
   409  }
   410  
   411  // CSIVolumeExternalStub is the storage provider's view of a volume, as
   412  // returned from the controller plugin; all IDs are for external resources
   413  type CSIVolumeExternalStub struct {
   414  	ExternalID               string
   415  	CapacityBytes            int64
   416  	VolumeContext            map[string]string
   417  	CloneID                  string
   418  	SnapshotID               string
   419  	PublishedExternalNodeIDs []string
   420  	IsAbnormal               bool
   421  	Status                   string
   422  }
   423  
   424  // CSIVolumeExternalStubSort is a sorting helper for external volumes. We
   425  // can't sort these by creation time because we don't get that data back from
   426  // the storage provider. Sort by External ID within this page.
   427  type CSIVolumeExternalStubSort []*CSIVolumeExternalStub
   428  
   429  func (v CSIVolumeExternalStubSort) Len() int {
   430  	return len(v)
   431  }
   432  
   433  func (v CSIVolumeExternalStubSort) Less(i, j int) bool {
   434  	return v[i].ExternalID > v[j].ExternalID
   435  }
   436  
   437  func (v CSIVolumeExternalStubSort) Swap(i, j int) {
   438  	v[i], v[j] = v[j], v[i]
   439  }
   440  
   441  type CSIVolumeCreateRequest struct {
   442  	Volumes []*CSIVolume
   443  	WriteRequest
   444  }
   445  
   446  type CSIVolumeCreateResponse struct {
   447  	Volumes []*CSIVolume
   448  	QueryMeta
   449  }
   450  
   451  type CSIVolumeRegisterRequest struct {
   452  	Volumes []*CSIVolume
   453  	WriteRequest
   454  }
   455  
   456  type CSIVolumeDeregisterRequest struct {
   457  	VolumeIDs []string
   458  	WriteRequest
   459  }
   460  
   461  type CSIVolumeDeleteRequest struct {
   462  	ExternalVolumeID string
   463  	Secrets          CSISecrets
   464  	WriteRequest
   465  }
   466  
   467  // CSISnapshot is the storage provider's view of a volume snapshot
   468  type CSISnapshot struct {
   469  	ID                     string // storage provider's ID
   470  	ExternalSourceVolumeID string // storage provider's ID for volume
   471  	SizeBytes              int64  // value from storage provider
   472  	CreateTime             int64  // value from storage provider
   473  	IsReady                bool   // value from storage provider
   474  	SourceVolumeID         string // Nomad volume ID
   475  	PluginID               string // CSI plugin ID
   476  
   477  	// These field are only used during snapshot creation and will not be
   478  	// populated when the snapshot is returned
   479  	Name       string            // suggested name of the snapshot, used for creation
   480  	Secrets    CSISecrets        // secrets needed to create snapshot
   481  	Parameters map[string]string // secrets needed to create snapshot
   482  }
   483  
   484  // CSISnapshotSort is a helper used for sorting snapshots by creation time.
   485  type CSISnapshotSort []*CSISnapshot
   486  
   487  func (v CSISnapshotSort) Len() int {
   488  	return len(v)
   489  }
   490  
   491  func (v CSISnapshotSort) Less(i, j int) bool {
   492  	return v[i].CreateTime > v[j].CreateTime
   493  }
   494  
   495  func (v CSISnapshotSort) Swap(i, j int) {
   496  	v[i], v[j] = v[j], v[i]
   497  }
   498  
   499  type CSISnapshotCreateRequest struct {
   500  	Snapshots []*CSISnapshot
   501  	WriteRequest
   502  }
   503  
   504  type CSISnapshotCreateResponse struct {
   505  	Snapshots []*CSISnapshot
   506  	QueryMeta
   507  }
   508  
   509  // CSISnapshotListRequest is a request to a controller plugin to list all the
   510  // snapshot known to the the storage provider. This request is paginated by
   511  // the plugin and accepts the QueryOptions.PerPage and QueryOptions.NextToken
   512  // fields
   513  type CSISnapshotListRequest struct {
   514  	PluginID string
   515  	Secrets  CSISecrets
   516  	QueryOptions
   517  }
   518  
   519  type CSISnapshotListResponse struct {
   520  	Snapshots []*CSISnapshot
   521  	NextToken string
   522  	QueryMeta
   523  }
   524  
   525  // CSI Plugins are jobs with plugin specific data
   526  type CSIPlugins struct {
   527  	client *Client
   528  }
   529  
   530  // CSIPlugin is used for serialization, see also nomad/structs/csi.go
   531  type CSIPlugin struct {
   532  	ID                 string
   533  	Provider           string
   534  	Version            string
   535  	ControllerRequired bool
   536  	// Map Node.ID to CSIInfo fingerprint results
   537  	Controllers         map[string]*CSIInfo
   538  	Nodes               map[string]*CSIInfo
   539  	Allocations         []*AllocationListStub
   540  	ControllersHealthy  int
   541  	ControllersExpected int
   542  	NodesHealthy        int
   543  	NodesExpected       int
   544  	CreateIndex         uint64
   545  	ModifyIndex         uint64
   546  }
   547  
   548  type CSIPluginListStub struct {
   549  	ID                  string
   550  	Provider            string
   551  	ControllerRequired  bool
   552  	ControllersHealthy  int
   553  	ControllersExpected int
   554  	NodesHealthy        int
   555  	NodesExpected       int
   556  	CreateIndex         uint64
   557  	ModifyIndex         uint64
   558  }
   559  
   560  // CSIPluginIndexSort is a helper used for sorting plugin stubs by creation
   561  // time.
   562  type CSIPluginIndexSort []*CSIPluginListStub
   563  
   564  func (v CSIPluginIndexSort) Len() int {
   565  	return len(v)
   566  }
   567  
   568  func (v CSIPluginIndexSort) Less(i, j int) bool {
   569  	return v[i].CreateIndex > v[j].CreateIndex
   570  }
   571  
   572  func (v CSIPluginIndexSort) Swap(i, j int) {
   573  	v[i], v[j] = v[j], v[i]
   574  }
   575  
   576  // CSIPlugins returns a handle on the CSIPlugins endpoint
   577  func (c *Client) CSIPlugins() *CSIPlugins {
   578  	return &CSIPlugins{client: c}
   579  }
   580  
   581  // List returns all CSI plugins
   582  func (v *CSIPlugins) List(q *QueryOptions) ([]*CSIPluginListStub, *QueryMeta, error) {
   583  	var resp []*CSIPluginListStub
   584  	qm, err := v.client.query("/v1/plugins?type=csi", &resp, q)
   585  	if err != nil {
   586  		return nil, nil, err
   587  	}
   588  	sort.Sort(CSIPluginIndexSort(resp))
   589  	return resp, qm, nil
   590  }
   591  
   592  // Info is used to retrieve a single CSI Plugin Job
   593  func (v *CSIPlugins) Info(id string, q *QueryOptions) (*CSIPlugin, *QueryMeta, error) {
   594  	var resp *CSIPlugin
   595  	qm, err := v.client.query("/v1/plugin/csi/"+id, &resp, q)
   596  	if err != nil {
   597  		return nil, nil, err
   598  	}
   599  	return resp, qm, nil
   600  }