From dda637fedf6873c5697d90939c256c47824dd7b1 Mon Sep 17 00:00:00 2001 From: Fabio Bertinatto Date: Wed, 27 Mar 2019 13:19:50 +0100 Subject: [PATCH] Add volume expansion --- pkg/cloud/cloud.go | 139 +++++++++++++++++++++++++--- pkg/cloud/cloud_test.go | 127 +++++++++++++++++++++++++ pkg/cloud/mocks/mock_ec2.go | 40 ++++++++ pkg/driver/controller.go | 34 ++++++- pkg/driver/controller_test.go | 80 ++++++++++++++++ pkg/driver/mocks/mock_cloud.go | 15 +++ pkg/driver/mocks/mock_mounter.go | 50 ++++++++++ pkg/driver/node.go | 40 +++++++- pkg/driver/node_test.go | 7 ++ tests/sanity/fake_cloud_provider.go | 10 ++ 10 files changed, 522 insertions(+), 20 deletions(-) diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 718c8dc1d0..ebfe39d1a0 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -160,6 +160,8 @@ type EC2 interface { CreateSnapshotWithContext(ctx aws.Context, input *ec2.CreateSnapshotInput, opts ...request.Option) (*ec2.Snapshot, error) DeleteSnapshotWithContext(ctx aws.Context, input *ec2.DeleteSnapshotInput, opts ...request.Option) (*ec2.DeleteSnapshotOutput, error) DescribeSnapshotsWithContext(ctx aws.Context, input *ec2.DescribeSnapshotsInput, opts ...request.Option) (*ec2.DescribeSnapshotsOutput, error) + ModifyVolumeWithContext(ctx aws.Context, input *ec2.ModifyVolumeInput, opts ...request.Option) (*ec2.ModifyVolumeOutput, error) + DescribeVolumesModificationsWithContext(ctx aws.Context, input *ec2.DescribeVolumesModificationsInput, opts ...request.Option) (*ec2.DescribeVolumesModificationsOutput, error) } type Cloud interface { @@ -168,6 +170,7 @@ type Cloud interface { DeleteDisk(ctx context.Context, volumeID string) (success bool, err error) AttachDisk(ctx context.Context, volumeID string, nodeID string) (devicePath string, err error) DetachDisk(ctx context.Context, volumeID string, nodeID string) (err error) + ResizeDisk(ctx context.Context, volumeID string, reqSize int64) (newSize int64, err error) WaitForAttachmentState(ctx context.Context, volumeID, state string) error GetDiskByName(ctx context.Context, name string, capacityBytes int64) (disk *Disk, err error) GetDiskByID(ctx context.Context, volumeID string) (disk *Disk, err error) @@ -765,27 +768,139 @@ func (c *cloud) waitForVolume(ctx context.Context, volumeID string) error { return err } -// Helper function for describeVolume callers. Tries to retype given error to AWS error -// and returns true in case the AWS error is "InvalidVolume.NotFound", false otherwise -func isAWSErrorVolumeNotFound(err error) bool { +// isAWSError returns a boolean indicating whether the error is AWS-related +// and has the given code. More information on AWS error codes at: +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html +func isAWSError(err error, code string) bool { if awsError, ok := err.(awserr.Error); ok { - // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html - if awsError.Code() == "InvalidVolume.NotFound" { + if awsError.Code() == code { return true } } return false } -// Helper function for describeSnapshot callers. Tries to retype given error to AWS error -// and returns true in case the AWS error is "InvalidSnapshot.NotFound", false otherwise +// isAWSErrorIncorrectModification returns a boolean indicating whether the given error +// is an AWS IncorrectModificationState error. This error means that a modification action +// on an EBS volume cannot occur because the volume is currently being modified. +func isAWSErrorIncorrectModification(err error) bool { + return isAWSError(err, "IncorrectModificationState") +} + +// isAWSErrorVolumeNotFound returns a boolean indicating whether the +// given error is an AWS InvalidVolume.NotFound error. This error is +// reported when the specified volume doesn't exist. +func isAWSErrorVolumeNotFound(err error) bool { + return isAWSError(err, "InvalidVolume.NotFound") +} + +// isAWSErrorSnapshotNotFound returns a boolean indicating whether the +// given error is an AWS InvalidSnapshot.NotFound error. This error is +// reported when the specified snapshot doesn't exist. func isAWSErrorSnapshotNotFound(err error) bool { - if awsError, ok := err.(awserr.Error); ok { - // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html - if awsError.Code() == "InvalidSnapshot.NotFound" { - return true + return isAWSError(err, "InvalidSnapshot.NotFound") +} + +// ResizeDisk resizes an EBS volume in GiB increments, rouding up to the next possible allocatable unit. +// It returns the volume size after this call or an error if the size couldn't be determined. +func (c *cloud) ResizeDisk(ctx context.Context, volumeID string, newSizeBytes int64) (int64, error) { + request := &ec2.DescribeVolumesInput{ + VolumeIds: []*string{ + aws.String(volumeID), + }, + } + volume, err := c.getVolume(ctx, request) + if err != nil { + return 0, err + } + + // AWS resizes in chunks of GiB (not GB) + newSizeGiB := util.RoundUpGiB(newSizeBytes) + oldSizeGiB := aws.Int64Value(volume.Size) + + if oldSizeGiB >= newSizeGiB { + klog.V(5).Infof("Volume %q's current size (%d GiB) is greater or equal to the new size (%d GiB)", volumeID, oldSizeGiB, newSizeGiB) + return oldSizeGiB, nil + } + + req := &ec2.ModifyVolumeInput{ + VolumeId: aws.String(volumeID), + Size: aws.Int64(newSizeGiB), + } + + var mod *ec2.VolumeModification + response, err := c.ec2.ModifyVolumeWithContext(ctx, req) + if err != nil { + if !isAWSErrorIncorrectModification(err) { + return 0, fmt.Errorf("could not modify AWS volume %q: %v", volumeID, err) + } + + m, err := c.getLatestVolumeModification(ctx, volumeID) + if err != nil { + return 0, err } + mod = m } - return false + if mod == nil { + mod = response.VolumeModification + } + + state := aws.StringValue(mod.ModificationState) + if state == ec2.VolumeModificationStateCompleted || state == ec2.VolumeModificationStateOptimizing { + return aws.Int64Value(mod.TargetSize), nil + } + + return c.waitForVolumeSize(ctx, volumeID) +} + +// waitForVolumeSize waits for a volume modification to finish and return its size. +func (c *cloud) waitForVolumeSize(ctx context.Context, volumeID string) (int64, error) { + backoff := wait.Backoff{ + Duration: 1 * time.Second, + Factor: 1.8, + Steps: 20, + } + + var modVolSizeGiB int64 + waitErr := wait.ExponentialBackoff(backoff, func() (bool, error) { + m, err := c.getLatestVolumeModification(ctx, volumeID) + if err != nil { + return false, err + } + + state := aws.StringValue(m.ModificationState) + if state == ec2.VolumeModificationStateCompleted || state == ec2.VolumeModificationStateOptimizing { + modVolSizeGiB = aws.Int64Value(m.TargetSize) + return true, nil + } + + return false, nil + }) + + if waitErr != nil { + return 0, waitErr + } + + return modVolSizeGiB, nil +} + +// getLatestVolumeModification returns the last modification of the volume. +func (c *cloud) getLatestVolumeModification(ctx context.Context, volumeID string) (*ec2.VolumeModification, error) { + request := &ec2.DescribeVolumesModificationsInput{ + VolumeIds: []*string{ + aws.String(volumeID), + }, + } + mod, err := c.ec2.DescribeVolumesModificationsWithContext(ctx, request) + if err != nil { + return nil, fmt.Errorf("error describing modifications in volume %q: %v", volumeID, err) + } + + volumeMods := mod.VolumesModifications + if len(volumeMods) == 0 { + return nil, fmt.Errorf("could not find any modifications for volume %q", volumeID) + } + + return volumeMods[len(volumeMods)-1], nil } diff --git a/pkg/cloud/cloud_test.go b/pkg/cloud/cloud_test.go index f877443001..7a040445f0 100644 --- a/pkg/cloud/cloud_test.go +++ b/pkg/cloud/cloud_test.go @@ -594,6 +594,133 @@ func TestDeleteSnapshot(t *testing.T) { } } +func TestResizeDisk(t *testing.T) { + testCases := []struct { + name string + volumeID string + existingVolume *ec2.Volume + existingVolumeError awserr.Error + modifiedVolume *ec2.ModifyVolumeOutput + modifiedVolumeError awserr.Error + descModVolume *ec2.DescribeVolumesModificationsOutput + reqSizeGiB int64 + expErr error + }{ + { + name: "success: normal", + volumeID: "vol-test", + existingVolume: &ec2.Volume{ + VolumeId: aws.String("vol-test"), + Size: aws.Int64(1), + AvailabilityZone: aws.String(defaultZone), + }, + modifiedVolume: &ec2.ModifyVolumeOutput{ + VolumeModification: &ec2.VolumeModification{ + VolumeId: aws.String("vol-test"), + TargetSize: aws.Int64(2), + ModificationState: aws.String(ec2.VolumeModificationStateOptimizing), + }, + }, + reqSizeGiB: 2, + expErr: nil, + }, + { + name: "success: normal modifying state", + volumeID: "vol-test", + existingVolume: &ec2.Volume{ + VolumeId: aws.String("vol-test"), + Size: aws.Int64(1), + AvailabilityZone: aws.String(defaultZone), + }, + modifiedVolume: &ec2.ModifyVolumeOutput{ + VolumeModification: &ec2.VolumeModification{ + VolumeId: aws.String("vol-test"), + TargetSize: aws.Int64(2), + ModificationState: aws.String(ec2.VolumeModificationStateModifying), + }, + }, + descModVolume: &ec2.DescribeVolumesModificationsOutput{ + VolumesModifications: []*ec2.VolumeModification{ + { + VolumeId: aws.String("vol-test"), + TargetSize: aws.Int64(2), + ModificationState: aws.String(ec2.VolumeModificationStateCompleted), + }, + }, + }, + reqSizeGiB: 2, + expErr: nil, + }, + { + name: "fail: volume doesn't exist", + volumeID: "vol-test", + existingVolumeError: awserr.New("InvalidVolume.NotFound", "", nil), + reqSizeGiB: 2, + expErr: fmt.Errorf("ResizeDisk generic error"), + }, + { + name: "sucess: there is a resizing in progress", + volumeID: "vol-test", + existingVolume: &ec2.Volume{ + VolumeId: aws.String("vol-test"), + Size: aws.Int64(1), + AvailabilityZone: aws.String(defaultZone), + }, + modifiedVolumeError: awserr.New("IncorrectModificationState", "", nil), + descModVolume: &ec2.DescribeVolumesModificationsOutput{ + VolumesModifications: []*ec2.VolumeModification{ + { + VolumeId: aws.String("vol-test"), + TargetSize: aws.Int64(2), + ModificationState: aws.String(ec2.VolumeModificationStateCompleted), + }, + }, + }, + reqSizeGiB: 2, + expErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockEC2 := mocks.NewMockEC2(mockCtrl) + c := newCloud(mockEC2) + + ctx := context.Background() + if tc.existingVolume != nil || tc.existingVolumeError != nil { + mockEC2.EXPECT().DescribeVolumesWithContext(gomock.Eq(ctx), gomock.Any()).Return( + &ec2.DescribeVolumesOutput{ + Volumes: []*ec2.Volume{tc.existingVolume}, + }, tc.existingVolumeError).AnyTimes() + } + if tc.modifiedVolume != nil || tc.modifiedVolumeError != nil { + mockEC2.EXPECT().ModifyVolumeWithContext(gomock.Eq(ctx), gomock.Any()).Return(tc.modifiedVolume, tc.modifiedVolumeError).AnyTimes() + } + if tc.descModVolume != nil { + mockEC2.EXPECT().DescribeVolumesModificationsWithContext(gomock.Eq(ctx), gomock.Any()).Return(tc.descModVolume, nil).AnyTimes() + } + + newSize, err := c.ResizeDisk(ctx, tc.volumeID, util.GiBToBytes(tc.reqSizeGiB)) + if err != nil { + if tc.expErr == nil { + t.Fatalf("ResizeDisk() failed: expected no error, got: %v", err) + } + } else { + if tc.expErr != nil { + t.Fatal("ResizeDisk() failed: expected error, got nothing") + } else { + if tc.reqSizeGiB != newSize { + t.Fatalf("ResizeDisk() failed: expected capacity %d, got %d", tc.reqSizeGiB, newSize) + } + } + } + + mockCtrl.Finish() + }) + } +} + func TestGetSnapshotByName(t *testing.T) { testCases := []struct { name string diff --git a/pkg/cloud/mocks/mock_ec2.go b/pkg/cloud/mocks/mock_ec2.go index 1ee105dd1e..a9bf45ab88 100644 --- a/pkg/cloud/mocks/mock_ec2.go +++ b/pkg/cloud/mocks/mock_ec2.go @@ -175,6 +175,26 @@ func (mr *MockEC2MockRecorder) DescribeSnapshotsWithContext(arg0, arg1 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSnapshotsWithContext", reflect.TypeOf((*MockEC2)(nil).DescribeSnapshotsWithContext), varargs...) } +// DescribeVolumesModificationsWithContext mocks base method +func (m *MockEC2) DescribeVolumesModificationsWithContext(arg0 aws.Context, arg1 *ec2.DescribeVolumesModificationsInput, arg2 ...request.Option) (*ec2.DescribeVolumesModificationsOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeVolumesModificationsWithContext", varargs...) + ret0, _ := ret[0].(*ec2.DescribeVolumesModificationsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeVolumesModificationsWithContext indicates an expected call of DescribeVolumesModificationsWithContext +func (mr *MockEC2MockRecorder) DescribeVolumesModificationsWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeVolumesModificationsWithContext", reflect.TypeOf((*MockEC2)(nil).DescribeVolumesModificationsWithContext), varargs...) +} + // DescribeVolumesWithContext mocks base method func (m *MockEC2) DescribeVolumesWithContext(arg0 aws.Context, arg1 *ec2.DescribeVolumesInput, arg2 ...request.Option) (*ec2.DescribeVolumesOutput, error) { m.ctrl.T.Helper() @@ -214,3 +234,23 @@ func (mr *MockEC2MockRecorder) DetachVolumeWithContext(arg0, arg1 interface{}, a varargs := append([]interface{}{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetachVolumeWithContext", reflect.TypeOf((*MockEC2)(nil).DetachVolumeWithContext), varargs...) } + +// ModifyVolumeWithContext mocks base method +func (m *MockEC2) ModifyVolumeWithContext(arg0 aws.Context, arg1 *ec2.ModifyVolumeInput, arg2 ...request.Option) (*ec2.ModifyVolumeOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ModifyVolumeWithContext", varargs...) + ret0, _ := ret[0].(*ec2.ModifyVolumeOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ModifyVolumeWithContext indicates an expected call of ModifyVolumeWithContext +func (mr *MockEC2MockRecorder) ModifyVolumeWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyVolumeWithContext", reflect.TypeOf((*MockEC2)(nil).ModifyVolumeWithContext), varargs...) +} diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 27cf2f5fc7..db23a3e58e 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -46,6 +46,7 @@ var ( csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME, csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT, csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS, + csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, } ) @@ -313,6 +314,35 @@ func (d *controllerService) ValidateVolumeCapabilities(ctx context.Context, req }, nil } +func (d *controllerService) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { + klog.V(4).Infof("ControllerExpandVolume: called with args %+v", *req) + volumeID := req.GetVolumeId() + if len(volumeID) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume ID not provided") + } + + capRange := req.GetCapacityRange() + if capRange == nil { + return nil, status.Error(codes.InvalidArgument, "Capacity range not provided") + } + + newSize := util.RoundUpBytes(capRange.GetRequiredBytes()) + maxVolSize := capRange.GetLimitBytes() + if maxVolSize > 0 && maxVolSize < newSize { + return nil, status.Error(codes.InvalidArgument, "After round-up, volume size exceeds the limit specified") + } + + actualSizeGiB, err := d.cloud.ResizeDisk(ctx, volumeID, newSize) + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not resize volume %q: %v", volumeID, err) + } + + return &csi.ControllerExpandVolumeResponse{ + CapacityBytes: util.GiBToBytes(actualSizeGiB), + NodeExpansionRequired: true, + }, nil +} + func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) bool { hasSupport := func(cap *csi.VolumeCapability) bool { for _, c := range volumeCaps { @@ -432,10 +462,6 @@ func (d *controllerService) ListSnapshots(ctx context.Context, req *csi.ListSnap return response, nil } -func (d *Driver) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, "") -} - // pickAvailabilityZone selects 1 zone given topology requirement. // if not found, empty string is returned. func pickAvailabilityZone(requirement *csi.TopologyRequirement) string { diff --git a/pkg/driver/controller_test.go b/pkg/driver/controller_test.go index f12ff7807f..fff2edb1de 100644 --- a/pkg/driver/controller_test.go +++ b/pkg/driver/controller_test.go @@ -1719,3 +1719,83 @@ func TestControllerUnpublishVolume(t *testing.T) { t.Run(tc.name, tc.testFunc) } } + +func TestControllerExpandVolume(t *testing.T) { + testCases := []struct { + name string + req *csi.ControllerExpandVolumeRequest + newSize int64 + expResp *csi.ControllerExpandVolumeResponse + expError bool + }{ + { + name: "success normal", + req: &csi.ControllerExpandVolumeRequest{ + VolumeId: "vol-test", + CapacityRange: &csi.CapacityRange{ + RequiredBytes: 5 * util.GiB, + }, + }, + expResp: &csi.ControllerExpandVolumeResponse{ + CapacityBytes: 5 * util.GiB, + }, + }, + { + name: "fail empty request", + req: &csi.ControllerExpandVolumeRequest{}, + expError: true, + }, + { + name: "fail exceeds limit after round up", + req: &csi.ControllerExpandVolumeRequest{ + VolumeId: "vol-test", + CapacityRange: &csi.CapacityRange{ + RequiredBytes: 5*util.GiB + 1, // should round up to 6 GiB + LimitBytes: 5 * util.GiB, + }, + }, + expError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + var retSizeGiB int64 + if tc.newSize != 0 { + retSizeGiB = tc.newSize + } else { + retSizeGiB = util.BytesToGiB(tc.req.CapacityRange.GetRequiredBytes()) + } + + mockCloud := mocks.NewMockCloud(mockCtl) + mockCloud.EXPECT().ResizeDisk(gomock.Eq(ctx), gomock.Eq(tc.req.VolumeId), gomock.Any()).Return(retSizeGiB, nil).AnyTimes() + + awsDriver := controllerService{cloud: mockCloud} + + resp, err := awsDriver.ControllerExpandVolume(ctx, tc.req) + if err != nil { + srvErr, ok := status.FromError(err) + if !ok { + t.Fatalf("Could not get error status code from error: %v", srvErr) + } + if !tc.expError { + t.Fatalf("Unexpected error: %v", err) + } + } else { + if tc.expError { + t.Fatalf("Expected error from ControllerExpandVolume, got nothing") + } + } + + sizeGiB := util.BytesToGiB(resp.GetCapacityBytes()) + expSizeGiB := util.BytesToGiB(tc.expResp.GetCapacityBytes()) + if sizeGiB != expSizeGiB { + t.Fatalf("Expected size %d GiB, got %d GiB", expSizeGiB, sizeGiB) + } + }) + } +} diff --git a/pkg/driver/mocks/mock_cloud.go b/pkg/driver/mocks/mock_cloud.go index cb5f47016b..0bfec15f8a 100644 --- a/pkg/driver/mocks/mock_cloud.go +++ b/pkg/driver/mocks/mock_cloud.go @@ -211,6 +211,21 @@ func (mr *MockCloudMockRecorder) ListSnapshots(arg0, arg1, arg2, arg3 interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSnapshots", reflect.TypeOf((*MockCloud)(nil).ListSnapshots), arg0, arg1, arg2, arg3) } +// ResizeDisk mocks base method +func (m *MockCloud) ResizeDisk(arg0 context.Context, arg1 string, arg2 int64) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResizeDisk", arg0, arg1, arg2) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResizeDisk indicates an expected call of ResizeDisk +func (mr *MockCloudMockRecorder) ResizeDisk(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResizeDisk", reflect.TypeOf((*MockCloud)(nil).ResizeDisk), arg0, arg1, arg2) +} + // WaitForAttachmentState mocks base method func (m *MockCloud) WaitForAttachmentState(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() diff --git a/pkg/driver/mocks/mock_mounter.go b/pkg/driver/mocks/mock_mounter.go index 35fb37840a..e06bcaa7bf 100644 --- a/pkg/driver/mocks/mock_mounter.go +++ b/pkg/driver/mocks/mock_mounter.go @@ -36,6 +36,7 @@ func (m *MockMounter) EXPECT() *MockMounterMockRecorder { // CleanSubPaths mocks base method func (m *MockMounter) CleanSubPaths(arg0, arg1 string) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CleanSubPaths", arg0, arg1) ret0, _ := ret[0].(error) return ret0 @@ -43,11 +44,13 @@ func (m *MockMounter) CleanSubPaths(arg0, arg1 string) error { // CleanSubPaths indicates an expected call of CleanSubPaths func (mr *MockMounterMockRecorder) CleanSubPaths(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanSubPaths", reflect.TypeOf((*MockMounter)(nil).CleanSubPaths), arg0, arg1) } // DeviceOpened mocks base method func (m *MockMounter) DeviceOpened(arg0 string) (bool, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeviceOpened", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) @@ -56,11 +59,13 @@ func (m *MockMounter) DeviceOpened(arg0 string) (bool, error) { // DeviceOpened indicates an expected call of DeviceOpened func (mr *MockMounterMockRecorder) DeviceOpened(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceOpened", reflect.TypeOf((*MockMounter)(nil).DeviceOpened), arg0) } // EvalHostSymlinks mocks base method func (m *MockMounter) EvalHostSymlinks(arg0 string) (string, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EvalHostSymlinks", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) @@ -69,11 +74,13 @@ func (m *MockMounter) EvalHostSymlinks(arg0 string) (string, error) { // EvalHostSymlinks indicates an expected call of EvalHostSymlinks func (mr *MockMounterMockRecorder) EvalHostSymlinks(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalHostSymlinks", reflect.TypeOf((*MockMounter)(nil).EvalHostSymlinks), arg0) } // ExistsPath mocks base method func (m *MockMounter) ExistsPath(arg0 string) (bool, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ExistsPath", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) @@ -82,11 +89,13 @@ func (m *MockMounter) ExistsPath(arg0 string) (bool, error) { // ExistsPath indicates an expected call of ExistsPath func (mr *MockMounterMockRecorder) ExistsPath(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsPath", reflect.TypeOf((*MockMounter)(nil).ExistsPath), arg0) } // FormatAndMount mocks base method func (m *MockMounter) FormatAndMount(arg0, arg1, arg2 string, arg3 []string) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FormatAndMount", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 @@ -94,11 +103,13 @@ func (m *MockMounter) FormatAndMount(arg0, arg1, arg2 string, arg3 []string) err // FormatAndMount indicates an expected call of FormatAndMount func (mr *MockMounterMockRecorder) FormatAndMount(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatAndMount", reflect.TypeOf((*MockMounter)(nil).FormatAndMount), arg0, arg1, arg2, arg3) } // GetDeviceName mocks base method func (m *MockMounter) GetDeviceName(arg0 string) (string, int, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDeviceName", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(int) @@ -108,11 +119,13 @@ func (m *MockMounter) GetDeviceName(arg0 string) (string, int, error) { // GetDeviceName indicates an expected call of GetDeviceName func (mr *MockMounterMockRecorder) GetDeviceName(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceName", reflect.TypeOf((*MockMounter)(nil).GetDeviceName), arg0) } // GetDeviceNameFromMount mocks base method func (m *MockMounter) GetDeviceNameFromMount(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDeviceNameFromMount", arg0, arg1) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) @@ -121,11 +134,13 @@ func (m *MockMounter) GetDeviceNameFromMount(arg0, arg1 string) (string, error) // GetDeviceNameFromMount indicates an expected call of GetDeviceNameFromMount func (mr *MockMounterMockRecorder) GetDeviceNameFromMount(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceNameFromMount", reflect.TypeOf((*MockMounter)(nil).GetDeviceNameFromMount), arg0, arg1) } // GetFSGroup mocks base method func (m *MockMounter) GetFSGroup(arg0 string) (int64, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFSGroup", arg0) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) @@ -134,11 +149,13 @@ func (m *MockMounter) GetFSGroup(arg0 string) (int64, error) { // GetFSGroup indicates an expected call of GetFSGroup func (mr *MockMounterMockRecorder) GetFSGroup(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFSGroup", reflect.TypeOf((*MockMounter)(nil).GetFSGroup), arg0) } // GetFileType mocks base method func (m *MockMounter) GetFileType(arg0 string) (mount.FileType, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFileType", arg0) ret0, _ := ret[0].(mount.FileType) ret1, _ := ret[1].(error) @@ -147,11 +164,13 @@ func (m *MockMounter) GetFileType(arg0 string) (mount.FileType, error) { // GetFileType indicates an expected call of GetFileType func (mr *MockMounterMockRecorder) GetFileType(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileType", reflect.TypeOf((*MockMounter)(nil).GetFileType), arg0) } // GetMode mocks base method func (m *MockMounter) GetMode(arg0 string) (os.FileMode, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetMode", arg0) ret0, _ := ret[0].(os.FileMode) ret1, _ := ret[1].(error) @@ -160,11 +179,13 @@ func (m *MockMounter) GetMode(arg0 string) (os.FileMode, error) { // GetMode indicates an expected call of GetMode func (mr *MockMounterMockRecorder) GetMode(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMode", reflect.TypeOf((*MockMounter)(nil).GetMode), arg0) } // GetMountRefs mocks base method func (m *MockMounter) GetMountRefs(arg0 string) ([]string, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetMountRefs", arg0) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) @@ -173,11 +194,13 @@ func (m *MockMounter) GetMountRefs(arg0 string) ([]string, error) { // GetMountRefs indicates an expected call of GetMountRefs func (mr *MockMounterMockRecorder) GetMountRefs(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMountRefs", reflect.TypeOf((*MockMounter)(nil).GetMountRefs), arg0) } // GetSELinuxSupport mocks base method func (m *MockMounter) GetSELinuxSupport(arg0 string) (bool, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSELinuxSupport", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) @@ -186,11 +209,13 @@ func (m *MockMounter) GetSELinuxSupport(arg0 string) (bool, error) { // GetSELinuxSupport indicates an expected call of GetSELinuxSupport func (mr *MockMounterMockRecorder) GetSELinuxSupport(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSELinuxSupport", reflect.TypeOf((*MockMounter)(nil).GetSELinuxSupport), arg0) } // IsLikelyNotMountPoint mocks base method func (m *MockMounter) IsLikelyNotMountPoint(arg0 string) (bool, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsLikelyNotMountPoint", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) @@ -199,11 +224,13 @@ func (m *MockMounter) IsLikelyNotMountPoint(arg0 string) (bool, error) { // IsLikelyNotMountPoint indicates an expected call of IsLikelyNotMountPoint func (mr *MockMounterMockRecorder) IsLikelyNotMountPoint(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLikelyNotMountPoint", reflect.TypeOf((*MockMounter)(nil).IsLikelyNotMountPoint), arg0) } // IsMountPointMatch mocks base method func (m *MockMounter) IsMountPointMatch(arg0 mount.MountPoint, arg1 string) bool { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsMountPointMatch", arg0, arg1) ret0, _ := ret[0].(bool) return ret0 @@ -211,11 +238,13 @@ func (m *MockMounter) IsMountPointMatch(arg0 mount.MountPoint, arg1 string) bool // IsMountPointMatch indicates an expected call of IsMountPointMatch func (mr *MockMounterMockRecorder) IsMountPointMatch(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMountPointMatch", reflect.TypeOf((*MockMounter)(nil).IsMountPointMatch), arg0, arg1) } // IsNotMountPoint mocks base method func (m *MockMounter) IsNotMountPoint(arg0 string) (bool, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsNotMountPoint", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) @@ -224,11 +253,13 @@ func (m *MockMounter) IsNotMountPoint(arg0 string) (bool, error) { // IsNotMountPoint indicates an expected call of IsNotMountPoint func (mr *MockMounterMockRecorder) IsNotMountPoint(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNotMountPoint", reflect.TypeOf((*MockMounter)(nil).IsNotMountPoint), arg0) } // List mocks base method func (m *MockMounter) List() ([]mount.MountPoint, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") ret0, _ := ret[0].([]mount.MountPoint) ret1, _ := ret[1].(error) @@ -237,11 +268,13 @@ func (m *MockMounter) List() ([]mount.MountPoint, error) { // List indicates an expected call of List func (mr *MockMounterMockRecorder) List() *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockMounter)(nil).List)) } // MakeDir mocks base method func (m *MockMounter) MakeDir(arg0 string) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "MakeDir", arg0) ret0, _ := ret[0].(error) return ret0 @@ -249,11 +282,13 @@ func (m *MockMounter) MakeDir(arg0 string) error { // MakeDir indicates an expected call of MakeDir func (mr *MockMounterMockRecorder) MakeDir(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeDir", reflect.TypeOf((*MockMounter)(nil).MakeDir), arg0) } // MakeFile mocks base method func (m *MockMounter) MakeFile(arg0 string) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "MakeFile", arg0) ret0, _ := ret[0].(error) return ret0 @@ -261,11 +296,13 @@ func (m *MockMounter) MakeFile(arg0 string) error { // MakeFile indicates an expected call of MakeFile func (mr *MockMounterMockRecorder) MakeFile(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeFile", reflect.TypeOf((*MockMounter)(nil).MakeFile), arg0) } // MakeRShared mocks base method func (m *MockMounter) MakeRShared(arg0 string) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "MakeRShared", arg0) ret0, _ := ret[0].(error) return ret0 @@ -273,11 +310,13 @@ func (m *MockMounter) MakeRShared(arg0 string) error { // MakeRShared indicates an expected call of MakeRShared func (mr *MockMounterMockRecorder) MakeRShared(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeRShared", reflect.TypeOf((*MockMounter)(nil).MakeRShared), arg0) } // Mount mocks base method func (m *MockMounter) Mount(arg0, arg1, arg2 string, arg3 []string) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Mount", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 @@ -285,11 +324,13 @@ func (m *MockMounter) Mount(arg0, arg1, arg2 string, arg3 []string) error { // Mount indicates an expected call of Mount func (mr *MockMounterMockRecorder) Mount(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mount", reflect.TypeOf((*MockMounter)(nil).Mount), arg0, arg1, arg2, arg3) } // PathIsDevice mocks base method func (m *MockMounter) PathIsDevice(arg0 string) (bool, error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PathIsDevice", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) @@ -298,11 +339,13 @@ func (m *MockMounter) PathIsDevice(arg0 string) (bool, error) { // PathIsDevice indicates an expected call of PathIsDevice func (mr *MockMounterMockRecorder) PathIsDevice(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PathIsDevice", reflect.TypeOf((*MockMounter)(nil).PathIsDevice), arg0) } // PrepareSafeSubpath mocks base method func (m *MockMounter) PrepareSafeSubpath(arg0 mount.Subpath) (string, func(), error) { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PrepareSafeSubpath", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(func()) @@ -312,11 +355,13 @@ func (m *MockMounter) PrepareSafeSubpath(arg0 mount.Subpath) (string, func(), er // PrepareSafeSubpath indicates an expected call of PrepareSafeSubpath func (mr *MockMounterMockRecorder) PrepareSafeSubpath(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareSafeSubpath", reflect.TypeOf((*MockMounter)(nil).PrepareSafeSubpath), arg0) } // Run mocks base method func (m *MockMounter) Run(arg0 string, arg1 ...string) ([]byte, error) { + m.ctrl.T.Helper() varargs := []interface{}{arg0} for _, a := range arg1 { varargs = append(varargs, a) @@ -329,12 +374,14 @@ func (m *MockMounter) Run(arg0 string, arg1 ...string) ([]byte, error) { // Run indicates an expected call of Run func (mr *MockMounterMockRecorder) Run(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockMounter)(nil).Run), varargs...) } // SafeMakeDir mocks base method func (m *MockMounter) SafeMakeDir(arg0, arg1 string, arg2 os.FileMode) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SafeMakeDir", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 @@ -342,11 +389,13 @@ func (m *MockMounter) SafeMakeDir(arg0, arg1 string, arg2 os.FileMode) error { // SafeMakeDir indicates an expected call of SafeMakeDir func (mr *MockMounterMockRecorder) SafeMakeDir(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeMakeDir", reflect.TypeOf((*MockMounter)(nil).SafeMakeDir), arg0, arg1, arg2) } // Unmount mocks base method func (m *MockMounter) Unmount(arg0 string) error { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Unmount", arg0) ret0, _ := ret[0].(error) return ret0 @@ -354,5 +403,6 @@ func (m *MockMounter) Unmount(arg0 string) error { // Unmount indicates an expected call of Unmount func (mr *MockMounterMockRecorder) Unmount(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unmount", reflect.TypeOf((*MockMounter)(nil).Unmount), arg0) } diff --git a/pkg/driver/node.go b/pkg/driver/node.go index d5863acbbd..2d67b09c9c 100644 --- a/pkg/driver/node.go +++ b/pkg/driver/node.go @@ -30,6 +30,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/klog" + "k8s.io/kubernetes/pkg/util/mount" + "k8s.io/kubernetes/pkg/util/resizefs" ) const ( @@ -61,6 +63,7 @@ var ( // nodeCaps represents the capability of node service. nodeCaps = []csi.NodeServiceCapability_RPC_Type{ csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME, + csi.NodeServiceCapability_RPC_EXPAND_VOLUME, } ) @@ -227,6 +230,39 @@ func (d *nodeService) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstag return &csi.NodeUnstageVolumeResponse{}, nil } +func (d *nodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { + klog.V(4).Infof("NodeExpandVolume: called with args %+v", *req) + volumeID := req.GetVolumeId() + if len(volumeID) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume ID not provided") + } + + args := []string{"-o", "source", "--noheadings", "--target", req.GetVolumePath()} + output, err := d.mounter.Run("findmnt", args...) + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not determine device path: %v", err) + + } + + devicePath := strings.TrimSpace(string(output)) + if len(devicePath) == 0 { + return nil, status.Errorf(codes.Internal, "Could not get valid device for mount path: %q", req.GetVolumePath()) + } + + // TODO: refactor Mounter to expose a mount.SafeFormatAndMount object + r := resizefs.NewResizeFs(&mount.SafeFormatAndMount{ + Interface: mount.New(""), + Exec: mount.NewOsExec(), + }) + + // TODO: lock per volume ID to have some idempotency + if _, err := r.Resize(devicePath, req.GetVolumePath()); err != nil { + return nil, status.Errorf(codes.Internal, "Could not resize volume %q (%q): %v", volumeID, devicePath, err) + } + + return &csi.NodeExpandVolumeResponse{}, nil +} + func (d *nodeService) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { klog.V(4).Infof("NodePublishVolume: called with args %+v", *req) volumeID := req.GetVolumeId() @@ -328,10 +364,6 @@ func (d *nodeService) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoReque }, nil } -func (d *nodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, "") -} - func (d *nodeService) nodePublishVolumeForBlock(req *csi.NodePublishVolumeRequest, mountOptions []string) error { target := req.GetTargetPath() volumeID := req.GetVolumeId() diff --git a/pkg/driver/node_test.go b/pkg/driver/node_test.go index 57fec4847c..7097545bfb 100644 --- a/pkg/driver/node_test.go +++ b/pkg/driver/node_test.go @@ -1042,6 +1042,13 @@ func TestNodeGetCapabilities(t *testing.T) { }, }, }, + { + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME, + }, + }, + }, } expResp := &csi.NodeGetCapabilitiesResponse{Capabilities: caps} diff --git a/tests/sanity/fake_cloud_provider.go b/tests/sanity/fake_cloud_provider.go index c7e374f654..2b532959a4 100644 --- a/tests/sanity/fake_cloud_provider.go +++ b/tests/sanity/fake_cloud_provider.go @@ -211,3 +211,13 @@ func (c *fakeCloudProvider) ListSnapshots(ctx context.Context, volumeID string, }, nil } + +func (c *fakeCloudProvider) ResizeDisk(ctx context.Context, volumeID string, newSize int64) (int64, error) { + for volName, f := range c.disks { + if f.Disk.VolumeID == volumeID { + c.disks[volName].CapacityGiB = newSize + return newSize, nil + } + } + return 0, cloud.ErrNotFound +}