package driver
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
csi "github.com/container-storage-interface/spec/lib/go/csi"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
testingexec "k8s.io/utils/exec/testing"
"k8s.io/utils/mount"
"gitcode.com/openFuyao/ub-ssu-csi/pkg/backend"
"gitcode.com/openFuyao/ub-ssu-csi/pkg/nvme"
)
type fakeMounter struct {
mountPoints map[string]bool
mountPointErr map[string]error
unmountErr error
mountErr error
mountedPaths []string
unmountedPaths []string
mountArgs []mountCallArg
}
type mountCallArg struct {
source string
target string
fstype string
options []string
}
func newFakeMounter() *fakeMounter {
return &fakeMounter{
mountPoints: make(map[string]bool),
mountPointErr: make(map[string]error),
}
}
func (f *fakeMounter) Mount(source, target, fstype string, options []string) error {
f.mountedPaths = append(f.mountedPaths, target)
f.mountArgs = append(f.mountArgs, mountCallArg{
source: source,
target: target,
fstype: fstype,
options: options,
})
if f.mountErr != nil {
return f.mountErr
}
f.mountPoints[target] = true
return nil
}
func (f *fakeMounter) MountSensitive(source, target, fstype string, options []string, sensitiveOptions []string) error {
return f.Mount(source, target, fstype, options)
}
func (f *fakeMounter) Unmount(target string) error {
f.unmountedPaths = append(f.unmountedPaths, target)
if f.unmountErr != nil {
return f.unmountErr
}
f.mountPoints[target] = false
return nil
}
func (f *fakeMounter) List() ([]mount.MountPoint, error) {
return nil, nil
}
func (f *fakeMounter) IsLikelyNotMountPoint(file string) (bool, error) {
if err, ok := f.mountPointErr[file]; ok {
return true, err
}
return !f.mountPoints[file], nil
}
func (f *fakeMounter) GetMountRefs(pathname string) ([]string, error) {
return nil, nil
}
type fakeStorageManager struct {
volStatus *backend.VolumeStatus
volStatusErr error
allocInfo *backend.VolumeInfo
allocInfoErr error
setAccessErr error
revokeAccessErr error
setAccessCalls []accessCall
revokeAccessCalls []accessCall
}
type accessCall struct {
volumeID string
hostNqn string
}
func (f *fakeStorageManager) AllocVolume(
name string, sizeBytes uint64, config backend.LogicalConfig,
) (*backend.VolumeInfo, error) {
if f.allocInfoErr != nil {
return nil, f.allocInfoErr
}
if f.allocInfo != nil && f.allocInfo.VolumeID == name {
return f.allocInfo, nil
}
return nil, backend.ErrVolumeInfoNotFound
}
func (f *fakeStorageManager) DeleteVolume(volumeID string) error { return nil }
func (f *fakeStorageManager) GetVolumeStatus(volumeID string) (*backend.VolumeStatus, error) {
return f.volStatus, f.volStatusErr
}
func (f *fakeStorageManager) GetVolumeInfo(volumeID string) (*backend.VolumeInfo, error) {
if f.allocInfoErr != nil {
return nil, f.allocInfoErr
}
if f.allocInfo != nil && f.allocInfo.VolumeID == volumeID {
return f.allocInfo, nil
}
return nil, backend.ErrVolumeInfoNotFound
}
func (f *fakeStorageManager) SetAccessPermission(volumeID string, hostNqn string) error {
f.setAccessCalls = append(f.setAccessCalls, accessCall{volumeID: volumeID, hostNqn: hostNqn})
return f.setAccessErr
}
func (f *fakeStorageManager) RevokeAccessPermission(volumeID string, hostNqn string) error {
f.revokeAccessCalls = append(f.revokeAccessCalls, accessCall{volumeID: volumeID, hostNqn: hostNqn})
return f.revokeAccessErr
}
func (f *fakeStorageManager) GetConnectionInfo(volumeID string) ([]*backend.ConnectionInfo, error) {
return nil, nil
}
func newFakeStorageManager() *fakeStorageManager {
return &fakeStorageManager{}
}
type connectorCall struct {
name string
hostNqn string
deviceID string
devicePath string
level uint8
chunkSize uint32
}
type fakeConnector struct {
connectCalls []connectorCall
disconnectCalls []connectorCall
linearCalls []connectorCall
linearDisconnects []connectorCall
stripingCalls []connectorCall
stripingDisconnects []connectorCall
connectErr error
disconnectErr error
linearErr error
linearDisconnectErr error
stripingErr error
stripingDisconnectErr error
}
func (f *fakeConnector) Connect(name, hostNqn, deviceID string) error {
f.connectCalls = append(f.connectCalls, connectorCall{name: name, hostNqn: hostNqn, deviceID: deviceID})
return f.connectErr
}
func (f *fakeConnector) Disconnect(name, hostNqn string) error {
f.disconnectCalls = append(f.disconnectCalls, connectorCall{name: name, hostNqn: hostNqn})
return f.disconnectErr
}
func (f *fakeConnector) ConnectWithStripingAddressing(name string, hostNqn string, devicePath string, level uint8, chunkSize uint32) error {
f.stripingCalls = append(f.stripingCalls, connectorCall{name: name, hostNqn: hostNqn, devicePath: devicePath, level: level, chunkSize: chunkSize})
return f.stripingErr
}
func (f *fakeConnector) DisconnectWithStripingAddressing(name, hostNqn, devicePath string) error {
f.stripingDisconnects = append(f.stripingDisconnects, connectorCall{name: name, hostNqn: hostNqn, devicePath: devicePath})
return f.stripingDisconnectErr
}
func (f *fakeConnector) ConnectWithLinearAddressing(name, hostNqn, devicePath string) error {
f.linearCalls = append(f.linearCalls, connectorCall{name: name, hostNqn: hostNqn, devicePath: devicePath})
return f.linearErr
}
func (f *fakeConnector) DisconnectWithLinearAddressing(name, hostNqn, devicePath string) error {
f.linearDisconnects = append(f.linearDisconnects, connectorCall{name: name, hostNqn: hostNqn, devicePath: devicePath})
return f.linearDisconnectErr
}
func newTestNodeServer(storage backend.StorageManager, connector nvme.Connector, mounter mount.Interface, formatter *mount.SafeFormatAndMount) *NodeServer {
return &NodeServer{
nodeID: "test-node",
hostNqn: nvme.GenerateHostNQN("test-node"),
storage: storage,
connector: connector,
mounter: mounter,
formatter: formatter,
}
}
func newDefaultTestNS() *NodeServer {
return newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, newFakeMounter(), nil)
}
func blockCap() *csi.VolumeCapability {
return &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Block{
Block: &csi.VolumeCapability_BlockVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
}
}
func mountCap(fsType string, flags []string) *csi.VolumeCapability {
return &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{
FsType: fsType,
MountFlags: flags,
},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
}
}
func makeStageReq(volumeID string, stagingPath string, cap *csi.VolumeCapability, volumeCtx map[string]string) *csi.NodeStageVolumeRequest {
return &csi.NodeStageVolumeRequest{
VolumeId: volumeID,
StagingTargetPath: stagingPath,
VolumeCapability: cap,
VolumeContext: volumeCtx,
}
}
func makeBlockPublishReq(targetPath, deviceID string, readonly bool) *csi.NodePublishVolumeRequest {
return &csi.NodePublishVolumeRequest{
VolumeId: "vol-1",
TargetPath: targetPath,
VolumeCapability: blockCap(),
Readonly: readonly,
VolumeContext: map[string]string{
"deviceID": deviceID,
},
}
}
func makeMountPublishReq(targetPath string, allocStrategy string, fstype string, readonly bool) *csi.NodePublishVolumeRequest {
return &csi.NodePublishVolumeRequest{
VolumeId: "vol-1",
TargetPath: targetPath,
StagingTargetPath: allocStrategy,
VolumeCapability: mountCap(fstype, nil),
Readonly: readonly,
VolumeContext: map[string]string{"allocStrategy": allocStrategy},
}
}
func makeUnpublishReq(volumeId, targetPath string) *csi.NodeUnpublishVolumeRequest {
return &csi.NodeUnpublishVolumeRequest{
VolumeId: volumeId,
TargetPath: targetPath,
}
}
func getErrorCode(err error) codes.Code {
if st, ok := status.FromError(err); ok {
return st.Code()
}
return codes.Unknown
}
func stageNormalBlockVolumeForTest(t *testing.T, server *NodeServer, stagingPath string) {
t.Helper()
_, err := server.NodeStageVolume(context.Background(), makeStageReq(
"vol-1",
stagingPath,
blockCap(),
map[string]string{"deviceID": "dev-1", "allocStrategy": "2"},
))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func unstageVolumeForTest(t *testing.T, server *NodeServer, stagingPath string) {
t.Helper()
_, err := server.NodeUnstageVolume(context.Background(), &csi.NodeUnstageVolumeRequest{
VolumeId: "vol-1",
StagingTargetPath: stagingPath,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func legacyStageMetadataFile(stagingPath string) string {
return filepath.Join(filepath.Dir(stagingPath), "."+filepath.Base(stagingPath)+".ssu-stage.json")
}
func writeLegacyStageMetadata(t *testing.T, stagingPath string, devicePath string) {
t.Helper()
metadata := fmt.Sprintf(`{"volumeID":"vol-1","devicePath":"%s"}`, devicePath)
if err := os.WriteFile(legacyStageMetadataFile(stagingPath), []byte(metadata), 0600); err != nil {
t.Fatalf("write legacy metadata: %v", err)
}
}
func TestNodeStageVolumeNormalBlock(t *testing.T) {
stagingPath := filepath.Join(t.TempDir(), "stage")
storage := newFakeStorageManager()
connector := &fakeConnector{}
ns := newTestNodeServer(storage, connector, newFakeMounter(), nil)
stageNormalBlockVolumeForTest(t, ns, stagingPath)
if len(storage.setAccessCalls) != 1 {
t.Fatalf("expected 1 set access call, got %d", len(storage.setAccessCalls))
}
if len(connector.connectCalls) != 1 {
t.Fatalf("expected normal connect call, got %#v", connector.connectCalls)
}
if connector.connectCalls[0].deviceID != "dev-1" {
t.Fatalf("expected deviceID dev-1, got %s", connector.connectCalls[0].deviceID)
}
if _, err := os.Stat(legacyStageMetadataFile(stagingPath)); !os.IsNotExist(err) {
t.Fatalf("expected no stage metadata file, got %v", err)
}
}
func TestNodeStageVolumeFilesystemFormatsAndMounts(t *testing.T) {
stagingPath := filepath.Join(t.TempDir(), "stage")
fm := newFakeMounter()
formatter := &mount.SafeFormatAndMount{
Interface: fm,
Exec: &testingexec.FakeExec{DisableScripts: true},
}
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, formatter)
_, err := ns.NodeStageVolume(context.Background(), makeStageReq(
"vol-1",
stagingPath,
mountCap("", []string{"noatime"}),
map[string]string{"deviceID": "dev-1", "allocStrategy": "2"},
))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fm.mountArgs) != 1 {
t.Fatalf("expected 1 format mount call, got %d", len(fm.mountArgs))
}
call := fm.mountArgs[0]
if call.source != "/dev/disk/by-id/nvme-dev-1" || call.target != stagingPath || call.fstype != "ext4" {
t.Fatalf("unexpected format mount call: %#v", call)
}
foundNoatime := false
for _, opt := range call.options {
if opt == "noatime" {
foundNoatime = true
}
}
if !foundNoatime {
t.Fatalf("unexpected mount options: %#v", call.options)
}
}
func TestNodeStageVolumeLinearAggregated(t *testing.T) {
stagingPath := filepath.Join(t.TempDir(), "stage")
connector := &fakeConnector{}
ns := newTestNodeServer(newFakeStorageManager(), connector, newFakeMounter(), nil)
_, err := ns.NodeStageVolume(context.Background(), makeStageReq(
"vol-1",
stagingPath,
blockCap(),
map[string]string{"allocStrategy": "1"},
))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(connector.linearCalls) != 1 {
t.Fatalf("expected 1 linear connect call, got %#v", connector.linearCalls)
}
if connector.linearCalls[0].devicePath != "/dev/vol-1" {
t.Fatalf("expected devicePath /dev/vol-1, got %s", connector.linearCalls[0].devicePath)
}
}
func TestNodeStageVolumeStripingAggregated(t *testing.T) {
stagingPath := filepath.Join(t.TempDir(), "stage")
connector := &fakeConnector{}
ns := newTestNodeServer(newFakeStorageManager(), connector, newFakeMounter(), nil)
_, err := ns.NodeStageVolume(context.Background(), makeStageReq(
"vol-1",
stagingPath,
blockCap(),
map[string]string{
"allocStrategy": "0",
"raidLevel": "5",
"chunkSize": "64",
"nsnum": "3",
},
))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(connector.stripingCalls) != 1 {
t.Fatalf("expected 1 striping connect call, got %#v", connector.stripingCalls)
}
if connector.stripingCalls[0].devicePath != "/dev/vol-1" ||
connector.stripingCalls[0].level != 5 ||
connector.stripingCalls[0].chunkSize != 64 {
t.Fatalf("unexpected striping call: %#v", connector.stripingCalls[0])
}
}
func TestNodeStageVolumeConnectFailureRevokesAccess(t *testing.T) {
storage := newFakeStorageManager()
connector := &fakeConnector{connectErr: fmt.Errorf("attach failed")}
ns := newTestNodeServer(storage, connector, newFakeMounter(), nil)
_, err := ns.NodeStageVolume(context.Background(), makeStageReq(
"vol-1",
filepath.Join(t.TempDir(), "stage"),
blockCap(),
map[string]string{"deviceID": "dev-1", "allocStrategy": "2"},
))
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
}
func TestNodeUnstageVolumeDisconnectsNormalFromAllocInfo(t *testing.T) {
storage := newFakeStorageManager()
storage.allocInfo = &backend.VolumeInfo{
VolumeID: "vol-1",
Strategy: 2,
Namespaces: []backend.Namespace{
{DeviceUUID: "dev-1", DevicePath: "/dev/nvme0n1"},
},
}
connector := &fakeConnector{}
ns := newTestNodeServer(storage, connector, newFakeMounter(), nil)
unstageVolumeForTest(t, ns, filepath.Join(t.TempDir(), "stage"))
if len(connector.disconnectCalls) != 1 {
t.Fatalf("expected disconnect, got %#v", connector.disconnectCalls)
}
if len(storage.revokeAccessCalls) != 1 {
t.Fatalf("expected revoke access, got %d", len(storage.revokeAccessCalls))
}
}
func TestNodeUnstageVolumeAllocInfoMissingReturnsError(t *testing.T) {
ns := newDefaultTestNS()
_, err := ns.NodeUnstageVolume(context.Background(), &csi.NodeUnstageVolumeRequest{
VolumeId: "vol-1",
StagingTargetPath: filepath.Join(t.TempDir(), "stage"),
})
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
}
func TestNodeUnstageVolumeDisconnectsLinearFromAllocInfo(t *testing.T) {
storage := newFakeStorageManager()
storage.allocInfo = &backend.VolumeInfo{
VolumeID: "vol-1",
Strategy: 1,
Namespaces: []backend.Namespace{{DevicePath: "/dev/pvc-1"}, {DevicePath: "/dev/pvc-2"}},
}
connector := &fakeConnector{}
fm := newFakeMounter()
stagingPath := filepath.Join(t.TempDir(), "stage")
fm.mountPoints[stagingPath] = true
ns := newTestNodeServer(storage, connector, fm, nil)
unstageVolumeForTest(t, ns, stagingPath)
if len(fm.unmountedPaths) != 1 || fm.unmountedPaths[0] != stagingPath {
t.Fatalf("expected staging path unmounted, got %#v", fm.unmountedPaths)
}
if len(connector.linearDisconnects) != 1 {
t.Fatalf("expected linear disconnect, got %#v", connector.linearDisconnects)
}
if connector.linearDisconnects[0].devicePath != "/dev/vol-1" {
t.Fatalf("expected devicePath /dev/vol-1, got %s", connector.linearDisconnects[0].devicePath)
}
if len(storage.revokeAccessCalls) != 1 {
t.Fatalf("expected revoke access, got %d", len(storage.revokeAccessCalls))
}
}
func TestNodeUnstageVolumeDisconnectsStripingFromAllocInfo(t *testing.T) {
storage := newFakeStorageManager()
storage.allocInfo = &backend.VolumeInfo{
VolumeID: "vol-1",
Strategy: 0,
Namespaces: []backend.Namespace{{DevicePath: "/dev/pvc-1"}, {DevicePath: "/dev/pvc-2"}},
}
connector := &fakeConnector{}
ns := newTestNodeServer(storage, connector, newFakeMounter(), nil)
unstageVolumeForTest(t, ns, filepath.Join(t.TempDir(), "stage"))
if len(connector.stripingDisconnects) != 1 {
t.Fatalf("expected striping disconnect, got %#v", connector.stripingDisconnects)
}
if connector.stripingDisconnects[0].devicePath != "/dev/vol-1" {
t.Fatalf("expected devicePath /dev/vol-1, got %s", connector.stripingDisconnects[0].devicePath)
}
if len(storage.revokeAccessCalls) != 1 {
t.Fatalf("expected revoke access, got %d", len(storage.revokeAccessCalls))
}
}
func TestNodePublishVolume_Validation(t *testing.T) {
ns := newDefaultTestNS()
tests := []struct {
name string
req *csi.NodePublishVolumeRequest
wantMsg string
}{
{
name: "volume_id_missing",
req: &csi.NodePublishVolumeRequest{},
wantMsg: "volume ID missing",
},
{
name: "target_path_missing",
req: &csi.NodePublishVolumeRequest{VolumeId: "vol-1"},
wantMsg: "target path missing",
},
{
name: "volume_capability_missing",
req: &csi.NodePublishVolumeRequest{VolumeId: "vol-1", TargetPath: "/tmp/target"},
wantMsg: "volume capability missing",
},
{
name: "neither_block_nor_mount",
req: &csi.NodePublishVolumeRequest{
VolumeId: "vol-1",
TargetPath: "/tmp/target",
VolumeCapability: &csi.VolumeCapability{
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
},
},
wantMsg: "block or mount access type",
},
{
name: "block_device_id_and_aggregate_path_missing",
req: &csi.NodePublishVolumeRequest{
VolumeId: "vol-1",
TargetPath: "/tmp/target",
VolumeCapability: blockCap(),
VolumeContext: map[string]string{},
},
wantMsg: "deviceID or allocStrategy",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := ns.NodePublishVolume(context.Background(), tc.req)
if getErrorCode(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), tc.wantMsg) {
t.Fatalf("expected error containing %q, got %v", tc.wantMsg, err)
}
})
}
}
func TestNodePublishVolume_BlockModeRoute(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target-block")
req := makeBlockPublishReq(targetPath, "sda", false)
resp, err := ns.NodePublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.mountedPaths) != 1 {
t.Fatalf("expected 1 mount call, got %d", len(fm.mountedPaths))
}
if fm.mountedPaths[0] != targetPath {
t.Fatalf("expected mount target %s, got %s", targetPath, fm.mountedPaths[0])
}
}
func TestNodePublishVolume_MountModeRoute(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target-mount")
req := makeMountPublishReq(targetPath, "/dev/sda", "ext4", false)
resp, err := ns.NodePublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.mountedPaths) != 1 {
t.Fatalf("expected 1 mount call, got %d", len(fm.mountedPaths))
}
}
func TestNodePublishVolume_Fstype(t *testing.T) {
tests := []struct {
name string
fstype string
wantFstype string
}{
{name: "default_ext4", fstype: "", wantFstype: "ext4"},
{name: "xfs", fstype: "xfs", wantFstype: "xfs"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeMountPublishReq(targetPath, "/dev/sda", tc.fstype, false)
_, err := ns.NodePublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fm.mountArgs) == 0 {
t.Fatal("expected at least 1 mount call")
}
for _, arg := range fm.mountArgs {
if arg.fstype != tc.wantFstype {
t.Errorf("expected fstype %s, got %s", tc.wantFstype, arg.fstype)
}
}
})
}
}
func TestNodePublishVolume_BlockDeviceResolution(t *testing.T) {
tests := []struct {
name string
volumeCtx map[string]string
wantSource string
}{
{
name: "deviceID",
volumeCtx: map[string]string{"deviceID": "nvme-xxx"},
wantSource: "/dev/disk/by-id/nvme-nvme-xxx",
},
{
name: "allocStrategy",
volumeCtx: map[string]string{"allocStrategy": "1"},
wantSource: "/dev/vol-1",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := &csi.NodePublishVolumeRequest{
VolumeId: "vol-1",
TargetPath: targetPath,
VolumeCapability: blockCap(),
VolumeContext: tc.volumeCtx,
}
resp, err := ns.NodePublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.mountArgs) != 1 {
t.Fatalf("expected 1 mount call, got %d", len(fm.mountArgs))
}
if fm.mountArgs[0].source != tc.wantSource {
t.Errorf("expected mount source %s, got %s", tc.wantSource, fm.mountArgs[0].source)
}
})
}
}
func TestNodePublishVolume_MountModeNoDeviceID(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := &csi.NodePublishVolumeRequest{
VolumeId: "vol-1",
TargetPath: targetPath,
StagingTargetPath: "/dev/sda",
VolumeCapability: mountCap("ext4", nil),
VolumeContext: map[string]string{},
}
resp, err := ns.NodePublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.mountedPaths) != 1 {
t.Fatalf("expected 1 mount call, got %d", len(fm.mountedPaths))
}
}
func TestNodePublishVolumeBlockIgnoresLegacyStageMetadata(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
tmpDir := t.TempDir()
stagingPath := filepath.Join(tmpDir, "stage")
targetPath := filepath.Join(tmpDir, "target")
writeLegacyStageMetadata(t, stagingPath, "/dev/staged-device")
req := makeBlockPublishReq(targetPath, "context-device", false)
req.StagingTargetPath = stagingPath
_, err := ns.NodePublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fm.mountArgs) != 1 {
t.Fatalf("expected 1 mount call, got %d", len(fm.mountArgs))
}
if fm.mountArgs[0].source != "/dev/disk/by-id/nvme-context-device" {
t.Fatalf("expected context device, got %s", fm.mountArgs[0].source)
}
}
func TestNodePublishVolumeBlock_OpenFileCreate(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
if err := os.MkdirAll(targetPath, 0755); err != nil {
t.Fatalf("setup: %v", err)
}
req := makeBlockPublishReq(targetPath, "/dev/sda", false)
_, err := ns.nodePublishVolumeBlock(req, targetPath, "/dev/sda")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
fi, err := os.Stat(targetPath)
if err != nil {
t.Fatalf("expected targetPath to exist after publish: %v", err)
}
if fi.IsDir() {
t.Fatalf("targetPath should be a file, not a directory")
}
}
func TestNodePublishVolumeBlock_BindMountSuccess(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeBlockPublishReq(targetPath, "/dev/sda", false)
resp, err := ns.nodePublishVolumeBlock(req, targetPath, "/dev/sda")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.mountArgs) != 1 {
t.Fatalf("expected 1 mount call, got %d", len(fm.mountArgs))
}
arg := fm.mountArgs[0]
if arg.fstype != "" {
t.Errorf("expected empty fstype for bind mount, got %s", arg.fstype)
}
foundBind := false
for _, opt := range arg.options {
if opt == "bind" {
foundBind = true
}
}
if !foundBind {
t.Error("expected 'bind' in mount options")
}
}
func TestNodePublishVolumeBlock_BindMountFail(t *testing.T) {
fm := newFakeMounter()
fm.mountErr = fmt.Errorf("mount failed")
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeBlockPublishReq(targetPath, "/dev/sda", false)
_, err := ns.nodePublishVolumeBlock(req, targetPath, "/dev/sda")
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), "mount block device") {
t.Fatalf("expected error containing 'mount block device', got %v", err.Error())
}
}
func TestNodePublishVolumeMount_MkdirAllSuccess(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeMountPublishReq(targetPath, "/dev/sda", "ext4", false)
_, err := ns.nodePublishVolumeMount(req, targetPath, "/dev/sda", "ext4", false, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
fi, err := os.Stat(targetPath)
if err != nil {
t.Fatalf("expected targetPath to exist: %v", err)
}
if !fi.IsDir() {
t.Fatalf("targetPath should be a directory, got file")
}
}
func TestNodePublishVolumeMount_AlreadyMounted(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
fm.mountPoints[targetPath] = true
req := makeMountPublishReq(targetPath, "/dev/sda", "ext4", false)
resp, err := ns.nodePublishVolumeMount(req, targetPath, "/dev/sda", "ext4", false, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.mountedPaths) != 0 {
t.Errorf("expected 0 mount calls (already mounted), got %d", len(fm.mountedPaths))
}
}
func TestNodePublishVolumeMount_NotMounted(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeMountPublishReq(targetPath, "/dev/sda", "ext4", false)
resp, err := ns.nodePublishVolumeMount(req, targetPath, "/dev/sda", "ext4", false, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.mountedPaths) != 1 {
t.Errorf("expected 1 mount call, got %d", len(fm.mountedPaths))
}
}
func TestNodePublishVolumeMount_CheckMountPointError(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
fm.mountPointErr[targetPath] = fmt.Errorf("permission denied")
_, err := ns.nodePublishVolumeMount(
&csi.NodePublishVolumeRequest{},
targetPath, "/dev/sda", "ext4", false, nil,
)
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), "check mount point") {
t.Fatalf("expected error containing 'check mount point', got %v", err.Error())
}
}
func TestNodePublishVolumeMount_CheckMountPointErrNotExist(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
fm.mountPointErr[targetPath] = os.ErrNotExist
req := makeMountPublishReq(targetPath, "/dev/sda", "ext4", false)
resp, err := ns.nodePublishVolumeMount(req, targetPath, "/dev/sda", "ext4", false, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
}
func TestNodePublishVolumeMount_Readonly(t *testing.T) {
tests := []struct {
name string
readonly bool
wantOpt string
badOpt string
}{
{name: "rw", readonly: false, wantOpt: "rw", badOpt: "ro"},
{name: "ro", readonly: true, wantOpt: "ro", badOpt: "rw"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeMountPublishReq(targetPath, "/dev/sda", "ext4", tc.readonly)
_, err := ns.nodePublishVolumeMount(req, targetPath, "/dev/sda", "ext4", tc.readonly, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fm.mountArgs) == 0 {
t.Fatal("expected at least 1 mount call")
}
foundWant, foundBad := false, false
for _, opt := range fm.mountArgs[0].options {
if opt == tc.wantOpt {
foundWant = true
}
if opt == tc.badOpt {
foundBad = true
}
}
if !foundWant {
t.Errorf("expected %q option", tc.wantOpt)
}
if foundBad {
t.Errorf("unexpected %q option", tc.badOpt)
}
})
}
}
func TestNodePublishVolumeMount_FormatAndMountFail(t *testing.T) {
fm := newFakeMounter()
fm.mountErr = fmt.Errorf("mount failed")
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeMountPublishReq(targetPath, "/dev/sda", "ext4", false)
_, err := ns.nodePublishVolumeMount(req, targetPath, "/dev/sda", "ext4", false, nil)
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), "mount device") {
t.Fatalf("expected error containing 'mount device', got %v", err.Error())
}
}
func TestNodeUnpublishVolume_Validation(t *testing.T) {
ns := newDefaultTestNS()
tests := []struct {
name string
volumeId string
targetPath string
wantMsg string
}{
{name: "volume_id_missing", volumeId: "", targetPath: "/tmp/target", wantMsg: "volume ID missing"},
{name: "target_path_missing", volumeId: "vol-1", targetPath: "", wantMsg: "target path missing"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := makeUnpublishReq(tc.volumeId, tc.targetPath)
_, err := ns.NodeUnpublishVolume(context.Background(), req)
if getErrorCode(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), tc.wantMsg) {
t.Fatalf("expected error containing %q, got %v", tc.wantMsg, err)
}
})
}
}
func TestNodeUnpublishVolume_NotMountPoint(t *testing.T) {
fm := newFakeMounter()
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
targetPath := filepath.Join(t.TempDir(), "target")
req := makeUnpublishReq("vol-1", targetPath)
resp, err := ns.NodeUnpublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.unmountedPaths) != 0 {
t.Fatalf("expected 0 unmount calls, got %d", len(fm.unmountedPaths))
}
}
func TestNodeUnpublishVolume_IsMountPoint(t *testing.T) {
fm := newFakeMounter()
targetPath := filepath.Join(t.TempDir(), "target")
fm.mountPoints[targetPath] = true
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
req := makeUnpublishReq("vol-1", targetPath)
resp, err := ns.NodeUnpublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(fm.unmountedPaths) != 1 {
t.Fatalf("expected 1 unmount call, got %d", len(fm.unmountedPaths))
}
}
func TestNodeUnpublishVolume_CheckMountPointError(t *testing.T) {
fm := newFakeMounter()
targetPath := filepath.Join(t.TempDir(), "target")
fm.mountPointErr[targetPath] = fmt.Errorf("permission denied")
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
req := makeUnpublishReq("vol-1", targetPath)
_, err := ns.NodeUnpublishVolume(context.Background(), req)
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), "check mount point") {
t.Fatalf("expected error containing 'check mount point', got %v", err)
}
}
func TestNodeUnpublishVolume_CheckMountPointErrNotExist(t *testing.T) {
fm := newFakeMounter()
targetPath := filepath.Join(t.TempDir(), "target")
fm.mountPointErr[targetPath] = os.ErrNotExist
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
req := makeUnpublishReq("vol-1", targetPath)
resp, err := ns.NodeUnpublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
}
func TestNodeUnpublishVolume_UnmountFail(t *testing.T) {
fm := newFakeMounter()
targetPath := filepath.Join(t.TempDir(), "target")
fm.mountPoints[targetPath] = true
fm.unmountErr = fmt.Errorf("unmount failed")
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
req := makeUnpublishReq("vol-1", targetPath)
_, err := ns.NodeUnpublishVolume(context.Background(), req)
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), "unmount") {
t.Fatalf("expected error containing 'unmount', got %v", err)
}
}
func TestNodeUnpublishVolume_Idempotent(t *testing.T) {
fm := newFakeMounter()
targetPath := filepath.Join(t.TempDir(), "target")
if err := os.MkdirAll(targetPath, 0755); err != nil {
t.Fatalf("setup: %v", err)
}
fm.mountPoints[targetPath] = true
ns := newTestNodeServer(newFakeStorageManager(), &fakeConnector{}, fm, nil)
req := makeUnpublishReq("vol-1", targetPath)
resp, err := ns.NodeUnpublishVolume(context.Background(), req)
if err != nil {
t.Fatalf("first call error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
resp2, err2 := ns.NodeUnpublishVolume(context.Background(), req)
if err2 != nil {
t.Fatalf("second call error: %v", err2)
}
if resp2 == nil {
t.Fatal("expected non-nil response on second call")
}
}
func TestNodeGetVolumeStats_Validation(t *testing.T) {
ns := newDefaultTestNS()
tests := []struct {
name string
volumeId string
volumePath string
wantMsg string
}{
{name: "volume_id_missing", volumeId: "", volumePath: "", wantMsg: "volume ID missing"},
{name: "volume_path_missing", volumeId: "vol-1", volumePath: "", wantMsg: "volume path missing"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := &csi.NodeGetVolumeStatsRequest{VolumeId: tc.volumeId, VolumePath: tc.volumePath}
_, err := ns.NodeGetVolumeStats(context.Background(), req)
if getErrorCode(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), tc.wantMsg) {
t.Fatalf("expected error containing %q, got %v", tc.wantMsg, err)
}
})
}
}
func TestNodeGetVolumeStats_Success(t *testing.T) {
sm := &fakeStorageManager{
volStatus: &backend.VolumeStatus{
TotalBytes: 1000,
UsedBytes: 500,
AvailableBytes: 500,
},
}
fm := newFakeMounter()
fm.mountPoints["/data"] = true
ns := newTestNodeServer(sm, &fakeConnector{}, fm, nil)
req := &csi.NodeGetVolumeStatsRequest{VolumeId: "vol-1", VolumePath: "/data"}
resp, err := ns.NodeGetVolumeStats(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(resp.Usage) != 1 {
t.Fatalf("expected 1 usage entry, got %d", len(resp.Usage))
}
u := resp.Usage[0]
if u.Total != 1000 || u.Used != 500 || u.Available != 500 {
t.Errorf("expected Total=1000 Used=500 Available=500, got Total=%d Used=%d Available=%d", u.Total, u.Used, u.Available)
}
}
func TestNodeGetVolumeStats_GetVolumeStatusFail(t *testing.T) {
sm := &fakeStorageManager{volStatusErr: fmt.Errorf("internal error")}
fm := newFakeMounter()
fm.mountPoints["/data"] = true
ns := newTestNodeServer(sm, &fakeConnector{}, fm, nil)
req := &csi.NodeGetVolumeStatsRequest{VolumeId: "vol-1", VolumePath: "/data"}
_, err := ns.NodeGetVolumeStats(context.Background(), req)
if getErrorCode(err) != codes.Internal {
t.Fatalf("expected Internal, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), "get volume status") {
t.Fatalf("expected error containing 'get volume status', got %v", err.Error())
}
}
func TestNodeGetVolumeStats_NotMountPoint(t *testing.T) {
ns := newDefaultTestNS()
req := &csi.NodeGetVolumeStatsRequest{VolumeId: "vol-1", VolumePath: "/not-mounted"}
_, err := ns.NodeGetVolumeStats(context.Background(), req)
if getErrorCode(err) != codes.NotFound {
t.Fatalf("expected NotFound, got %v", getErrorCode(err))
}
if !strings.Contains(err.Error(), "not a valid mount point") {
t.Fatalf("expected error containing 'not a valid mount point', got %v", err.Error())
}
}
func TestNodeGetCapabilities(t *testing.T) {
ns := newDefaultTestNS()
resp, err := ns.NodeGetCapabilities(context.Background(), &csi.NodeGetCapabilitiesRequest{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(resp.Capabilities) != 2 {
t.Fatalf("expected 2 capabilities, got %d", len(resp.Capabilities))
}
types := make(map[csi.NodeServiceCapability_RPC_Type]bool)
for _, cap := range resp.Capabilities {
if rpc := cap.GetRpc(); rpc != nil {
types[rpc.Type] = true
}
}
if !types[csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME] {
t.Fatalf("missing STAGE_UNSTAGE_VOLUME capability")
}
if !types[csi.NodeServiceCapability_RPC_GET_VOLUME_STATS] {
t.Fatalf("missing GET_VOLUME_STATS capability")
}
}
func TestNodeGetInfo(t *testing.T) {
ns := newDefaultTestNS()
resp, err := ns.NodeGetInfo(context.Background(), &csi.NodeGetInfoRequest{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.NodeId != "test-node" {
t.Fatalf("expected NodeId=test-node, got %s", resp.NodeId)
}
}