Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ The following table lists the configurable parameters of the latest NFS CSI Driv
| `controller.runOnControlPlane` | run controller on control plane node |`false` |
| `controller.dnsPolicy` | dnsPolicy of controller driver, available values: `Default`, `ClusterFirstWithHostNet`, `ClusterFirst` | `ClusterFirstWithHostNet` |
| `controller.defaultOnDeletePolicy` | default policy for deleting subdirectory when deleting a volume, available values: `delete`, `retain`, `archive` | `delete` |
| `controller.enableSnapshotCompression` | enable compression when creating volume snapshots. When `false`, snapshots will be stored without gzip compression (using tar instead of tar.gz) | `true` |
| `controller.livenessProbe.healthPort ` | the health check port for liveness probe | `29652` |
| `controller.logLevel` | controller driver log level |`5` |
| `controller.workingMountDir` | working directory for provisioner to mount nfs shares temporarily | `/tmp` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ spec:
- "--working-mount-dir={{ .Values.controller.workingMountDir }}"
- "--default-ondelete-policy={{ .Values.controller.defaultOnDeletePolicy }}"
- "--use-tar-command-in-snapshot={{ .Values.controller.useTarCommandInSnapshot }}"
- "--enable-snapshot-compression={{ .Values.controller.enableSnapshotCompression }}"
env:
- name: NODE_ID
valueFrom:
Expand Down
1 change: 1 addition & 0 deletions charts/latest/csi-driver-nfs/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ controller:
runOnControlPlane: false
enableSnapshotter: true
useTarCommandInSnapshot: false
enableSnapshotCompression: true
livenessProbe:
healthPort: 29652
logLevel: 5
Expand Down
2 changes: 2 additions & 0 deletions cmd/nfsplugin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var (
volStatsCacheExpireInMinutes = flag.Int("vol-stats-cache-expire-in-minutes", 10, "The cache expire time in minutes for volume stats cache")
removeArchivedVolumePath = flag.Bool("remove-archived-volume-path", false, "remove archived volume path in DeleteVolume")
useTarCommandInSnapshot = flag.Bool("use-tar-command-in-snapshot", false, "use tar command to pack and unpack snapshot data")
enableSnapshotCompression = flag.Bool("enable-snapshot-compression", true, "enable compression when creating volume snapshots")
)

