Skip to content

Commit 38d6308

Browse files
tryuuuclaude
andcommitted
feat: implement CreateVolume with loopback image creation via Job
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cf749fc commit 38d6308

3 files changed

Lines changed: 123 additions & 4 deletions

File tree

charts/csi-driver/templates/controller-deployment.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ spec:
2323
valueFrom:
2424
fieldRef:
2525
fieldPath: spec.nodeName
26+
- name: CSI_IMAGE
27+
value: {{ .Values.image.repository }}:{{ .Values.image.tag }}
2628
volumeMounts:
2729
- name: socket-dir
2830
mountPath: /csi

internal/driver/controller.go

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
topologyKey = "kubernetes.io/hostname"
2323
deleteJobNS = "kube-system"
2424
deleteJobTimeout = 2 * time.Minute
25+
createJobTimeout = 5 * time.Minute
2526
)
2627

2728
var errNodeNotFound = errors.New("node not found")
@@ -30,28 +31,44 @@ var errNoNodeAffinity = errors.New("no nodeAffinity on PV")
3031
type ControllerServer struct {
3132
csi.UnimplementedControllerServer
3233
kubeClient kubernetes.Interface
34+
jobImage string
3335
}
3436

35-
func (s *ControllerServer) CreateVolume(_ context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
37+
func (s *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
3638
if req.GetName() == "" {
3739
return nil, status.Error(codes.InvalidArgument, "volume name is required")
3840
}
3941

42+
sizeBytes := req.GetCapacityRange().GetRequiredBytes()
43+
if sizeBytes == 0 {
44+
return nil, status.Error(codes.InvalidArgument, "required bytes must be greater than 0")
45+
}
46+
4047
// Preferred → Requisite の順でトポロジーを選ぶ。
4148
// external-provisioner が WaitForFirstConsumer で決定したノードに PV を固定
4249
var topology []*csi.Topology
50+
var nodeName string
4351
if topo := req.GetAccessibilityRequirements(); topo != nil {
4452
if len(topo.GetPreferred()) > 0 {
4553
topology = []*csi.Topology{topo.GetPreferred()[0]}
54+
nodeName = topo.GetPreferred()[0].GetSegments()[topologyKey]
4655
} else if len(topo.GetRequisite()) > 0 {
4756
topology = []*csi.Topology{topo.GetRequisite()[0]}
57+
nodeName = topo.GetRequisite()[0].GetSegments()[topologyKey]
4858
}
4959
}
60+
if nodeName == "" {
61+
return nil, status.Error(codes.InvalidArgument, "cannot determine target node from topology")
62+
}
63+
64+
if err := s.runCreateJob(ctx, req.GetName(), nodeName, sizeBytes); err != nil {
65+
return nil, status.Errorf(codes.Internal, "create job failed: %v", err)
66+
}
5067

5168
return &csi.CreateVolumeResponse{
5269
Volume: &csi.Volume{
5370
VolumeId: req.GetName(),
54-
CapacityBytes: req.GetCapacityRange().GetRequiredBytes(),
71+
CapacityBytes: sizeBytes,
5572
AccessibleTopology: topology,
5673
},
5774
}, nil
@@ -107,7 +124,104 @@ func (s *ControllerServer) nodeFromPV(ctx context.Context, volumeID string) (str
107124
return "", errNodeNotFound
108125
}
109126

110-
// は対象ノードで `rm -rf <volumeBasePath>/<volumeID>` を実行する Job を作成し、完了を待つ。
127+
// 対象ノードでイメージファイルの作成・ループデバイス割り当て・フォーマットを行う Job を実行する。
128+
func (s *ControllerServer) runCreateJob(ctx context.Context, volumeID, nodeName string, sizeBytes int64) error {
129+
jobName := createJobName(volumeID)
130+
imgPath := fmt.Sprintf("%s/%s.img", volumeBasePath, volumeID)
131+
132+
script := fmt.Sprintf(`set -e
133+
IMG='%s'
134+
if [ ! -f "$IMG" ]; then
135+
fallocate -l %d "$IMG"
136+
fi
137+
DEV=$(losetup -j "$IMG" | cut -d: -f1)
138+
if [ -z "$DEV" ]; then
139+
DEV=$(losetup -f --show "$IMG")
140+
fi
141+
if ! blkid "$DEV" >/dev/null 2>&1; then
142+
mkfs.ext4 "$DEV"
143+
fi`, imgPath, sizeBytes)
144+
145+
privileged := true
146+
job := &batchv1.Job{
147+
ObjectMeta: metav1.ObjectMeta{
148+
Name: jobName,
149+
Namespace: deleteJobNS,
150+
},
151+
Spec: batchv1.JobSpec{
152+
Template: corev1.PodTemplateSpec{
153+
Spec: corev1.PodSpec{
154+
NodeName: nodeName,
155+
RestartPolicy: corev1.RestartPolicyNever,
156+
Containers: []corev1.Container{
157+
{
158+
Name: "create",
159+
Image: s.jobImage,
160+
Command: []string{"/bin/sh", "-c", script},
161+
SecurityContext: &corev1.SecurityContext{
162+
Privileged: &privileged,
163+
},
164+
VolumeMounts: []corev1.VolumeMount{
165+
{Name: "data", MountPath: volumeBasePath},
166+
{Name: "dev", MountPath: "/dev"},
167+
},
168+
},
169+
},
170+
Volumes: []corev1.Volume{
171+
{
172+
Name: "data",
173+
VolumeSource: corev1.VolumeSource{
174+
HostPath: &corev1.HostPathVolumeSource{Path: volumeBasePath},
175+
},
176+
},
177+
{
178+
Name: "dev",
179+
VolumeSource: corev1.VolumeSource{
180+
HostPath: &corev1.HostPathVolumeSource{Path: "/dev"},
181+
},
182+
},
183+
},
184+
},
185+
},
186+
},
187+
}
188+
189+
_, err := s.kubeClient.BatchV1().Jobs(deleteJobNS).Create(ctx, job, metav1.CreateOptions{})
190+
if err != nil && !k8serrors.IsAlreadyExists(err) {
191+
return err
192+
}
193+
194+
deadline := time.Now().Add(createJobTimeout)
195+
for time.Now().Before(deadline) {
196+
j, err := s.kubeClient.BatchV1().Jobs(deleteJobNS).Get(ctx, jobName, metav1.GetOptions{})
197+
if err != nil {
198+
return err
199+
}
200+
if j.Status.Succeeded > 0 {
201+
_ = s.kubeClient.BatchV1().Jobs(deleteJobNS).Delete(ctx, jobName, metav1.DeleteOptions{})
202+
return nil
203+
}
204+
if j.Status.Failed > 3 {
205+
_ = s.kubeClient.BatchV1().Jobs(deleteJobNS).Delete(ctx, jobName, metav1.DeleteOptions{})
206+
return fmt.Errorf("create job failed after retries")
207+
}
208+
time.Sleep(3 * time.Second)
209+
}
210+
211+
return fmt.Errorf("create job timed out after %v", createJobTimeout)
212+
}
213+
214+
func createJobName(volumeID string) string {
215+
const prefix = "csi-create-"
216+
name := prefix + volumeID
217+
if len(name) <= 63 {
218+
return name
219+
}
220+
h := sha256.Sum256([]byte(volumeID))
221+
return fmt.Sprintf("%s%x", prefix, h[:])[:63]
222+
}
223+
224+
// 対象ノードで `rm -rf <volumeBasePath>/<volumeID>` を実行する Job を作成し、完了を待つ。
111225
func (s *ControllerServer) runDeleteJob(ctx context.Context, volumeID, nodeName string) error {
112226
jobName := deleteJobName(volumeID)
113227

internal/driver/server.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ func Run(socketPath string) error {
3434

3535
srv := grpc.NewServer()
3636
csi.RegisterIdentityServer(srv, &IdentityServer{})
37-
csi.RegisterControllerServer(srv, &ControllerServer{kubeClient: kubeClient})
37+
csi.RegisterControllerServer(srv, &ControllerServer{
38+
kubeClient: kubeClient,
39+
jobImage: os.Getenv("CSI_IMAGE"),
40+
})
3841
csi.RegisterNodeServer(srv, nodeServer)
3942

4043
return srv.Serve(lis)

0 commit comments

Comments
 (0)