Skip to content

Commit d9260b0

Browse files
authored
Merge pull request #6 from serverscom/fix-multi-volumes
Fix long mount and multi volumes mount
2 parents e98a578 + 42b0a9b commit d9260b0

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

deploy/rbac.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ rules:
114114
- apiGroups: [""]
115115
resources: ["pods"]
116116
verbs: ["get", "list", "watch"]
117+
- apiGroups: ["storage.k8s.io"]
118+
resources: ["volumeattributesclasses"]
119+
verbs: ["get", "list", "watch"]
117120

118121
---
119122

pkg/csi/node.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"sync"
78
"time"
89

910
"github.com/container-storage-interface/spec/lib/go/csi"
@@ -24,6 +25,7 @@ type NodeService struct {
2425
nodeID string
2526
iscsiManager iscsi.ISCSIManager
2627
mountManager mount.MountManager
28+
stagingMu sync.Map // map[volumeID → *sync.Mutex], serializes concurrent stage calls per volume
2729
}
2830

2931
// NewNodeService creates a new node service
@@ -39,6 +41,15 @@ func NewNodeService(nodeID string) *NodeService {
3941
func (s *NodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
4042
klog.V(2).InfoS("NodeStageVolume called", "volume_id", req.GetVolumeId())
4143

44+
// Serialize concurrent staging calls for the same volume. Kubelet may retry
45+
// NodeStageVolume while a previous call is still formatting the disk (mkfs can
46+
// take minutes). Without this lock the second call races FormatAndMountDevice
47+
// and hits "already mounted" once the first call finishes.
48+
mu, _ := s.stagingMu.LoadOrStore(req.GetVolumeId(), &sync.Mutex{})
49+
lock := mu.(*sync.Mutex)
50+
lock.Lock()
51+
defer lock.Unlock()
52+
4253
// Extract iSCSI connection information from publish context
4354
publishContext := req.GetPublishContext()
4455
klog.V(2).InfoS("PublishContext", "context", util.MaskSensitiveMap(publishContext))
@@ -142,6 +153,16 @@ func (s *NodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol
142153
}
143154
}
144155

156+
mounted, err := s.mountManager.IsMounted(stagingPath)
157+
if err != nil {
158+
return nil, status.Errorf(codes.Internal, "failed to check if staging path is mounted: %v", err)
159+
}
160+
if mounted {
161+
klog.V(1).InfoS("Staging path already mounted, volume already staged",
162+
"volume_id", req.GetVolumeId(), "staging_path", stagingPath)
163+
return &csi.NodeStageVolumeResponse{}, nil
164+
}
165+
145166
// Use FormatAndMount which will check if already formatted and only format if needed
146167
klog.V(2).InfoS("Formatting and mounting device if needed",
147168
"device_path", devicePath,

pkg/csi/node_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func TestNodeStageVolume_Success(t *testing.T) {
6868
miscsi.EXPECT().Login(ctx, target).Return(nil)
6969
miscsi.EXPECT().GetDevice(ctx, target).Return("/dev/sdb", nil)
7070
miscsi.EXPECT().WaitForDevice(ctx, "/dev/sdb", 30*time.Second).Return(nil)
71+
mmount.EXPECT().IsMounted("/tmp/staging").Return(false, nil)
7172
mmount.EXPECT().FormatAndMountDevice(ctx, "/dev/sdb", "/tmp/staging", "ext4", nil).Return(nil)
7273

7374
resp, err := svc.NodeStageVolume(ctx, req)
@@ -112,6 +113,7 @@ func TestNodeStageVolume_AlreadyLoggedIn(t *testing.T) {
112113
miscsi.EXPECT().IsLoggedIn(ctx, target).Return(true, nil)
113114
miscsi.EXPECT().GetDevice(ctx, target).Return("/dev/sdb", nil)
114115
miscsi.EXPECT().WaitForDevice(ctx, "/dev/sdb", 30*time.Second).Return(nil)
116+
mmount.EXPECT().IsMounted("/tmp/staging").Return(false, nil)
115117
mmount.EXPECT().FormatAndMountDevice(ctx, "/dev/sdb", "/tmp/staging", "xfs", nil).Return(nil)
116118

117119
resp, err := svc.NodeStageVolume(ctx, req)
@@ -120,6 +122,51 @@ func TestNodeStageVolume_AlreadyLoggedIn(t *testing.T) {
120122
g.Expect(resp).NotTo(BeNil())
121123
}
122124

125+
func TestNodeStageVolume_AlreadyStaged(t *testing.T) {
126+
g := NewGomegaWithT(t)
127+
ctrl := gomock.NewController(t)
128+
defer ctrl.Finish()
129+
130+
svc, miscsi, mmount := newTestNode(ctrl)
131+
ctx := context.Background()
132+
133+
req := &csi.NodeStageVolumeRequest{
134+
VolumeId: "vol-1",
135+
StagingTargetPath: "/tmp/staging",
136+
PublishContext: map[string]string{
137+
"target-iqn": "iqn.2024-01.com.example:target",
138+
"ip-address": "192.168.1.100",
139+
"username": "testuser",
140+
"password": "testpass",
141+
},
142+
VolumeCapability: &csi.VolumeCapability{
143+
AccessType: &csi.VolumeCapability_Mount{
144+
Mount: &csi.VolumeCapability_MountVolume{
145+
FsType: "ext4",
146+
},
147+
},
148+
},
149+
}
150+
151+
target := &iscsi.TargetInfo{
152+
Portal: "192.168.1.100:3260",
153+
IQN: "iqn.2024-01.com.example:target",
154+
Username: "testuser",
155+
Password: "testpass",
156+
}
157+
158+
miscsi.EXPECT().IsLoggedIn(ctx, target).Return(true, nil)
159+
miscsi.EXPECT().GetDevice(ctx, target).Return("/dev/sdb", nil)
160+
miscsi.EXPECT().WaitForDevice(ctx, "/dev/sdb", 30*time.Second).Return(nil)
161+
mmount.EXPECT().IsMounted("/tmp/staging").Return(true, nil)
162+
// FormatAndMountDevice must NOT be called for an already-staged volume
163+
164+
resp, err := svc.NodeStageVolume(ctx, req)
165+
166+
g.Expect(err).To(BeNil())
167+
g.Expect(resp).NotTo(BeNil())
168+
}
169+
123170
func TestNodeStageVolume_MissingTargetIQN(t *testing.T) {
124171
g := NewGomegaWithT(t)
125172
ctrl := gomock.NewController(t)
@@ -355,6 +402,31 @@ func TestNodePublishVolume_AlreadyMounted(t *testing.T) {
355402
g.Expect(resp).NotTo(BeNil())
356403
}
357404

405+
func TestNodePublishVolume_BindMountFails(t *testing.T) {
406+
g := NewGomegaWithT(t)
407+
ctrl := gomock.NewController(t)
408+
defer ctrl.Finish()
409+
410+
svc, _, mmount := newTestNode(ctrl)
411+
ctx := context.Background()
412+
413+
req := &csi.NodePublishVolumeRequest{
414+
VolumeId: "vol-1",
415+
TargetPath: "/tmp/target",
416+
StagingTargetPath: "/tmp/staging",
417+
}
418+
419+
mmount.EXPECT().IsMounted("/tmp/target").Return(false, nil)
420+
mmount.EXPECT().BindMount(ctx, "/tmp/staging", "/tmp/target", []string{}).
421+
Return(errors.New("already mounted"))
422+
423+
resp, err := svc.NodePublishVolume(ctx, req)
424+
425+
g.Expect(err).NotTo(BeNil())
426+
g.Expect(resp).To(BeNil())
427+
g.Expect(status.Code(err)).To(Equal(codes.Internal))
428+
}
429+
358430
func TestNodeUnpublishVolume_Success(t *testing.T) {
359431
g := NewGomegaWithT(t)
360432
ctrl := gomock.NewController(t)

0 commit comments

Comments
 (0)