csi: implement NodeExpandVolume (#18522)

following ControllerExpandVolume
in c6dbba7cde,
which expands the disk at e.g. a cloud vendor,
the controller plugin may say that we also need
to issue NodeExpandVolume for the node plugin to
make the new disk space available to task(s) that
have claims on the volume by e.g. expanding
the filesystem on the node.

csi spec:
https://github.com/container-storage-interface/spec/blob/c918b7f/spec.md#nodeexpandvolume
This commit is contained in:
Daniel Bennett
2023-09-18 10:30:15 -05:00
committed by GitHub
parent d564d7811b
commit 4895d708b4
16 changed files with 744 additions and 26 deletions

View File

@@ -926,5 +926,37 @@ func (c *client) NodeUnpublishVolume(ctx context.Context, volumeID, targetPath s
}
func (c *client) NodeExpandVolume(ctx context.Context, req *NodeExpandVolumeRequest, opts ...grpc.CallOption) (*NodeExpandVolumeResponse, error) {
return nil, nil
if err := req.Validate(); err != nil {
return nil, err
}
if err := c.ensureConnected(ctx); err != nil {
return nil, err
}
exReq := req.ToCSIRepresentation()
resp, err := c.nodeClient.NodeExpandVolume(ctx, exReq, opts...)
if err != nil {
code := status.Code(err)
switch code {
case codes.InvalidArgument:
return nil, fmt.Errorf(
"requested capabilities not compatible with volume %q: %v",
req.ExternalVolumeID, err)
case codes.NotFound:
return nil, fmt.Errorf("%w: volume %q could not be found: %v",
structs.ErrCSIClientRPCIgnorable, req.ExternalVolumeID, err)
case codes.FailedPrecondition:
return nil, fmt.Errorf("volume %q cannot be expanded while in use: %v", req.ExternalVolumeID, err)
case codes.OutOfRange:
return nil, fmt.Errorf(
"unsupported capacity_range for volume %q: %v", req.ExternalVolumeID, err)
case codes.Internal:
return nil, fmt.Errorf(
"node plugin returned an internal error, check the plugin allocation logs for more information: %v", err)
default:
return nil, fmt.Errorf("node plugin returned an error: %v", err)
}
}
return &NodeExpandVolumeResponse{resp.GetCapacityBytes()}, nil
}

View File

@@ -1518,3 +1518,163 @@ func TestClient_RPC_NodeUnpublishVolume(t *testing.T) {
})
}
}
func TestClient_RPC_NodeExpandVolume(t *testing.T) {
// minimum valid request
minRequest := &NodeExpandVolumeRequest{
ExternalVolumeID: "test-vol",
TargetPath: "/test-path",
}
cases := []struct {
Name string
Request *NodeExpandVolumeRequest
ExpectCall *csipbv1.NodeExpandVolumeRequest
ResponseErr error
ExpectedErr error
}{
{
Name: "success min",
Request: minRequest,
ExpectCall: &csipbv1.NodeExpandVolumeRequest{
VolumeId: "test-vol",
VolumePath: "/test-path",
},
},
{
Name: "success full",
Request: &NodeExpandVolumeRequest{
ExternalVolumeID: "test-vol",
TargetPath: "/test-path",
StagingPath: "/test-staging-path",
CapacityRange: &CapacityRange{
RequiredBytes: 5,
LimitBytes: 10,
},
Capability: &VolumeCapability{
AccessType: VolumeAccessTypeMount,
AccessMode: VolumeAccessModeMultiNodeSingleWriter,
MountVolume: &structs.CSIMountOptions{
FSType: "test-fstype",
MountFlags: []string{"test-flags"},
},
},
},
ExpectCall: &csipbv1.NodeExpandVolumeRequest{
VolumeId: "test-vol",
VolumePath: "/test-path",
StagingTargetPath: "/test-staging-path",
CapacityRange: &csipbv1.CapacityRange{
RequiredBytes: 5,
LimitBytes: 10,
},
VolumeCapability: &csipbv1.VolumeCapability{
AccessType: &csipbv1.VolumeCapability_Mount{
Mount: &csipbv1.VolumeCapability_MountVolume{
FsType: "test-fstype",
MountFlags: []string{"test-flags"},
VolumeMountGroup: "",
}},
AccessMode: &csipbv1.VolumeCapability_AccessMode{
Mode: csipbv1.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER},
},
},
},
{
Name: "validate missing volume id",
Request: &NodeExpandVolumeRequest{
TargetPath: "/test-path",
},
ExpectedErr: errors.New("ExternalVolumeID is required"),
},
{
Name: "validate missing target path",
Request: &NodeExpandVolumeRequest{
ExternalVolumeID: "test-volume",
},
ExpectedErr: errors.New("TargetPath is required"),
},
{
Name: "validate min greater than max",
Request: &NodeExpandVolumeRequest{
ExternalVolumeID: "test-vol",
TargetPath: "/test-path",
CapacityRange: &CapacityRange{
RequiredBytes: 4,
LimitBytes: 2,
},
},
ExpectedErr: errors.New("LimitBytes cannot be less than RequiredBytes"),
},
{
Name: "grpc error default case",
Request: minRequest,
ResponseErr: status.Errorf(codes.DataLoss, "misc unspecified error"),
ExpectedErr: errors.New("node plugin returned an error: rpc error: code = DataLoss desc = misc unspecified error"),
},
{
Name: "grpc error invalid argument",
Request: minRequest,
ResponseErr: status.Errorf(codes.InvalidArgument, "sad args"),
ExpectedErr: errors.New("requested capabilities not compatible with volume \"test-vol\": rpc error: code = InvalidArgument desc = sad args"),
},
{
Name: "grpc error NotFound",
Request: minRequest,
ResponseErr: status.Errorf(codes.NotFound, "does not exist"),
ExpectedErr: errors.New("CSI client error (ignorable): volume \"test-vol\" could not be found: rpc error: code = NotFound desc = does not exist"),
},
{
Name: "grpc error FailedPrecondition",
Request: minRequest,
ResponseErr: status.Errorf(codes.FailedPrecondition, "unsupported"),
ExpectedErr: errors.New("volume \"test-vol\" cannot be expanded while in use: rpc error: code = FailedPrecondition desc = unsupported"),
},
{
Name: "grpc error OutOfRange",
Request: minRequest,
ResponseErr: status.Errorf(codes.OutOfRange, "too small"),
ExpectedErr: errors.New("unsupported capacity_range for volume \"test-vol\": rpc error: code = OutOfRange desc = too small"),
},
{
Name: "grpc error Internal",
Request: minRequest,
ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
ExpectedErr: errors.New("node plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
_, _, nc, client := newTestClient(t)
nc.NextErr = tc.ResponseErr
// the fake client should take ~no time, but set a timeout just in case
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
defer cancel()
resp, err := client.NodeExpandVolume(ctx, tc.Request)
if tc.ExpectedErr != nil {
must.EqError(t, err, tc.ExpectedErr.Error())
return
}
must.NoError(t, err)
must.NotNil(t, resp)
must.Eq(t, tc.ExpectCall, nc.LastExpandVolumeRequest)
})
}
t.Run("connection error", func(t *testing.T) {
c := &client{} // induce c.ensureConnected() error
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
defer cancel()
resp, err := c.NodeExpandVolume(ctx, &NodeExpandVolumeRequest{
ExternalVolumeID: "valid-id",
TargetPath: "/some-path",
})
must.Nil(t, resp)
must.EqError(t, err, "address is empty")
})
}

