Skip to content

Commit f849d61

Browse files
committed
Add test and integrate mounts into initrd
1 parent 97fe246 commit f849d61

File tree

3 files changed

+144
-4
lines changed

3 files changed

+144
-4
lines changed

lib/instances/configdisk.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,29 @@ GUEST_GW="%s"
104104
GUEST_DNS="%s"
105105
`, netConfig.IP, cidr, netConfig.Gateway, netConfig.DNS)
106106
}
107+
108+
// Build volume mounts section
109+
// Volumes are attached as /dev/vdd, /dev/vde, etc. (after vda=rootfs, vdb=overlay, vdc=config)
110+
volumeSection := ""
111+
if len(inst.Volumes) > 0 {
112+
var volumeLines strings.Builder
113+
volumeLines.WriteString("\n# Volume mounts (device:path:readonly)\n")
114+
volumeLines.WriteString("VOLUME_MOUNTS=\"")
115+
for i, vol := range inst.Volumes {
116+
// Device naming: vdd, vde, vdf, ...
117+
device := fmt.Sprintf("/dev/vd%c", 'd'+i)
118+
readonly := "rw"
119+
if vol.Readonly {
120+
readonly = "ro"
121+
}
122+
if i > 0 {
123+
volumeLines.WriteString(" ")
124+
}
125+
volumeLines.WriteString(fmt.Sprintf("%s:%s:%s", device, vol.MountPath, readonly))
126+
}
127+
volumeLines.WriteString("\"\n")
128+
volumeSection = volumeLines.String()
129+
}
107130

108131
// Generate script as a readable template block
109132
// ENTRYPOINT and CMD contain shell-quoted arrays that will be eval'd in init
@@ -116,13 +139,14 @@ CMD="%s"
116139
WORKDIR=%s
117140
118141
# Environment variables
119-
%s%s`,
142+
%s%s%s`,
120143
inst.Id,
121144
entrypoint,
122145
cmd,
123146
workdir,
124147
envLines.String(),
125148
networkSection,
149+
volumeSection,
126150
)
127151

128152
return script

lib/instances/manager_test.go

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package instances
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"os"
@@ -11,6 +12,7 @@ import (
1112
"time"
1213

1314
"github.com/onkernel/hypeman/cmd/api/config"
15+
"github.com/onkernel/hypeman/lib/exec"
1416
"github.com/onkernel/hypeman/lib/images"
1517
"github.com/onkernel/hypeman/lib/network"
1618
"github.com/onkernel/hypeman/lib/paths"
@@ -200,7 +202,23 @@ func TestCreateAndDeleteInstance(t *testing.T) {
200202
require.NoError(t, err)
201203
t.Log("System files ready")
202204

203-
// Create instance with real nginx image (stays running)
205+
// Create a volume to attach
206+
p := paths.New(tmpDir)
207+
volumeManager := volumes.NewManager(p)
208+
t.Log("Creating volume...")
209+
vol, err := volumeManager.CreateVolume(ctx, volumes.CreateVolumeRequest{
210+
Name: "test-data",
211+
SizeGb: 1,
212+
})
213+
require.NoError(t, err)
214+
require.NotNil(t, vol)
215+
t.Logf("Volume created: %s", vol.Id)
216+
217+
// Verify volume file exists and is not attached
218+
assert.FileExists(t, p.VolumeData(vol.Id))
219+
assert.Nil(t, vol.AttachedTo, "Volume should not be attached yet")
220+
221+
// Create instance with real nginx image and attached volume
204222
req := CreateInstanceRequest{
205223
Name: "test-nginx",
206224
Image: "docker.io/library/nginx:alpine",
@@ -212,6 +230,13 @@ func TestCreateAndDeleteInstance(t *testing.T) {
212230
Env: map[string]string{
213231
"TEST_VAR": "test_value",
214232
},
233+
Volumes: []VolumeAttachment{
234+
{
235+
VolumeID: vol.Id,
236+
MountPath: "/mnt/data",
237+
Readonly: false,
238+
},
239+
},
215240
}
216241

217242
t.Log("Creating instance...")
@@ -228,8 +253,19 @@ func TestCreateAndDeleteInstance(t *testing.T) {
228253
assert.False(t, inst.HasSnapshot)
229254
assert.NotEmpty(t, inst.KernelVersion)
230255

256+
// Verify volume is attached to instance
257+
assert.Len(t, inst.Volumes, 1, "Instance should have 1 volume attached")
258+
assert.Equal(t, vol.Id, inst.Volumes[0].VolumeID)
259+
assert.Equal(t, "/mnt/data", inst.Volumes[0].MountPath)
260+
261+
// Verify volume shows as attached
262+
vol, err = volumeManager.GetVolume(ctx, vol.Id)
263+
require.NoError(t, err)
264+
require.NotNil(t, vol.AttachedTo, "Volume should be attached")
265+
assert.Equal(t, inst.Id, *vol.AttachedTo)
266+
assert.Equal(t, "/mnt/data", *vol.MountPath)
267+
231268
// Verify directories exist
232-
p := paths.New(tmpDir)
233269
assert.DirExists(t, p.InstanceDir(inst.Id))
234270
assert.FileExists(t, p.InstanceMetadata(inst.Id))
235271
assert.FileExists(t, p.InstanceOverlay(inst.Id))
@@ -270,6 +306,50 @@ func TestCreateAndDeleteInstance(t *testing.T) {
270306
// Verify nginx started successfully
271307
assert.True(t, foundNginxStartup, "Nginx should have started worker processes within 5 seconds")
272308

309+
// Test volume is accessible from inside the guest via exec
310+
t.Log("Testing volume from inside guest via exec...")
311+
312+
// Helper to run command in guest
313+
runCmd := func(command ...string) (string, int, error) {
314+
var stdout, stderr bytes.Buffer
315+
exit, err := exec.ExecIntoInstance(ctx, inst.VsockSocket, exec.ExecOptions{
316+
Command: command,
317+
Stdout: &stdout,
318+
Stderr: &stderr,
319+
TTY: false,
320+
})
321+
if err != nil {
322+
return stderr.String(), -1, err
323+
}
324+
return strings.TrimSpace(stdout.String()), exit.Code, nil
325+
}
326+
327+
// Verify volume mount point exists
328+
output, exitCode, err := runCmd("ls", "-la", "/mnt/data")
329+
require.NoError(t, err, "Should be able to ls /mnt/data")
330+
assert.Equal(t, 0, exitCode, "ls /mnt/data should succeed")
331+
t.Logf("Volume mount contents: %s", output)
332+
333+
// Write a test file to the volume
334+
testContent := "hello-from-volume-test"
335+
output, exitCode, err = runCmd("sh", "-c", fmt.Sprintf("echo '%s' > /mnt/data/test.txt", testContent))
336+
require.NoError(t, err, "Should be able to write to volume")
337+
assert.Equal(t, 0, exitCode, "Write to volume should succeed")
338+
339+
// Read the test file back
340+
output, exitCode, err = runCmd("cat", "/mnt/data/test.txt")
341+
require.NoError(t, err, "Should be able to read from volume")
342+
assert.Equal(t, 0, exitCode, "Read from volume should succeed")
343+
assert.Equal(t, testContent, output, "Volume content should match what was written")
344+
t.Log("Volume read/write test passed!")
345+
346+
// Verify it's a real mount (not just a directory)
347+
output, exitCode, err = runCmd("df", "/mnt/data")
348+
require.NoError(t, err, "Should be able to df /mnt/data")
349+
assert.Equal(t, 0, exitCode, "df /mnt/data should succeed")
350+
assert.Contains(t, output, "/dev/vd", "Volume should be mounted from a block device")
351+
t.Logf("Volume mount info: %s", output)
352+
273353
// Test streaming logs with live updates
274354
t.Log("Testing log streaming with live updates...")
275355
streamCtx, streamCancel := context.WithCancel(ctx)
@@ -323,8 +403,23 @@ func TestCreateAndDeleteInstance(t *testing.T) {
323403
// Verify instance no longer exists
324404
_, err = manager.GetInstance(ctx, inst.Id)
325405
assert.ErrorIs(t, err, ErrNotFound)
406+
407+
// Verify volume is detached but still exists
408+
vol, err = volumeManager.GetVolume(ctx, vol.Id)
409+
require.NoError(t, err)
410+
assert.Nil(t, vol.AttachedTo, "Volume should be detached after instance deletion")
411+
assert.FileExists(t, p.VolumeData(vol.Id), "Volume file should still exist")
412+
413+
// Delete volume
414+
t.Log("Deleting volume...")
415+
err = volumeManager.DeleteVolume(ctx, vol.Id)
416+
require.NoError(t, err)
417+
418+
// Verify volume is gone
419+
_, err = volumeManager.GetVolume(ctx, vol.Id)
420+
assert.ErrorIs(t, err, volumes.ErrNotFound)
326421

327-
t.Log("Instance lifecycle test complete!")
422+
t.Log("Instance and volume lifecycle test complete!")
328423
}
329424

330425
func TestStorageOperations(t *testing.T) {

lib/system/init_script.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ else
7171
exit 1
7272
fi
7373
74+
# Mount attached volumes (from config: VOLUME_MOUNTS="device:path:mode device:path:mode ...")
75+
if [ -n "${VOLUME_MOUNTS:-}" ]; then
76+
echo "overlay-init: mounting volumes"
77+
for vol in $VOLUME_MOUNTS; do
78+
device=$(echo "$vol" | cut -d: -f1)
79+
path=$(echo "$vol" | cut -d: -f2)
80+
mode=$(echo "$vol" | cut -d: -f3)
81+
82+
# Create mount point in overlay
83+
mkdir -p "/overlay/newroot${path}"
84+
85+
# Mount with appropriate options
86+
if [ "$mode" = "ro" ]; then
87+
mount -t ext4 -o ro "$device" "/overlay/newroot${path}"
88+
else
89+
mount -t ext4 "$device" "/overlay/newroot${path}"
90+
fi
91+
echo "overlay-init: mounted volume $device at $path ($mode)"
92+
done
93+
fi
94+
7495
# Prepare new root mount points
7596
# We use bind mounts instead of move so that the original /dev remains populated
7697
# for processes running in the initrd namespace (like exec-agent).

0 commit comments

Comments
 (0)