github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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 // we'll have validated the volume and node *should* exist at the 319 // server, so if we get a not-found here it's because we've previously 320 // checkpointed. we'll return an error so the caller can log it for 321 // diagnostic purposes. 322 err = fmt.Errorf("%w: volume %q or node %q could not be found: %v", 323 structs.ErrCSIClientRPCIgnorable, req.ExternalID, req.NodeID, err) 324 case codes.Internal: 325 err = fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 326 } 327 return nil, err 328 } 329 330 return &ControllerUnpublishVolumeResponse{}, nil 331 } 332 333 func (c *client) ControllerValidateCapabilities(ctx context.Context, req *ControllerValidateVolumeRequest, opts ...grpc.CallOption) error { 334 if c == nil { 335 return fmt.Errorf("Client not initialized") 336 } 337 if c.controllerClient == nil { 338 return fmt.Errorf("controllerClient not initialized") 339 } 340 341 if req.ExternalID == "" { 342 return fmt.Errorf("missing volume ID") 343 } 344 345 if req.Capabilities == nil { 346 return fmt.Errorf("missing Capabilities") 347 } 348 349 creq := req.ToCSIRepresentation() 350 resp, err := c.controllerClient.ValidateVolumeCapabilities(ctx, creq, opts...) 351 if err != nil { 352 code := status.Code(err) 353 switch code { 354 case codes.NotFound: 355 err = fmt.Errorf("volume %q could not be found: %v", req.ExternalID, err) 356 case codes.Internal: 357 err = fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 358 } 359 return err 360 } 361 362 if resp.Message != "" { 363 // this should only ever be set if Confirmed isn't set, but 364 // it's not a validation failure. 365 c.logger.Debug(resp.Message) 366 } 367 368 // The protobuf accessors below safely handle nil pointers. 369 // The CSI spec says we can only assert the plugin has 370 // confirmed the volume capabilities, not that it hasn't 371 // confirmed them, so if the field is nil we have to assume 372 // the volume is ok. 373 confirmedCaps := resp.GetConfirmed().GetVolumeCapabilities() 374 if confirmedCaps != nil { 375 for _, requestedCap := range creq.VolumeCapabilities { 376 err := compareCapabilities(requestedCap, confirmedCaps) 377 if err != nil { 378 return fmt.Errorf("volume capability validation failed: %v", err) 379 } 380 } 381 } 382 383 return nil 384 } 385 386 // compareCapabilities returns an error if the 'got' capabilities aren't found 387 // within the 'expected' capability. 388 // 389 // Note that plugins in the wild are known to return incomplete 390 // VolumeCapability responses, so we can't require that all capabilities we 391 // expect have been validated, only that the ones that have been validated 392 // match. This appears to violate the CSI specification but until that's been 393 // resolved in upstream we have to loosen our validation requirements. The 394 // tradeoff is that we're more likely to have runtime errors during 395 // NodeStageVolume. 396 func compareCapabilities(expected *csipbv1.VolumeCapability, got []*csipbv1.VolumeCapability) error { 397 var err multierror.Error 398 NEXT_CAP: 399 for _, cap := range got { 400 401 expectedMode := expected.GetAccessMode().GetMode() 402 capMode := cap.GetAccessMode().GetMode() 403 404 // The plugin may not validate AccessMode, in which case we'll 405 // get UNKNOWN as our response 406 if capMode != csipbv1.VolumeCapability_AccessMode_UNKNOWN { 407 if expectedMode != capMode { 408 multierror.Append(&err, 409 fmt.Errorf("requested access mode %v, got %v", expectedMode, capMode)) 410 continue NEXT_CAP 411 } 412 } 413 414 capBlock := cap.GetBlock() 415 capMount := cap.GetMount() 416 expectedBlock := expected.GetBlock() 417 expectedMount := expected.GetMount() 418 419 if capBlock != nil && expectedBlock == nil { 420 multierror.Append(&err, fmt.Errorf( 421 "'block-device' access type was not requested but was validated by the controller")) 422 continue NEXT_CAP 423 } 424 425 if capMount == nil { 426 continue NEXT_CAP 427 } 428 429 if expectedMount == nil { 430 multierror.Append(&err, fmt.Errorf( 431 "'file-system' access type was not requested but was validated by the controller")) 432 continue NEXT_CAP 433 } 434 435 if expectedMount.FsType != capMount.FsType { 436 multierror.Append(&err, fmt.Errorf( 437 "requested filesystem type %v, got %v", 438 expectedMount.FsType, capMount.FsType)) 439 continue NEXT_CAP 440 } 441 442 for _, expectedFlag := range expectedMount.MountFlags { 443 var ok bool 444 for _, flag := range capMount.MountFlags { 445 if expectedFlag == flag { 446 ok = true 447 break 448 } 449 } 450 if !ok { 451 // mount flags can contain sensitive data, so we can't log details 452 multierror.Append(&err, fmt.Errorf( 453 "requested mount flags did not match available capabilities")) 454 continue NEXT_CAP 455 } 456 } 457 458 return nil 459 } 460 return err.ErrorOrNil() 461 } 462 463 // 464 // Node Endpoints 465 // 466 467 func (c *client) NodeGetCapabilities(ctx context.Context) (*NodeCapabilitySet, error) { 468 if c == nil { 469 return nil, fmt.Errorf("Client not initialized") 470 } 471 if c.nodeClient == nil { 472 return nil, fmt.Errorf("Client not initialized") 473 } 474 475 // note: no grpc retries needed here, as this is called in 476 // fingerprinting and will get retried by the caller 477 resp, err := c.nodeClient.NodeGetCapabilities(ctx, &csipbv1.NodeGetCapabilitiesRequest{}) 478 if err != nil { 479 return nil, err 480 } 481 482 return NewNodeCapabilitySet(resp), nil 483 } 484 485 func (c *client) NodeGetInfo(ctx context.Context) (*NodeGetInfoResponse, error) { 486 if c == nil { 487 return nil, fmt.Errorf("Client not initialized") 488 } 489 if c.nodeClient == nil { 490 return nil, fmt.Errorf("Client not initialized") 491 } 492 493 result := &NodeGetInfoResponse{} 494 495 // note: no grpc retries needed here, as this is called in 496 // fingerprinting and will get retried by the caller 497 resp, err := c.nodeClient.NodeGetInfo(ctx, &csipbv1.NodeGetInfoRequest{}) 498 if err != nil { 499 return nil, err 500 } 501 502 if resp.GetNodeId() == "" { 503 return nil, fmt.Errorf("plugin failed to return nodeid") 504 } 505 506 result.NodeID = resp.GetNodeId() 507 result.MaxVolumes = resp.GetMaxVolumesPerNode() 508 if result.MaxVolumes == 0 { 509 // set safe default so that scheduler ignores this constraint when not set 510 result.MaxVolumes = math.MaxInt64 511 } 512 513 return result, nil 514 } 515 516 func (c *client) NodeStageVolume(ctx context.Context, req *NodeStageVolumeRequest, opts ...grpc.CallOption) error { 517 if c == nil { 518 return fmt.Errorf("Client not initialized") 519 } 520 if c.nodeClient == nil { 521 return fmt.Errorf("Client not initialized") 522 } 523 err := req.Validate() 524 if err != nil { 525 return err 526 } 527 528 // NodeStageVolume's response contains no extra data. If err == nil, we were 529 // successful. 530 _, err = c.nodeClient.NodeStageVolume(ctx, req.ToCSIRepresentation(), opts...) 531 if err != nil { 532 code := status.Code(err) 533 switch code { 534 case codes.NotFound: 535 err = fmt.Errorf("volume %q could not be found: %v", req.ExternalID, err) 536 case codes.AlreadyExists: 537 err = fmt.Errorf( 538 "volume %q is already staged to %q but with incompatible capabilities for this request: %v", 539 req.ExternalID, req.StagingTargetPath, err) 540 case codes.FailedPrecondition: 541 err = fmt.Errorf("volume %q is already published on another node and does not have MULTI_NODE volume capability: %v", 542 req.ExternalID, err) 543 case codes.Internal: 544 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 545 } 546 } 547 548 return err 549 } 550 551 func (c *client) NodeUnstageVolume(ctx context.Context, volumeID string, stagingTargetPath string, opts ...grpc.CallOption) error { 552 if c == nil { 553 return fmt.Errorf("Client not initialized") 554 } 555 if c.nodeClient == nil { 556 return fmt.Errorf("Client not initialized") 557 } 558 // These errors should not be returned during production use but exist as aids 559 // during Nomad development 560 if volumeID == "" { 561 return fmt.Errorf("missing volumeID") 562 } 563 if stagingTargetPath == "" { 564 return fmt.Errorf("missing stagingTargetPath") 565 } 566 567 req := &csipbv1.NodeUnstageVolumeRequest{ 568 VolumeId: volumeID, 569 StagingTargetPath: stagingTargetPath, 570 } 571 572 // NodeUnstageVolume's response contains no extra data. If err == nil, we were 573 // successful. 574 _, err := c.nodeClient.NodeUnstageVolume(ctx, req, opts...) 575 if err != nil { 576 code := status.Code(err) 577 switch code { 578 case codes.NotFound: 579 err = fmt.Errorf("%w: volume %q could not be found: %v", 580 structs.ErrCSIClientRPCIgnorable, volumeID, err) 581 case codes.Internal: 582 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 583 } 584 } 585 586 return err 587 } 588 589 func (c *client) NodePublishVolume(ctx context.Context, req *NodePublishVolumeRequest, opts ...grpc.CallOption) error { 590 if c == nil { 591 return fmt.Errorf("Client not initialized") 592 } 593 if c.nodeClient == nil { 594 return fmt.Errorf("Client not initialized") 595 } 596 597 if err := req.Validate(); err != nil { 598 return fmt.Errorf("validation error: %v", err) 599 } 600 601 // NodePublishVolume's response contains no extra data. If err == nil, we were 602 // successful. 603 _, err := c.nodeClient.NodePublishVolume(ctx, req.ToCSIRepresentation(), opts...) 604 if err != nil { 605 code := status.Code(err) 606 switch code { 607 case codes.NotFound: 608 err = fmt.Errorf("volume %q could not be found: %v", req.ExternalID, err) 609 case codes.AlreadyExists: 610 err = fmt.Errorf( 611 "volume %q is already published at target path %q but with capabilities or a read_only setting incompatible with this request: %v", 612 req.ExternalID, req.TargetPath, err) 613 case codes.FailedPrecondition: 614 err = fmt.Errorf("volume %q is already published on another node and does not have MULTI_NODE volume capability: %v", 615 req.ExternalID, err) 616 case codes.Internal: 617 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 618 } 619 } 620 return err 621 } 622 623 func (c *client) NodeUnpublishVolume(ctx context.Context, volumeID, targetPath string, opts ...grpc.CallOption) error { 624 if c == nil { 625 return fmt.Errorf("Client not initialized") 626 } 627 if c.nodeClient == nil { 628 return fmt.Errorf("Client not initialized") 629 } 630 631 // These errors should not be returned during production use but exist as aids 632 // during Nomad development 633 if volumeID == "" { 634 return fmt.Errorf("missing volumeID") 635 } 636 if targetPath == "" { 637 return fmt.Errorf("missing targetPath") 638 } 639 640 req := &csipbv1.NodeUnpublishVolumeRequest{ 641 VolumeId: volumeID, 642 TargetPath: targetPath, 643 } 644 645 // NodeUnpublishVolume's response contains no extra data. If err == nil, we were 646 // successful. 647 _, err := c.nodeClient.NodeUnpublishVolume(ctx, req, opts...) 648 if err != nil { 649 code := status.Code(err) 650 switch code { 651 case codes.NotFound: 652 err = fmt.Errorf("%w: volume %q could not be found: %v", 653 structs.ErrCSIClientRPCIgnorable, volumeID, err) 654 case codes.Internal: 655 err = fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: %v", err) 656 } 657 } 658 659 return err 660 }