View File

@@ -1020,6 +1020,19 @@ type CapacityRange struct {
LimitBytes int64
}
func (c *CapacityRange) Validate() error {
if c == nil {
return nil
}
if c.RequiredBytes == 0 && c.LimitBytes == 0 {
return errors.New("either RequiredBytes or LimitBytes must be set")
}
if c.LimitBytes > 0 && c.LimitBytes < c.RequiredBytes {
return errors.New("LimitBytes cannot be less than RequiredBytes")
}
return nil
}
func (c *CapacityRange) ToCSIRepresentation() *csipbv1.CapacityRange {
if c == nil {
return nil
@@ -1032,11 +1045,24 @@ func (c *CapacityRange) ToCSIRepresentation() *csipbv1.CapacityRange {
type NodeExpandVolumeRequest struct {
ExternalVolumeID string
RequiredBytes int64
LimitBytes int64
CapacityRange *CapacityRange
Capability *VolumeCapability
TargetPath string
StagingPath string
Capability *VolumeCapability
}
func (r *NodeExpandVolumeRequest) Validate() error {
var err error
if r.ExternalVolumeID == "" {
err = errors.Join(err, errors.New("ExternalVolumeID is required"))
}
if r.TargetPath == "" {
err = errors.Join(err, errors.New("TargetPath is required"))
}
if e := r.CapacityRange.Validate(); e != nil {
err = errors.Join(err, e)
}
return err
}
func (r *NodeExpandVolumeRequest) ToCSIRepresentation() *csipbv1.NodeExpandVolumeRequest {
@@ -1044,13 +1070,10 @@ func (r *NodeExpandVolumeRequest) ToCSIRepresentation() *csipbv1.NodeExpandVolum
return nil
}
return &csipbv1.NodeExpandVolumeRequest{
VolumeId: r.ExternalVolumeID,
VolumePath: r.TargetPath,
CapacityRange: &csipbv1.CapacityRange{
RequiredBytes: r.RequiredBytes,
LimitBytes: r.LimitBytes,
},
VolumeId: r.ExternalVolumeID,
VolumePath: r.TargetPath,
StagingTargetPath: r.StagingPath,
CapacityRange: r.CapacityRange.ToCSIRepresentation(),
VolumeCapability: r.Capability.ToCSIRepresentation(),
}
}

View File

@@ -150,6 +150,7 @@ type NodeClient struct {
NextPublishVolumeResponse *csipbv1.NodePublishVolumeResponse
NextUnpublishVolumeResponse *csipbv1.NodeUnpublishVolumeResponse
NextExpandVolumeResponse *csipbv1.NodeExpandVolumeResponse
LastExpandVolumeRequest *csipbv1.NodeExpandVolumeRequest
}
// NewNodeClient returns a new stub NodeClient
@@ -193,5 +194,6 @@ func (c *NodeClient) NodeUnpublishVolume(ctx context.Context, in *csipbv1.NodeUn
}
func (c *NodeClient) NodeExpandVolume(ctx context.Context, in *csipbv1.NodeExpandVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodeExpandVolumeResponse, error) {
c.LastExpandVolumeRequest = in
return c.NextExpandVolumeResponse, c.NextErr
}