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  }