func main() {
Expand All @@ -60,6 +61,7 @@ func handle() {
VolStatsCacheExpireInMinutes: *volStatsCacheExpireInMinutes,
RemoveArchivedVolumePath: *removeArchivedVolumePath,
UseTarCommandInSnapshot: *useTarCommandInSnapshot,
EnableSnapshotCompression: *enableSnapshotCompression,
}
d := nfs.NewDriver(&driverOptions)
d.Run(false)
Expand Down
10 changes: 10 additions & 0 deletions docs/driver-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ server | NFS Server address | domain name `nfs-server.default.svc.cluster.local`
share | NFS share path | `/` | No | use share from source volume by default
mountOptions | mount options separated by comma during snapshot creation, e.g. `"nfsvers=4.1,sec=sys"` | | No | ""

### Driver Deployment Parameters

The following parameters can be set when deploying the driver:

Name | Meaning | Available Value | Mandatory | Default value
--- | --- | --- | --- | ---
`--enable-snapshot-compression` | enable compression when creating volume snapshots | `true`, `false` | No | `true`

> **Note:** When `--enable-snapshot-compression=false`, snapshots are stored without gzip compression (using `.tar` format instead of `.tar.gz`). This can significantly speed up snapshot creation and restoration for volumes containing already-compressed data. The driver automatically detects the archive format when restoring from a snapshot, ensuring backward compatibility with existing compressed snapshots.

### Tips
#### `subDir` parameter supports following pv/pvc metadata conversion
> if `subDir` value contains following strings, it would be converted into corresponding pv/pvc name or namespace
Expand Down
52 changes: 40 additions & 12 deletions pkg/nfs/controllerserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@ type nfsSnapshot struct {
src string
}

func (snap nfsSnapshot) archiveName() string {
return fmt.Sprintf("%v.tar.gz", snap.src)
func (snap nfsSnapshot) archiveName(enableCompression bool) string {
if enableCompression {
return fmt.Sprintf("%v.tar.gz", snap.src)
}
return fmt.Sprintf("%v.tar", snap.src)
}

// Ordering of elements in the CSI volume id.
Expand Down Expand Up @@ -377,7 +380,7 @@ func (cs *ControllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS
if err = os.MkdirAll(snapInternalVolPath, 0777); err != nil {
return nil, status.Errorf(codes.Internal, "failed to make subdirectory: %v", err)
}
if err := validateSnapshot(snapInternalVolPath, snapshot); err != nil {
if err := validateSnapshot(snapInternalVolPath, snapshot, cs.Driver.enableSnapshotCompression); err != nil {
return nil, err
}

Expand All @@ -391,15 +394,21 @@ func (cs *ControllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS
}()

srcPath := getInternalVolumePath(cs.Driver.workingMountDir, srcVol)
dstPath := filepath.Join(snapInternalVolPath, snapshot.archiveName())
dstPath := filepath.Join(snapInternalVolPath, snapshot.archiveName(cs.Driver.enableSnapshotCompression))

klog.V(2).Infof("tar %v -> %v", srcPath, dstPath)
if cs.Driver.useTarCommandInSnapshot {
if out, err := exec.Command("tar", "-C", srcPath, "-czvf", dstPath, ".").CombinedOutput(); err != nil {
var tarArgs []string
if cs.Driver.enableSnapshotCompression {
tarArgs = []string{"-C", srcPath, "-czvf", dstPath, "."}
} else {
tarArgs = []string{"-C", srcPath, "-cvf", dstPath, "."}
}
if out, err := exec.Command("tar", tarArgs...).CombinedOutput(); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create archive for snapshot: %v: %v", err, string(out))
}
} else {
if err := TarPack(srcPath, dstPath, true); err != nil {
if err := TarPack(srcPath, dstPath, cs.Driver.enableSnapshotCompression); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create archive for snapshot: %v", err)
}
}
Expand Down Expand Up @@ -551,16 +560,30 @@ func (cs *ControllerServer) copyFromSnapshot(ctx context.Context, req *csi.Creat
}()

// untar snapshot archive to dst path
snapPath := filepath.Join(getInternalVolumePath(cs.Driver.workingMountDir, snapVol), snap.archiveName())
snapInternalVolPath := getInternalVolumePath(cs.Driver.workingMountDir, snapVol)
// Try compressed archive first for backward compatibility, then uncompressed
enableCompression := true
snapPath := filepath.Join(snapInternalVolPath, snap.archiveName(true))
if _, err := os.Stat(snapPath); os.IsNotExist(err) {
// Try uncompressed archive
snapPath = filepath.Join(snapInternalVolPath, snap.archiveName(false))
enableCompression = false
}
dstPath := getInternalVolumePath(cs.Driver.workingMountDir, dstVol)
klog.V(2).Infof("copy volume from snapshot %v -> %v", snapPath, dstPath)

if cs.Driver.useTarCommandInSnapshot {
if out, err := exec.Command("tar", "-xzvf", snapPath, "-C", dstPath).CombinedOutput(); err != nil {
var tarArgs []string
if enableCompression {
tarArgs = []string{"-xzvf", snapPath, "-C", dstPath}
} else {
tarArgs = []string{"-xvf", snapPath, "-C", dstPath}
}
if out, err := exec.Command("tar", tarArgs...).CombinedOutput(); err != nil {
return status.Errorf(codes.Internal, "failed to copy volume for snapshot: %v: %v", err, string(out))
}
} else {
if err := TarUnpack(snapPath, dstPath, true); err != nil {
if err := TarUnpack(snapPath, dstPath, enableCompression); err != nil {
return status.Errorf(codes.Internal, "failed to copy volume for snapshot: %v", err)
}
}
Expand Down Expand Up @@ -839,7 +862,8 @@ func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) error {
}

// Validate snapshot after internal mount
func validateSnapshot(snapInternalVolPath string, snap *nfsSnapshot) error {
func validateSnapshot(snapInternalVolPath string, snap *nfsSnapshot, enableCompression bool) error {
expectedArchiveName := snap.archiveName(enableCompression)
return filepath.WalkDir(snapInternalVolPath, func(path string, d fs.DirEntry, err error) error {
if path == snapInternalVolPath {
// skip root
Expand All @@ -848,10 +872,14 @@ func validateSnapshot(snapInternalVolPath string, snap *nfsSnapshot) error {
if err != nil {
return err
}
if d.Name() != snap.archiveName() {
// Check if it's either compressed or uncompressed archive
compressedName := snap.archiveName(true)
uncompressedName := snap.archiveName(false)
if d.Name() != compressedName && d.Name() != uncompressedName {
// there should be just one archive in the snapshot path and archive name should match
return status.Errorf(codes.AlreadyExists, "snapshot with the same name but different source volume ID already exists: found %q, desired %q", d.Name(), snap.archiveName())
return status.Errorf(codes.AlreadyExists, "snapshot with the same name but different source volume ID already exists: found %q, desired %q", d.Name(), expectedArchiveName)
}
// If archive already exists (either compressed or uncompressed), snapshot was already created
return nil
})
}
Expand Down
152 changes: 150 additions & 2 deletions pkg/nfs/controllerserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@
func initTestController(_ *testing.T) *ControllerServer {
mounter := &mount.FakeMounter{MountPoints: []mount.MountPoint{}}
driver := NewDriver(&DriverOptions{
WorkingMountDir: "/tmp",
MountPermissions: 0777,
WorkingMountDir: "/tmp",
MountPermissions: 0777,
EnableSnapshotCompression: true,
})
driver.ns = NewNodeServer(driver, mounter)
cs := NewControllerServer(driver)
Expand Down Expand Up @@ -1159,3 +1160,150 @@
}
return fmt.Errorf("mismatch CreateSnapshotResponse in fields: %v", strings.Join(errs, ", "))
}

func initTestControllerWithOptions(t *testing.T, opts *DriverOptions) *ControllerServer {

Check failure on line 1164 in pkg/nfs/controllerserver_test.go

View workflow job for this annotation

GitHub Actions / Go Lint

unused-parameter: parameter 't' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 1164 in pkg/nfs/controllerserver_test.go

View workflow job for this annotation

GitHub Actions / Go Lint

unused-parameter: parameter 't' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 1164 in pkg/nfs/controllerserver_test.go

View workflow job for this annotation

GitHub Actions / Go Lint

unused-parameter: parameter 't' seems to be unused, consider removing or renaming it as _ (revive)
mounter := &mount.FakeMounter{MountPoints: []mount.MountPoint{}}
if opts.WorkingMountDir == "" {
opts.WorkingMountDir = "/tmp"
}
driver := NewDriver(opts)
driver.ns = NewNodeServer(driver, mounter)
cs := NewControllerServer(driver)
return cs
}

func TestCreateSnapshotWithoutCompression(t *testing.T) {
// Test creating a snapshot without compression
cs := initTestControllerWithOptions(t, &DriverOptions{
WorkingMountDir: "/tmp",
MountPermissions: 0777,
EnableSnapshotCompression: false,
})

// Setup: create source directory
srcPath := "/tmp/src-pv-name-no-compress/subdir"
if err := os.MkdirAll(srcPath, 0777); err != nil {
t.Fatalf("failed to create source directory: %v", err)
}
defer func() { _ = os.RemoveAll("/tmp/src-pv-name-no-compress") }()

req := &csi.CreateSnapshotRequest{
SourceVolumeId: "nfs-server.default.svc.cluster.local#share#subdir#src-pv-name-no-compress",
Name: "snapshot-name-no-compress",
}

resp, err := cs.CreateSnapshot(context.TODO(), req)
if err != nil {
t.Fatalf("CreateSnapshot failed: %v", err)
}

if resp == nil || resp.Snapshot == nil {
t.Fatalf("CreateSnapshot returned nil response")
}

// Check that the snapshot was created with .tar extension (not .tar.gz)
snapPath := "/tmp/snapshot-name-no-compress/snapshot-name-no-compress/src-pv-name-no-compress.tar"
if _, err := os.Stat(snapPath); os.IsNotExist(err) {
t.Errorf("expected uncompressed snapshot archive at %s, but it does not exist", snapPath)
}

// Cleanup
_ = os.RemoveAll("/tmp/snapshot-name-no-compress")
}

func TestCopyVolumeFromUncompressedSnapshot(t *testing.T) {
// Create an uncompressed snapshot archive and test restoration
srcPath := "/tmp/uncompressed-snapshot-test/uncompressed-snapshot-test"
if err := os.MkdirAll(srcPath, 0777); err != nil {
t.Fatalf("failed to create snapshot directory: %v", err)
}
defer func() { _ = os.RemoveAll("/tmp/uncompressed-snapshot-test") }()

// Create an uncompressed tar archive
archivePath := filepath.Join(srcPath, "src-vol.tar")
file, err := os.Create(archivePath)
if err != nil {
t.Fatalf("failed to create tar archive: %v", err)
}
defer file.Close()

tarWriter := tar.NewWriter(file)
body := "test content for uncompressed snapshot"
hdr := &tar.Header{
Name: "test.txt",
Mode: 0644,
Size: int64(len(body)),
}
if err := tarWriter.WriteHeader(hdr); err != nil {
t.Fatalf("failed to write tar header: %v", err)
}
if _, err := tarWriter.Write([]byte(body)); err != nil {
t.Fatalf("failed to write tar content: %v", err)
}
if err := tarWriter.Close(); err != nil {
t.Fatalf("failed to close tar writer: %v", err)
}
file.Close()

// Test copying from uncompressed snapshot
cs := initTestController(t)

req := &csi.CreateVolumeRequest{
Name: "restored-volume",
VolumeContentSource: &csi.VolumeContentSource{
Type: &csi.VolumeContentSource_Snapshot{
Snapshot: &csi.VolumeContentSource_SnapshotSource{
SnapshotId: "nfs-server.default.svc.cluster.local#share#uncompressed-snapshot-test#uncompressed-snapshot-test#src-vol",
},
},
},
}

dstVol := &nfsVolume{
id: "nfs-server.default.svc.cluster.local#share#subdir#dst-pv-name-restored",
server: "//nfs-server.default.svc.cluster.local",
baseDir: "share",
subDir: "subdir",
uuid: "dst-pv-name-restored",
}

// Create destination directory
dstPath := filepath.Join("/tmp", dstVol.uuid, dstVol.subDir)
if err := os.MkdirAll(dstPath, 0777); err != nil {
t.Fatalf("failed to create destination directory: %v", err)
}
defer func() { _ = os.RemoveAll("/tmp/dst-pv-name-restored") }()

err = cs.copyFromSnapshot(context.TODO(), req, dstVol)
if err != nil {
t.Fatalf("copyFromSnapshot failed for uncompressed archive: %v", err)
}

// Verify file was restored
restoredFile := filepath.Join(dstPath, "test.txt")
content, err := os.ReadFile(restoredFile)
if err != nil {
t.Fatalf("failed to read restored file: %v", err)
}
if string(content) != body {
t.Errorf("restored content mismatch: got %q, expected %q", string(content), body)
}
}

func TestArchiveNameWithCompression(t *testing.T) {
snap := nfsSnapshot{
src: "test-volume",
}

// Test with compression enabled
nameWithCompression := snap.archiveName(true)
if nameWithCompression != "test-volume.tar.gz" {
t.Errorf("expected 'test-volume.tar.gz', got %q", nameWithCompression)
}

// Test with compression disabled
nameWithoutCompression := snap.archiveName(false)
if nameWithoutCompression != "test-volume.tar" {
t.Errorf("expected 'test-volume.tar', got %q", nameWithoutCompression)
}
}
21 changes: 12 additions & 9 deletions pkg/nfs/nfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,20 @@ type DriverOptions struct {
VolStatsCacheExpireInMinutes int
RemoveArchivedVolumePath bool
UseTarCommandInSnapshot bool
EnableSnapshotCompression bool
}

type Driver struct {
name string
nodeID string
version string
endpoint string
mountPermissions uint64
workingMountDir string
defaultOnDeletePolicy string
removeArchivedVolumePath bool
useTarCommandInSnapshot bool
name string
nodeID string
version string
endpoint string
mountPermissions uint64
workingMountDir string
defaultOnDeletePolicy string
removeArchivedVolumePath bool
useTarCommandInSnapshot bool
enableSnapshotCompression bool

//ids *identityServer
ns *NodeServer
Expand Down Expand Up @@ -99,6 +101,7 @@ func NewDriver(options *DriverOptions) *Driver {
volStatsCacheExpireInMinutes: options.VolStatsCacheExpireInMinutes,
removeArchivedVolumePath: options.RemoveArchivedVolumePath,
useTarCommandInSnapshot: options.UseTarCommandInSnapshot,
enableSnapshotCompression: options.EnableSnapshotCompression,
defaultOnDeletePolicy: options.DefaultOnDeletePolicy,
}

Expand Down
Loading