mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 18:35:44 +03:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user