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 }