github.com/rohankumardubey/nomad@v0.11.8/plugins/csi/client.go (about) 1 package csi 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "net" 8 "time" 9 10 csipbv1 "github.com/container-storage-interface/spec/lib/go/csi" 11 "github.com/hashicorp/go-hclog" 12 multierror "github.com/hashicorp/go-multierror" 13 "github.com/hashicorp/nomad/helper" 14 "github.com/hashicorp/nomad/helper/grpc-middleware/logging" 15 "github.com/hashicorp/nomad/nomad/structs" 16 "github.com/hashicorp/nomad/plugins/base" 17 "github.com/hashicorp/nomad/plugins/shared/hclspec" 18 "google.golang.org/grpc" 19 "google.golang.org/grpc/codes" 20 "google.golang.org/grpc/status" 21 ) 22 23 // PluginTypeCSI implements the CSI plugin interface 24 const PluginTypeCSI = "csi" 25 26 type NodeGetInfoResponse struct { 27 NodeID string 28 MaxVolumes int64 29 AccessibleTopology *Topology 30 } 31 32 // Topology is a map of topological domains to topological segments. 33 // A topological domain is a sub-division of a cluster, like "region", 34 // "zone", "rack", etc. 35 // 36 // According to CSI, there are a few requirements for the keys within this map: 37 // - Valid keys have two segments: an OPTIONAL prefix and name, separated 38 // by a slash (/), for example: "com.company.example/zone". 39 // - The key name segment is REQUIRED. The prefix is OPTIONAL. 40 // - The key name MUST be 63 characters or less, begin and end with an 41 // alphanumeric character ([a-z0-9A-Z]), and contain only dashes (-), 42 // underscores (_), dots (.), or alphanumerics in between, for example 43 // "zone". 44 // - The key prefix MUST be 63 characters or less, begin and end with a 45 // lower-case alphanumeric character ([a-z0-9]), contain only 46 // dashes (-), dots (.), or lower-case alphanumerics in between, and 47 // follow domain name notation format 48 // (https://tools.ietf.org/html/rfc1035#section-2.3.1). 49 // - The key prefix SHOULD include the plugin's host company name and/or 50 // the plugin name, to minimize the possibility of collisions with keys 51 // from other plugins. 52 // - If a key prefix is specified, it MUST be identical across all 53 // topology keys returned by the SP (across all RPCs). 54 // - Keys MUST be case-insensitive. Meaning the keys "Zone" and "zone" 55 // MUST not both exist. 56 // - Each value (topological segment) MUST contain 1 or more strings. 57 // - Each string MUST be 63 characters or less and begin and end with an 58 // alphanumeric character with '-', '_', '.', or alphanumerics in 59 // between. 60 type Topology struct { 61 Segments map[string]string 62 } 63 64 // CSIControllerClient defines the minimal CSI Controller Plugin interface used 65 // by nomad to simplify the interface required for testing. 66 type CSIControllerClient interface { 67 ControllerGetCapabilities(ctx context.Context, in *csipbv1.ControllerGetCapabilitiesRequest, opts ...grpc.CallOption) (*csipbv1.ControllerGetCapabilitiesResponse, error) 68 ControllerPublishVolume(ctx context.Context, in *csipbv1.ControllerPublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.ControllerPublishVolumeResponse, error) 69 ControllerUnpublishVolume(ctx context.Context, in *csipbv1.ControllerUnpublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.ControllerUnpublishVolumeResponse, error) 70 ValidateVolumeCapabilities(ctx context.Context, in *csipbv1.ValidateVolumeCapabilitiesRequest, opts ...grpc.CallOption) (*csipbv1.ValidateVolumeCapabilitiesResponse, error) 71 } 72 73 // CSINodeClient defines the minimal CSI Node Plugin interface used 74 // by nomad to simplify the interface required for testing. 75 type CSINodeClient interface { 76 NodeGetCapabilities(ctx context.Context, in *csipbv1.NodeGetCapabilitiesRequest, opts ...grpc.CallOption) (*csipbv1.NodeGetCapabilitiesResponse, error) 77 NodeGetInfo(ctx context.Context, in *csipbv1.NodeGetInfoRequest, opts ...grpc.CallOption) (*csipbv1.NodeGetInfoResponse, error) 78 NodeStageVolume(ctx context.Context, in *csipbv1.NodeStageVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodeStageVolumeResponse, error) 79 NodeUnstageVolume(ctx context.Context, in *csipbv1.NodeUnstageVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodeUnstageVolumeResponse, error) 80 NodePublishVolume(ctx context.Context, in *csipbv1.NodePublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodePublishVolumeResponse, error) 81 NodeUnpublishVolume(ctx context.Context, in *csipbv1.NodeUnpublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodeUnpublishVolumeResponse, error) 82 } 83 84 type client struct { 85 conn *grpc.ClientConn 86 identityClient csipbv1.IdentityClient 87 controllerClient CSIControllerClient 88 nodeClient CSINodeClient 89 logger hclog.Logger 90 } 91 92 func (c *client) Close() error { 93 if c.conn != nil { 94 return c.conn.Close() 95 } 96 return nil 97 } 98 99 func NewClient(addr string, logger hclog.Logger) (CSIPlugin, error) { 100 if addr == "" { 101 return nil, fmt.Errorf("address is empty") 102 } 103 104 conn, err := newGrpcConn(addr, logger) 105 if err != nil { 106 return nil, err 107 } 108 109 return &client{ 110 conn: conn, 111 identityClient: csipbv1.NewIdentityClient(conn), 112 controllerClient: csipbv1.NewControllerClient(conn), 113 nodeClient: csipbv1.NewNodeClient(conn), 114 logger: logger, 115 }, nil 116 } 117 118 func newGrpcConn(addr string, logger hclog.Logger) (*grpc.ClientConn, error) { 119 ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 120 defer cancel() 121 conn, err := grpc.DialContext( 122 ctx, 123 addr, 124 grpc.WithBlock(), 125 grpc.WithInsecure(), 126 grpc.WithUnaryInterceptor(logging.UnaryClientInterceptor(logger)), 127 grpc.WithStreamInterceptor(logging.StreamClientInterceptor(logger)), 128 grpc.WithDialer(func(target string, timeout time.Duration) (net.Conn, error) { 129 return net.DialTimeout("unix", target, timeout) 130 }), 131 ) 132 133 if err != nil { 134 return nil, fmt.Errorf("failed to open grpc connection to addr: %s, err: %v", addr, err) 135 } 136 137 return conn, nil 138 } 139 140 // PluginInfo describes the type and version of a plugin as required by the nomad 141 // base.BasePlugin interface. 142 func (c *client) PluginInfo() (*base.PluginInfoResponse, error) { 143 // note: no grpc retries needed here, as this is called in 144 // fingerprinting and will get retried by the caller. 145 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 146 defer cancel() 147 name, version, err := c.PluginGetInfo(ctx) 148 if err != nil { 149 return nil, err 150 } 151 152 return &base.PluginInfoResponse{ 153 Type: PluginTypeCSI, // note: this isn't a Nomad go-plugin type 154 PluginApiVersions: []string{"1.0.0"}, // TODO(tgross): we want to fingerprint spec version, but this isn't included as a field from the plugins 155 PluginVersion: version, 156 Name: name, 157 }, nil 158 } 159 160 // ConfigSchema returns the schema for parsing the plugins configuration as 161 // required by the base.BasePlugin interface. It will always return nil. 162 func (c *client) ConfigSchema() (*hclspec.Spec, error) { 163 return nil, nil 164 } 165 166 // SetConfig is used to set the configuration by passing a MessagePack 167 // encoding of it. 168 func (c *client) SetConfig(_ *base.Config) error { 169 return fmt.Errorf("unsupported") 170 } 171 172 func (c *client) PluginProbe(ctx context.Context) (bool, error) { 173 // note: no grpc retries should be done here 174 req, err := c.identityClient.Probe(ctx, &csipbv1.ProbeRequest{}) 175 if err != nil { 176 return false, err 177 } 178 179 wrapper := req.GetReady() 180 181 // wrapper.GetValue() protects against wrapper being `nil`, and returns false. 182 ready := wrapper.GetValue() 183 184 if wrapper == nil { 185 // If the plugin returns a nil value for ready, then it should be 186 // interpreted as the plugin is ready for compatibility with plugins that 187 // do not do health checks. 188 ready = true 189 } 190 191 return ready, nil 192 } 193 194 func (c *client) PluginGetInfo(ctx context.Context) (string, string, error) { 195 if c == nil { 196 return "", "", fmt.Errorf("Client not initialized") 197 } 198 if c.identityClient == nil { 199 return "", "", fmt.Errorf("Client not initialized") 200 } 201 202 resp, err := c.identityClient.GetPluginInfo(ctx, &csipbv1.GetPluginInfoRequest{}) 203 if err != nil { 204 return "", "", err 205 } 206 207 name := resp.GetName() 208 if name == "" { 209 return "", "", fmt.Errorf("PluginGetInfo: plugin returned empty name field") 210 } 211 version := resp.GetVendorVersion() 212 213 return name, version, nil 214 } 215 216 func (c *client) PluginGetCapabilities(ctx context.Context) (*PluginCapabilitySet, error) { 217 if c == nil { 218 return nil, fmt.Errorf("Client not initialized") 219 } 220 if c.identityClient == nil { 221 return nil, fmt.Errorf("Client not initialized") 222 } 223 224 // note: no grpc retries needed here, as this is called in 225 // fingerprinting and will get retried by the caller 226 resp, err := c.identityClient.GetPluginCapabilities(ctx, 227 &csipbv1.GetPluginCapabilitiesRequest{}) 228 if err != nil { 229 return nil, err 230 } 231 232 return NewPluginCapabilitySet(resp), nil 233 } 234 235 // 236 // Controller Endpoints 237 // 238 239 func (c *client) ControllerGetCapabilities(ctx context.Context) (*ControllerCapabilitySet, error) { 240 if c == nil { 241 return nil, fmt.Errorf("Client not initialized") 242 } 243 if c.controllerClient == nil { 244 return nil, fmt.Errorf("controllerClient not initialized") 245 } 246 247 // note: no grpc retries needed here, as this is called in 248 // fingerprinting and will get retried by the caller 249 resp, err := c.controllerClient.ControllerGetCapabilities(ctx, 250 &csipbv1.ControllerGetCapabilitiesRequest{}) 251 if err != nil { 252 return nil, err 253 } 254 255 return NewControllerCapabilitySet(resp), nil 256 } 257 258 func (c *client) ControllerPublishVolume(ctx context.Context, req *ControllerPublishVolumeRequest, opts ...grpc.CallOption) (*ControllerPublishVolumeResponse, error) { 259 if c == nil { 260 return nil, fmt.Errorf("Client not initialized") 261 } 262 if c.controllerClient == nil { 263 return nil, fmt.Errorf("controllerClient not initialized") 264 } 265 266 err := req.Validate() 267 if err != nil { 268 return nil, err 269 } 270 271 pbrequest := req.ToCSIRepresentation() 272 resp, err := c.controllerClient.ControllerPublishVolume(ctx, pbrequest, opts...) 273 if err != nil { 274 code := status.Code(err) 275 switch code { 276 case codes.NotFound: 277 err = fmt.Errorf("volume %q or node %q could not be found: %v", 278 req.ExternalID, req.NodeID, err) 279 case codes.AlreadyExists: 280 err = fmt.Errorf( 281 "volume %q is already published at node %q but with capabilities or a read_only setting incompatible with this request: %v", 282 req.ExternalID, req.NodeID, err) 283 case codes.ResourceExhausted: 284 err = fmt.Errorf("node %q has reached the maximum allowable number of attached volumes: %v", 285 req.NodeID, err) 286 case codes.FailedPrecondition: 287 err = fmt.Errorf("volume %q is already published on another node and does not have MULTI_NODE volume capability: %v", 288 req.ExternalID, err) 289 case codes.Internal: 290 err = fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 291 } 292 return nil, err 293 } 294 295 return &ControllerPublishVolumeResponse{ 296 PublishContext: helper.CopyMapStringString(resp.PublishContext), 297 }, nil 298 } 299 300 func (c *client) ControllerUnpublishVolume(ctx context.Context, req *ControllerUnpublishVolumeRequest, opts ...grpc.CallOption) (*ControllerUnpublishVolumeResponse, error) { 301 if c == nil { 302 return nil, fmt.Errorf("Client not initialized") 303 } 304 if c.controllerClient == nil { 305 return nil, fmt.Errorf("controllerClient not initialized") 306 } 307 err := req.Validate() 308 if err != nil { 309 return nil, err 310 } 311 312 upbrequest := req.ToCSIRepresentation() 313 _, err = c.controllerClient.ControllerUnpublishVolume(ctx, upbrequest, opts...) 314 if err != nil { 315 code := status.Code(err) 316 switch code { 317 case codes.NotFound: 318 err = fmt.Errorf("volume %q or node %q could not be found: %v", 319 req.ExternalID, req.NodeID, err) 320 case codes.Internal: 321 err = fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 322 } 323 return nil, err 324 } 325 326 return &ControllerUnpublishVolumeResponse{}, nil 327 } 328 329 func (c *client) ControllerValidateCapabilities(ctx context.Context, req *ControllerValidateVolumeRequest, opts ...grpc.CallOption) error { 330 if c == nil { 331 return fmt.Errorf("Client not initialized") 332 } 333 if c.controllerClient == nil { 334 return fmt.Errorf("controllerClient not initialized") 335 } 336 337 if req.ExternalID == "" { 338 return fmt.Errorf("missing volume ID") 339 } 340 341 if req.Capabilities == nil { 342 return fmt.Errorf("missing Capabilities") 343 } 344 345 creq := req.ToCSIRepresentation() 346 resp, err := c.controllerClient.ValidateVolumeCapabilities(ctx, creq, opts...) 347 if err != nil { 348 code := status.Code(err) 349 switch code { 350 case codes.NotFound: 351 err = fmt.Errorf("volume %q could not be found: %v", req.ExternalID, err) 352 case codes.Internal: 353 err = fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 354 } 355 return err 356 } 357 358 if resp.Message != "" { 359 // this should only ever be set if Confirmed isn't set, but 360 // it's not a validation failure. 361 c.logger.Debug(resp.Message) 362 } 363 364 // The protobuf accessors below safely handle nil pointers. 365 // The CSI spec says we can only assert the plugin has 366 // confirmed the volume capabilities, not that it hasn't 367 // confirmed them, so if the field is nil we have to assume 368 // the volume is ok. 369 confirmedCaps := resp.GetConfirmed().GetVolumeCapabilities() 370 if confirmedCaps != nil { 371 for _, requestedCap := range creq.VolumeCapabilities { 372 err := compareCapabilities(requestedCap, confirmedCaps) 373 if err != nil { 374 return fmt.Errorf("volume capability validation failed: %v", err) 375 } 376 } 377 } 378 379 return nil 380 } 381 382 // compareCapabilities returns an error if the 'got' capabilities does not 383 // contain the 'expected' capability 384 func compareCapabilities(expected *csipbv1.VolumeCapability, got []*csipbv1.VolumeCapability) error { 385 var err multierror.Error 386 NEXT_CAP: 387 for _, cap := range got { 388 389 expectedMode := expected.GetAccessMode().GetMode() 390 capMode := cap.GetAccessMode().GetMode() 391 392 if expectedMode != capMode { 393 multierror.Append(&err, 394 fmt.Errorf("requested AccessMode %v, got %v", expectedMode, capMode)) 395 continue NEXT_CAP 396 } 397 398 // AccessType Block is an empty struct even if set, so the 399 // only way to test for it is to check that the AccessType 400 // isn't Mount. 401 expectedMount := expected.GetMount() 402 capMount := cap.GetMount() 403 404 if expectedMount == nil { 405 if capMount == nil { 406 return nil 407 } 408 multierror.Append(&err, fmt.Errorf( 409 "requested AccessType Block but got AccessType Mount")) 410 continue NEXT_CAP 411 } 412 413 if capMount == nil { 414 multierror.Append(&err, fmt.Errorf( 415 "requested AccessType Mount but got AccessType Block")) 416 continue NEXT_CAP 417 } 418 419 if expectedMount.FsType != capMount.FsType { 420 multierror.Append(&err, fmt.Errorf( 421 "requested AccessType mount filesystem type %v, got %v", 422 expectedMount.FsType, capMount.FsType)) 423 continue NEXT_CAP 424 } 425 426 for _, expectedFlag := range expectedMount.MountFlags { 427 var ok bool 428 for _, flag := range capMount.MountFlags { 429 if expectedFlag == flag { 430 ok = true 431 break 432 } 433 } 434 if !ok { 435 // mount flags can contain sensitive data, so we can't log details 436 multierror.Append(&err, fmt.Errorf( 437 "requested mount flags did not match available capabilities")) 438 continue NEXT_CAP 439 } 440 } 441 return nil 442 } 443 return err.ErrorOrNil() 444 } 445 446 // 447 // Node Endpoints 448 // 449 450 func (c *client) NodeGetCapabilities(ctx context.Context) (*NodeCapabilitySet, error) { 451 if c == nil { 452 return nil, fmt.Errorf("Client not initialized") 453 } 454 if c.nodeClient == nil { 455 return nil, fmt.Errorf("Client not initialized") 456 } 457 458 // note: no grpc retries needed here, as this is called in 459 // fingerprinting and will get retried by the caller 460 resp, err := c.nodeClient.NodeGetCapabilities(ctx, &csipbv1.NodeGetCapabilitiesRequest{}) 461 if err != nil { 462 return nil, err 463 } 464 465 return NewNodeCapabilitySet(resp), nil 466 } 467 468 func (c *client) NodeGetInfo(ctx context.Context) (*NodeGetInfoResponse, error) { 469 if c == nil { 470 return nil, fmt.Errorf("Client not initialized") 471 } 472 if c.nodeClient == nil { 473 return nil, fmt.Errorf("Client not initialized") 474 } 475 476 result := &NodeGetInfoResponse{} 477 478 // note: no grpc retries needed here, as this is called in 479 // fingerprinting and will get retried by the caller 480 resp, err := c.nodeClient.NodeGetInfo(ctx, &csipbv1.NodeGetInfoRequest{}) 481 if err != nil { 482 return nil, err 483 } 484 485 if resp.GetNodeId() == "" { 486 return nil, fmt.Errorf("plugin failed to return nodeid") 487 } 488 489 result.NodeID = resp.GetNodeId() 490 result.MaxVolumes = resp.GetMaxVolumesPerNode() 491 if result.MaxVolumes == 0 { 492 // set safe default so that scheduler ignores this constraint when not set 493 result.MaxVolumes = math.MaxInt64 494 } 495 496 return result, nil 497 } 498 499 func (c *client) NodeStageVolume(ctx context.Context, volumeID string, publishContext map[string]string, stagingTargetPath string, capabilities *VolumeCapability, secrets structs.CSISecrets, opts ...grpc.CallOption) error { 500 if c == nil { 501 return fmt.Errorf("Client not initialized") 502 } 503 if c.nodeClient == nil { 504 return fmt.Errorf("Client not initialized") 505 } 506 507 // These errors should not be returned during production use but exist as aids 508 // during Nomad development 509 if volumeID == "" { 510 return fmt.Errorf("missing volumeID") 511 } 512 if stagingTargetPath == "" { 513 return fmt.Errorf("missing stagingTargetPath") 514 } 515 516 req := &csipbv1.NodeStageVolumeRequest{ 517 VolumeId: volumeID, 518 PublishContext: publishContext, 519 StagingTargetPath: stagingTargetPath, 520 VolumeCapability: capabilities.ToCSIRepresentation(), 521 Secrets: secrets, 522 } 523 524 // NodeStageVolume's response contains no extra data. If err == nil, we were 525 // successful. 526 _, err := c.nodeClient.NodeStageVolume(ctx, req, opts...) 527 if err != nil { 528 code := status.Code(err) 529 switch code { 530 case codes.NotFound: 531 err = fmt.Errorf("volume %q could not be found: %v", volumeID, err) 532 case codes.AlreadyExists: 533 err = fmt.Errorf( 534 "volume %q is already staged to %q but with incompatible capabilities for this request: %v", 535 volumeID, stagingTargetPath, err) 536 case codes.FailedPrecondition: 537 err = fmt.Errorf("volume %q is already published on another node and does not have MULTI_NODE volume capability: %v", 538 volumeID, err) 539 case codes.Internal: 540 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 541 } 542 } 543 544 return err 545 } 546 547 func (c *client) NodeUnstageVolume(ctx context.Context, volumeID string, stagingTargetPath string, opts ...grpc.CallOption) error { 548 if c == nil { 549 return fmt.Errorf("Client not initialized") 550 } 551 if c.nodeClient == nil { 552 return fmt.Errorf("Client not initialized") 553 } 554 // These errors should not be returned during production use but exist as aids 555 // during Nomad development 556 if volumeID == "" { 557 return fmt.Errorf("missing volumeID") 558 } 559 if stagingTargetPath == "" { 560 return fmt.Errorf("missing stagingTargetPath") 561 } 562 563 req := &csipbv1.NodeUnstageVolumeRequest{ 564 VolumeId: volumeID, 565 StagingTargetPath: stagingTargetPath, 566 } 567 568 // NodeUnstageVolume's response contains no extra data. If err == nil, we were 569 // successful. 570 _, err := c.nodeClient.NodeUnstageVolume(ctx, req, opts...) 571 if err != nil { 572 code := status.Code(err) 573 switch code { 574 case codes.NotFound: 575 err = fmt.Errorf("volume %q could not be found: %v", volumeID, err) 576 case codes.Internal: 577 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 578 } 579 } 580 581 return err 582 } 583 584 func (c *client) NodePublishVolume(ctx context.Context, req *NodePublishVolumeRequest, opts ...grpc.CallOption) error { 585 if c == nil { 586 return fmt.Errorf("Client not initialized") 587 } 588 if c.nodeClient == nil { 589 return fmt.Errorf("Client not initialized") 590 } 591 592 if err := req.Validate(); err != nil { 593 return fmt.Errorf("validation error: %v", err) 594 } 595 596 // NodePublishVolume's response contains no extra data. If err == nil, we were 597 // successful. 598 _, err := c.nodeClient.NodePublishVolume(ctx, req.ToCSIRepresentation(), opts...) 599 if err != nil { 600 code := status.Code(err) 601 switch code { 602 case codes.NotFound: 603 err = fmt.Errorf("volume %q could not be found: %v", req.ExternalID, err) 604 case codes.AlreadyExists: 605 err = fmt.Errorf( 606 "volume %q is already published at target path %q but with capabilities or a read_only setting incompatible with this request: %v", 607 req.ExternalID, req.TargetPath, err) 608 case codes.FailedPrecondition: 609 err = fmt.Errorf("volume %q is already published on another node and does not have MULTI_NODE volume capability: %v", 610 req.ExternalID, err) 611 case codes.Internal: 612 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 613 } 614 } 615 return err 616 } 617 618 func (c *client) NodeUnpublishVolume(ctx context.Context, volumeID, targetPath string, opts ...grpc.CallOption) error { 619 if c == nil { 620 return fmt.Errorf("Client not initialized") 621 } 622 if c.nodeClient == nil { 623 return fmt.Errorf("Client not initialized") 624 } 625 626 // These errors should not be returned during production use but exist as aids 627 // during Nomad development 628 if volumeID == "" { 629 return fmt.Errorf("missing volumeID") 630 } 631 if targetPath == "" { 632 return fmt.Errorf("missing targetPath") 633 } 634 635 req := &csipbv1.NodeUnpublishVolumeRequest{ 636 VolumeId: volumeID, 637 TargetPath: targetPath, 638 } 639 640 // NodeUnpublishVolume's response contains no extra data. If err == nil, we were 641 // successful. 642 _, err := c.nodeClient.NodeUnpublishVolume(ctx, req, opts...) 643 if err != nil { 644 code := status.Code(err) 645 switch code { 646 case codes.NotFound: 647 err = fmt.Errorf("volume %q could not be found: %v", volumeID, err) 648 case codes.Internal: 649 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 650 } 651 } 652 653 return err 654 }