Skip to content

Commit d228f4a

Browse files
authored
feat: add resize fs (#382)
* feat: resize volume when staged * chore: advertise VolumeExpansion as OFFLINE * chore: move function to helper * feat: resize fs on NodeExpandVolume * test: ignore shallow err * test: add mocking for Format and Resize * test: improve NodeExpandVolume * test(chainsaw): output CSI logs on error * fix: nil checks * fix: missing Formater * chore: point to SafeFormatAndMount * fix: test * fix: resize deps need resize2fs, dumpe2fs and xfs_io * test: resizing disk size * fix: validate input first * test: expand read only * test: df 9.8G * test: fix pod create * test: fix pod creation * chore: revert grpc error * test: partition * chore: grpc error change * test: getReadOnlyFromCapability vc nil * chore: use grpc errors * fix: return notfound on bad volumename * chore: move functions in helpers/errors * doc: offline resizing
1 parent 63ff42f commit d228f4a

File tree

31 files changed

+922
-97
lines changed

31 files changed

+922
-97
lines changed

.golangci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ issues:
156156
linters:
157157
- gosec
158158
- gas
159+
160+
- text: 'shadow: declaration of "(err|ctx)" shadows declaration at'
161+
linters: [govet]
159162

160163
exclude-use-default: false
161164
new: false

Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ FROM alpine:3.20.3
2020
LABEL maintainers="Linode"
2121
LABEL description="Linode CSI Driver"
2222

23-
RUN apk add --no-cache e2fsprogs findmnt blkid cryptsetup
24-
RUN apk add --no-cache xfsprogs=6.2.0-r2 --repository=http://dl-cdn.alpinelinux.org/alpine/v3.18/main
23+
RUN apk add --no-cache e2fsprogs e2fsprogs-extra findmnt blkid cryptsetup
24+
RUN apk add --no-cache xfsprogs=6.2.0-r2 xfsprogs-extra=6.2.0-r2 --repository=http://dl-cdn.alpinelinux.org/alpine/v3.18/main
2525

2626
COPY --from=builder /bin/linode-blockstorage-csi-driver /linode
2727

Dockerfile.dev

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ RUN apk add \
1313
cryptsetup-dev \
1414
curl \
1515
e2fsprogs \
16+
e2fsprogs-extra \
1617
findmnt \
1718
gcc \
1819
lsblk \
1920
make \
2021
musl-dev \
2122
pkgconfig \
22-
xfsprogs
23+
xfsprogs \
24+
xfsprogs-extra
2325

2426
COPY go.mod go.sum ./
2527
RUN go mod tidy

docs/deployment.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,10 @@ kubectl apply -f https://raw.githubusercontent.com/linode/linode-blockstorage-cs
159159
160160
**Note:** To support this change, block storage volume attachments are no longer persisted across reboots.
161161
162-
<!-- Add note about volume resizing limitations -->
162+
4. **Offline Volume Resizing**
163+
164+
- The CSI driver supports offline volume resizing. This means that the volume must be unmounted from all nodes before resizing.
165+
- To resize a volume, update the `spec.resources.requests.storage` field in the PersistentVolumeClaim (PVC) manifest and apply the changes.
166+
- The CSI driver will automatically resize the underlying Linode Block Storage Volume to match the new size specified in the PVC.
167+
- The volume must be unmounted from all nodes before resizing.
168+
It could be unmounted by deleting the pod using the volume or by using the `kubectl delete pod <pod-name>` command.

internal/driver/driver.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ import (
2525
"github.com/container-storage-interface/spec/lib/go/csi"
2626
"google.golang.org/grpc/codes"
2727
"google.golang.org/grpc/status"
28-
"k8s.io/mount-utils"
2928

3029
devicemanager "github.com/linode/linode-blockstorage-csi-driver/pkg/device-manager"
3130
linodeclient "github.com/linode/linode-blockstorage-csi-driver/pkg/linode-client"
3231
"github.com/linode/linode-blockstorage-csi-driver/pkg/logger"
32+
mountmanager "github.com/linode/linode-blockstorage-csi-driver/pkg/mount-manager"
3333
"github.com/linode/linode-blockstorage-csi-driver/pkg/observability"
3434
)
3535

@@ -80,8 +80,9 @@ func GetLinodeDriver(ctx context.Context) *LinodeDriver {
8080
func (linodeDriver *LinodeDriver) SetupLinodeDriver(
8181
ctx context.Context,
8282
linodeClient linodeclient.LinodeClient,
83-
mounter *mount.SafeFormatAndMount,
83+
mounter *mountmanager.SafeFormatAndMount,
8484
deviceUtils devicemanager.DeviceUtils,
85+
resizeFs mountmanager.ResizeFSer,
8586
metadata Metadata,
8687
name,
8788
vendorVersion,
@@ -118,7 +119,7 @@ func (linodeDriver *LinodeDriver) SetupLinodeDriver(
118119
linodeDriver.volumeLabelPrefix = volumeLabelPrefix
119120

120121
log.V(2).Info("Setting up RPC Servers")
121-
linodeDriver.ns, err = NewNodeServer(ctx, linodeDriver, mounter, deviceUtils, linodeClient, metadata, encrypt)
122+
linodeDriver.ns, err = NewNodeServer(ctx, linodeDriver, mounter, deviceUtils, linodeClient, metadata, encrypt, resizeFs)
122123
if err != nil {
123124
return fmt.Errorf("new node server: %w", err)
124125
}

internal/driver/driver_test.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/linode/linode-blockstorage-csi-driver/mocks"
1313
linodeclient "github.com/linode/linode-blockstorage-csi-driver/pkg/linode-client"
14+
mountmanager "github.com/linode/linode-blockstorage-csi-driver/pkg/mount-manager"
1415
)
1516

1617
var (
@@ -30,14 +31,17 @@ func TestDriverSuite(t *testing.T) {
3031
mockCtrl := gomock.NewController(t)
3132
defer mockCtrl.Finish()
3233

33-
mounter := &mount.SafeFormatAndMount{
34-
Interface: mocks.NewMockMounter(mockCtrl),
35-
Exec: mocks.NewMockExecutor(mockCtrl),
34+
mounter := &mountmanager.SafeFormatAndMount{
35+
SafeFormatAndMount: &mount.SafeFormatAndMount{
36+
Interface: mocks.NewMockMounter(mockCtrl),
37+
Exec: mocks.NewMockExecutor(mockCtrl),
38+
},
3639
}
3740
deviceUtils := mocks.NewMockDeviceUtils(mockCtrl)
3841
fileSystem := mocks.NewMockFileSystem(mockCtrl)
3942
cryptSetup := mocks.NewMockCryptSetupClient(mockCtrl)
4043
encrypt := NewLuksEncryption(mounter.Exec, fileSystem, cryptSetup)
44+
resizeFs := mocks.NewMockResizeFSer(mockCtrl)
4145

4246
fakeCloudProvider, err := linodeclient.NewLinodeClient("dummy", fmt.Sprintf("LinodeCSI/%s", vendorVersion), "")
4347
if err != nil {
@@ -57,7 +61,7 @@ func TestDriverSuite(t *testing.T) {
5761
metricsPort := "10251"
5862
enableTracing := "true"
5963
tracingPort := "4318"
60-
if err := linodeDriver.SetupLinodeDriver(context.Background(), fakeCloudProvider, mounter, deviceUtils, md, driver, vendorVersion, bsPrefix, encrypt, enableMetrics, metricsPort, enableTracing, tracingPort); err != nil {
64+
if err := linodeDriver.SetupLinodeDriver(context.Background(), fakeCloudProvider, mounter, deviceUtils, resizeFs, md, driver, vendorVersion, bsPrefix, encrypt, enableMetrics, metricsPort, enableTracing, tracingPort); err != nil {
6165
t.Fatalf("Failed to setup Linode Driver: %v", err)
6266
}
6367

internal/driver/errors.go

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package driver
22

33
import (
4+
"errors"
5+
46
"github.com/container-storage-interface/spec/lib/go/csi"
57
"google.golang.org/grpc/codes"
68
"google.golang.org/grpc/status"
@@ -53,6 +55,9 @@ var (
5355
// operation was not specified, despite indicating a new volume should be
5456
// created by cloning an existing one.
5557
errNoSourceVolume = status.Error(codes.InvalidArgument, "no volume content source specified")
58+
59+
ErrNoVolumeCapability = errors.New("volume capability is required")
60+
ErrNoAccessMode = errors.New("access mode is nil")
5661
)
5762

5863
// errRegionMismatch returns an error indicating a volume is in gotRegion, but

internal/driver/identityserver.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func (linodeIdentity *IdentityServer) GetPluginCapabilities(ctx context.Context,
105105
// 2. Delete and recreate the pod that is using the PVC(or scale replicas accordingly)
106106
// 3. This operation should detach and re-attach the volume to the newly created pod allowing you to use the updated size
107107
VolumeExpansion: &csi.PluginCapability_VolumeExpansion{
108-
Type: csi.PluginCapability_VolumeExpansion_ONLINE,
108+
Type: csi.PluginCapability_VolumeExpansion_OFFLINE,
109109
},
110110
},
111111
},

internal/driver/identityserver_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func TestIdentityServer_GetPluginCapabilities(t *testing.T) {
127127
{
128128
Type: &csi.PluginCapability_VolumeExpansion_{
129129
VolumeExpansion: &csi.PluginCapability_VolumeExpansion{
130-
Type: csi.PluginCapability_VolumeExpansion_ONLINE,
130+
Type: csi.PluginCapability_VolumeExpansion_OFFLINE,
131131
},
132132
},
133133
},

internal/driver/nodeserver.go

+96-29
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,35 @@ limitations under the License.
1515
*/
1616

1717
import (
18-
"encoding/json"
18+
"errors"
1919
"fmt"
2020
"strconv"
2121
"sync"
2222
"time"
2323

2424
"github.com/container-storage-interface/spec/lib/go/csi"
25-
"github.com/linode/linodego"
2625
"golang.org/x/net/context"
26+
"google.golang.org/grpc/codes"
27+
"google.golang.org/grpc/status"
2728
"k8s.io/mount-utils"
2829

2930
devicemanager "github.com/linode/linode-blockstorage-csi-driver/pkg/device-manager"
3031
filesystem "github.com/linode/linode-blockstorage-csi-driver/pkg/filesystem"
3132
linodeclient "github.com/linode/linode-blockstorage-csi-driver/pkg/linode-client"
3233
linodevolumes "github.com/linode/linode-blockstorage-csi-driver/pkg/linode-volumes"
3334
"github.com/linode/linode-blockstorage-csi-driver/pkg/logger"
35+
mountmanager "github.com/linode/linode-blockstorage-csi-driver/pkg/mount-manager"
3436
"github.com/linode/linode-blockstorage-csi-driver/pkg/observability"
3537
)
3638

3739
type NodeServer struct {
3840
driver *LinodeDriver
39-
mounter *mount.SafeFormatAndMount
41+
mounter *mountmanager.SafeFormatAndMount
4042
deviceutils devicemanager.DeviceUtils
4143
client linodeclient.LinodeClient
4244
metadata Metadata
4345
encrypt Encryption
46+
resizeFs mountmanager.ResizeFSer
4447
// TODO: Only lock mutually exclusive calls and make locking more fine grained
4548
mux sync.Mutex
4649

@@ -49,7 +52,7 @@ type NodeServer struct {
4952

5053
var _ csi.NodeServer = &NodeServer{}
5154

52-
func NewNodeServer(ctx context.Context, linodeDriver *LinodeDriver, mounter *mount.SafeFormatAndMount, deviceUtils devicemanager.DeviceUtils, client linodeclient.LinodeClient, metadata Metadata, encrypt Encryption) (*NodeServer, error) {
55+
func NewNodeServer(ctx context.Context, linodeDriver *LinodeDriver, mounter *mountmanager.SafeFormatAndMount, deviceUtils devicemanager.DeviceUtils, client linodeclient.LinodeClient, metadata Metadata, encrypt Encryption, resize mountmanager.ResizeFSer) (*NodeServer, error) {
5356
log := logger.GetLogger(ctx)
5457

5558
log.V(4).Info("Creating new NodeServer")
@@ -78,6 +81,7 @@ func NewNodeServer(ctx context.Context, linodeDriver *LinodeDriver, mounter *mou
7881
client: client,
7982
metadata: metadata,
8083
encrypt: encrypt,
84+
resizeFs: resize,
8185
}
8286

8387
log.V(4).Info("NodeServer created successfully")
@@ -199,25 +203,39 @@ func (ns *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol
199203
ns.mux.Lock()
200204
defer ns.mux.Unlock()
201205

206+
// Part 1: Validate request object
207+
202208
// Before to functionStartTime, validate the request object (NodeStageVolumeRequest)
203209
log.V(4).Info("Validating request", "volumeID", volumeID)
204210
if err := validateNodeStageVolumeRequest(ctx, req); err != nil {
205211
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Failed, functionStartTime)
206-
return nil, err
212+
return nil, status.Error(codes.InvalidArgument, err.Error())
213+
}
214+
215+
// Part 2: Get information of attached device
216+
217+
readonly, err := getReadOnlyFromCapability(req.GetVolumeCapability())
218+
if err != nil {
219+
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Failed, functionStartTime)
220+
return nil, errors.Join(errInternal("failed to get readonly from volume capability: %v", err), err)
207221
}
208222

223+
stagingTargetPath := req.GetStagingTargetPath()
224+
209225
// Get the LinodeVolumeKey which we need to find the device path
210226
LinodeVolumeKey, err := linodevolumes.ParseLinodeVolumeKey(volumeID)
211227
if err != nil {
212228
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Failed, functionStartTime)
213-
return nil, err
229+
return nil, errors.Join(status.Errorf(codes.InvalidArgument, "volume not found: %v", err), err)
214230
}
215231

216232
// Get device path of attached device
217233
partition := ""
218234

219-
if part, ok := req.GetVolumeContext()["partition"]; ok {
220-
partition = part
235+
if vc := req.GetVolumeContext(); vc != nil {
236+
if part, ok := vc["partition"]; ok {
237+
partition = part
238+
}
221239
}
222240

223241
log.V(4).Info("Finding device path", "volumeID", volumeID)
@@ -227,9 +245,10 @@ func (ns *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol
227245
return nil, err
228246
}
229247

230-
// Check if staging target path is a valid mount point.
231-
log.V(4).Info("Ensuring staging target path is a valid mount point", "volumeID", volumeID, "stagingTargetPath", req.GetStagingTargetPath())
232-
notMnt, err := ns.ensureMountPoint(ctx, req.GetStagingTargetPath(), filesystem.NewFileSystem())
248+
// Part 3: check if staging target path is a valid mount point.
249+
250+
log.V(4).Info("Ensuring staging target path is a valid mount point", "volumeID", volumeID, "stagingTargetPath", stagingTargetPath)
251+
notMnt, err := ns.ensureMountPoint(ctx, stagingTargetPath, filesystem.NewFileSystem())
233252
if err != nil {
234253
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Failed, functionStartTime)
235254
return nil, err
@@ -244,7 +263,7 @@ func (ns *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol
244263
245264
*/
246265
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Failed, functionStartTime)
247-
log.V(4).Info("Staging target path is already a mount point", "volumeID", volumeID, "stagingTargetPath", req.GetStagingTargetPath())
266+
log.V(4).Info("Staging target path is already a mount point", "volumeID", volumeID, "stagingTargetPath", stagingTargetPath)
248267
return &csi.NodeStageVolumeResponse{}, nil
249268
}
250269

@@ -256,14 +275,27 @@ func (ns *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol
256275
return &csi.NodeStageVolumeResponse{}, nil
257276
}
258277

259-
// Mount device to stagingTargetPath
260-
// If LUKS is enabled, format the device accordingly
261-
log.V(4).Info("Mounting device", "volumeID", volumeID, "devicePath", devicePath, "stagingTargetPath", req.GetStagingTargetPath())
278+
// Part 4: Mount device and format if needed
279+
280+
log.V(4).Info("Mounting device", "volumeID", volumeID, "devicePath", devicePath, "stagingTargetPath", stagingTargetPath)
262281
if err := ns.mountVolume(ctx, devicePath, req); err != nil {
263282
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Failed, functionStartTime)
264283
return nil, err
265284
}
266285

286+
// Part 5: Resize fs
287+
288+
if !readonly {
289+
resized, err := ns.resize(devicePath, stagingTargetPath)
290+
if err != nil {
291+
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Failed, functionStartTime)
292+
return nil, errInternal("failed to resize volume %s: %v", volumeID, err)
293+
}
294+
if resized {
295+
log.V(4).Info("Successfully resized volume", "volumeID", volumeID)
296+
}
297+
}
298+
267299
// Record functionStatus metric
268300
observability.RecordMetrics(observability.NodeStageVolumeTotal, observability.NodeStageVolumeDuration, observability.Completed, functionStartTime)
269301

@@ -317,37 +349,72 @@ func (ns *NodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandV
317349
defer done()
318350

319351
functionStartTime := time.Now()
320-
volumeID := req.GetVolumeId()
321-
log.V(2).Info("Processing request", "volumeID", volumeID)
322352

353+
volumeID := req.GetVolumeId()
323354
// Validate req (NodeExpandVolumeRequest)
355+
324356
log.V(4).Info("Validating request", "volumeID", volumeID)
325357
if err := validateNodeExpandVolumeRequest(ctx, req); err != nil {
326358
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Failed, functionStartTime)
327-
return nil, err
359+
return nil, errors.Join(status.Error(codes.InvalidArgument, fmt.Sprintf("validation failed: %v", err)), err)
328360
}
329361

330-
// Check linode to see if a give volume exists by volume ID
331-
// Make call to linode api using the linode api client
362+
log.V(2).Info("Processing request", "volumeID", volumeID)
363+
332364
LinodeVolumeKey, err := linodevolumes.ParseLinodeVolumeKey(volumeID)
333365
log.V(4).Info("Processed LinodeVolumeKey", "LinodeVolumeKey", LinodeVolumeKey)
334366
if err != nil {
335-
// Node volume expansion is not supported yet. To meet the spec, we need to implement this.
336-
// For now, we'll return a not found error.
337-
338367
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Failed, functionStartTime)
339-
return nil, errNotFound("volume not found: %v", err)
368+
return nil, errors.Join(status.Errorf(codes.NotFound, "volume not found: %v", err), err)
369+
}
370+
371+
// We have no context for the partition, so we'll leave it empty
372+
partition := ""
373+
374+
volumePath := req.GetVolumePath()
375+
if volumePath == "" {
376+
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Completed, functionStartTime)
377+
return nil, status.Error(codes.InvalidArgument, "volume path must be provided")
378+
}
379+
380+
volumeCapability := req.GetVolumeCapability()
381+
// VolumeCapability is optional, if specified, use that as source of truth
382+
if volumeCapability != nil {
383+
if blk := volumeCapability.GetBlock(); blk != nil {
384+
// Noop for Block NodeExpandVolume
385+
log.V(4).Info("NodeExpandVolume: called. Since it is a block device, ignoring...", "volumeID", volumeID, "volumePath", volumePath)
386+
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Completed, functionStartTime)
387+
return &csi.NodeExpandVolumeResponse{}, nil
388+
}
389+
390+
readonly, err := getReadOnlyFromCapability(volumeCapability)
391+
if err != nil {
392+
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Failed, functionStartTime)
393+
return nil, errInternal("failed to check if capability for volume %s is readonly: %v", volumeID, err)
394+
}
395+
if readonly {
396+
log.V(4).Info("NodeExpandVolume succeeded", "volumeID", volumeID, "volumePath", volumePath)
397+
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Completed, functionStartTime)
398+
return &csi.NodeExpandVolumeResponse{}, nil
399+
}
340400
}
341-
jsonFilter, err := json.Marshal(map[string]string{"label": LinodeVolumeKey.Label})
401+
402+
devicePath, err := ns.findDevicePath(ctx, *LinodeVolumeKey, partition)
342403
if err != nil {
343404
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Failed, functionStartTime)
344-
return nil, errInternal("marshal json filter: %v", err)
405+
return nil, err
345406
}
346407

347-
log.V(4).Info("Listing volumes", "volumeID", volumeID)
348-
if _, err = ns.client.ListVolumes(ctx, linodego.NewListOptions(0, string(jsonFilter))); err != nil {
408+
// Resize the volume
409+
410+
resized, err := ns.resize(devicePath, volumePath)
411+
if err != nil {
349412
observability.RecordMetrics(observability.NodeExpandTotal, observability.NodeExpandDuration, observability.Failed, functionStartTime)
350-
return nil, errVolumeNotFound(LinodeVolumeKey.VolumeID)
413+
return nil, errInternal("failed to resize volume %s: %v", volumeID, err)
414+
}
415+
416+
if resized {
417+
log.V(4).Info("Successfully resized volume", "volumeID", volumeID)
351418
}
352419

353420
// Record functionStatus metric

0 commit comments

Comments
 (0)