Skip to content

Commit 97fe246

Browse files
committed
Generate volumes feature
1 parent 9e69646 commit 97fe246

File tree

20 files changed

+636
-111
lines changed

20 files changed

+636
-111
lines changed

cmd/api/api/instances.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,23 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
9595
networkEnabled = *request.Body.Network.Enabled
9696
}
9797

98+
// Parse volumes
99+
var volumes []instances.VolumeAttachment
100+
if request.Body.Volumes != nil {
101+
volumes = make([]instances.VolumeAttachment, len(*request.Body.Volumes))
102+
for i, vol := range *request.Body.Volumes {
103+
readonly := false
104+
if vol.Readonly != nil {
105+
readonly = *vol.Readonly
106+
}
107+
volumes[i] = instances.VolumeAttachment{
108+
VolumeID: vol.VolumeId,
109+
MountPath: vol.MountPath,
110+
Readonly: readonly,
111+
}
112+
}
113+
}
114+
98115
domainReq := instances.CreateInstanceRequest{
99116
Name: request.Body.Name,
100117
Image: request.Body.Image,
@@ -104,6 +121,7 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
104121
Vcpus: vcpus,
105122
Env: env,
106123
NetworkEnabled: networkEnabled,
124+
Volumes: volumes,
107125
}
108126

109127
inst, err := s.InstanceManager.CreateInstance(ctx, domainReq)
@@ -359,5 +377,18 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
359377
oapiInst.Env = &inst.Env
360378
}
361379

380+
// Convert volume attachments
381+
if len(inst.Volumes) > 0 {
382+
oapiVolumes := make([]oapi.VolumeAttachment, len(inst.Volumes))
383+
for i, vol := range inst.Volumes {
384+
oapiVolumes[i] = oapi.VolumeAttachment{
385+
VolumeId: vol.VolumeID,
386+
MountPath: vol.MountPath,
387+
Readonly: lo.ToPtr(vol.Readonly),
388+
}
389+
}
390+
oapiInst.Volumes = &oapiVolumes
391+
}
392+
362393
return oapiInst
363394
}

cmd/api/api/volumes.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,18 @@ func (s *ApiService) DeleteVolume(ctx context.Context, request oapi.DeleteVolume
103103
}
104104

105105
func volumeToOAPI(vol volumes.Volume) oapi.Volume {
106-
return oapi.Volume{
106+
oapiVol := oapi.Volume{
107107
Id: vol.Id,
108108
Name: vol.Name,
109109
SizeGb: vol.SizeGb,
110110
CreatedAt: vol.CreatedAt,
111111
}
112+
if vol.AttachedTo != nil {
113+
oapiVol.AttachedTo = vol.AttachedTo
114+
}
115+
if vol.MountPath != nil {
116+
oapiVol.MountPath = vol.MountPath
117+
}
118+
return oapiVol
112119
}
113120

cmd/api/wire_gen.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
6666
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
6767
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
6868
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
69+
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
70+
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
6971
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7072
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
7173
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -203,6 +205,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
203205
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
204206
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
205207
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
208+
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
209+
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
206210
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
207211
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
208212
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -227,6 +231,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
227231
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
228232
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
229233
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
234+
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
235+
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
230236
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
231237
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
232238
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=

lib/images/disk.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,33 @@ func dirSize(path string) (int64, error) {
199199
return size, err
200200
}
201201

202+
// CreateEmptyExt4Disk creates a sparse disk file and formats it as ext4.
203+
// Used for volumes and instance overlays that need empty writable filesystems.
204+
func CreateEmptyExt4Disk(diskPath string, sizeBytes int64) error {
205+
// Ensure parent directory exists
206+
if err := os.MkdirAll(filepath.Dir(diskPath), 0755); err != nil {
207+
return fmt.Errorf("create disk parent dir: %w", err)
208+
}
209+
210+
// Create sparse file
211+
file, err := os.Create(diskPath)
212+
if err != nil {
213+
return fmt.Errorf("create disk file: %w", err)
214+
}
215+
file.Close()
216+
217+
// Truncate to specified size to create sparse file
218+
if err := os.Truncate(diskPath, sizeBytes); err != nil {
219+
return fmt.Errorf("truncate disk file: %w", err)
220+
}
221+
222+
// Format as ext4
223+
cmd := exec.Command("mkfs.ext4", "-F", diskPath)
224+
output, err := cmd.CombinedOutput()
225+
if err != nil {
226+
return fmt.Errorf("mkfs.ext4 failed: %w, output: %s", err, output)
227+
}
228+
229+
return nil
230+
}
231+

lib/instances/create.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/onkernel/hypeman/lib/network"
1313
"github.com/onkernel/hypeman/lib/system"
1414
"github.com/onkernel/hypeman/lib/vmm"
15+
"github.com/onkernel/hypeman/lib/volumes"
1516
"gvisor.dev/gvisor/pkg/cleanup"
1617
)
1718

@@ -179,6 +180,41 @@ func (m *manager) createInstance(
179180
})
180181
}
181182

183+
// 10.5. Validate and attach volumes
184+
if len(req.Volumes) > 0 {
185+
log.DebugContext(ctx, "validating volumes", "id", id, "count", len(req.Volumes))
186+
for _, volAttach := range req.Volumes {
187+
// Check volume exists and is not attached
188+
vol, err := m.volumeManager.GetVolume(ctx, volAttach.VolumeID)
189+
if err != nil {
190+
log.ErrorContext(ctx, "volume not found", "id", id, "volume_id", volAttach.VolumeID, "error", err)
191+
return nil, fmt.Errorf("volume %s: %w", volAttach.VolumeID, err)
192+
}
193+
if vol.AttachedTo != nil {
194+
log.ErrorContext(ctx, "volume already attached", "id", id, "volume_id", volAttach.VolumeID, "attached_to", *vol.AttachedTo)
195+
return nil, fmt.Errorf("volume %s is already attached to instance %s", volAttach.VolumeID, *vol.AttachedTo)
196+
}
197+
198+
// Mark volume as attached
199+
if err := m.volumeManager.AttachVolume(ctx, volAttach.VolumeID, volumes.AttachVolumeRequest{
200+
InstanceID: id,
201+
MountPath: volAttach.MountPath,
202+
Readonly: volAttach.Readonly,
203+
}); err != nil {
204+
log.ErrorContext(ctx, "failed to attach volume", "id", id, "volume_id", volAttach.VolumeID, "error", err)
205+
return nil, fmt.Errorf("attach volume %s: %w", volAttach.VolumeID, err)
206+
}
207+
208+
// Add volume cleanup to stack
209+
volumeID := volAttach.VolumeID // capture for closure
210+
cu.Add(func() {
211+
m.volumeManager.DetachVolume(ctx, volumeID)
212+
})
213+
}
214+
// Store volume attachments in metadata
215+
stored.Volumes = req.Volumes
216+
}
217+
182218
// 11. Create config disk (needs Instance for buildVMConfig)
183219
inst := &Instance{StoredMetadata: *stored}
184220
log.DebugContext(ctx, "creating config disk", "id", id)
@@ -388,6 +424,15 @@ func (m *manager) buildVMConfig(inst *Instance, imageInfo *images.Image, netConf
388424
},
389425
}
390426

427+
// Add attached volumes as additional disks
428+
for _, volAttach := range inst.Volumes {
429+
volumePath := m.volumeManager.GetVolumePath(volAttach.VolumeID)
430+
disks = append(disks, vmm.DiskConfig{
431+
Path: &volumePath,
432+
Readonly: ptr(volAttach.Readonly),
433+
})
434+
}
435+
391436
// Serial console configuration
392437
serial := vmm.ConsoleConfig{
393438
Mode: vmm.ConsoleConfigMode("File"),

lib/instances/delete.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,18 @@ func (m *manager) deleteInstance(
5858
}
5959
}
6060

61-
// 5. Delete all instance data
61+
// 5. Detach volumes
62+
if len(inst.Volumes) > 0 {
63+
log.DebugContext(ctx, "detaching volumes", "id", id, "count", len(inst.Volumes))
64+
for _, volAttach := range inst.Volumes {
65+
if err := m.volumeManager.DetachVolume(ctx, volAttach.VolumeID); err != nil {
66+
// Log error but continue with cleanup
67+
log.WarnContext(ctx, "failed to detach volume, continuing with cleanup", "id", id, "volume_id", volAttach.VolumeID, "error", err)
68+
}
69+
}
70+
}
71+
72+
// 6. Delete all instance data
6273
log.DebugContext(ctx, "deleting instance data", "id", id)
6374
if err := m.deleteInstanceData(id); err != nil {
6475
log.ErrorContext(ctx, "failed to delete instance data", "id", id, "error", err)

lib/instances/manager.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/onkernel/hypeman/lib/network"
1010
"github.com/onkernel/hypeman/lib/paths"
1111
"github.com/onkernel/hypeman/lib/system"
12+
"github.com/onkernel/hypeman/lib/volumes"
1213
)
1314

1415
type Manager interface {
@@ -29,18 +30,20 @@ type manager struct {
2930
imageManager images.Manager
3031
systemManager system.Manager
3132
networkManager network.Manager
32-
maxOverlaySize int64 // Maximum overlay disk size in bytes
33-
instanceLocks sync.Map // map[string]*sync.RWMutex - per-instance locks
34-
hostTopology *HostTopology // Cached host CPU topology
33+
volumeManager volumes.Manager
34+
maxOverlaySize int64 // Maximum overlay disk size in bytes
35+
instanceLocks sync.Map // map[string]*sync.RWMutex - per-instance locks
36+
hostTopology *HostTopology // Cached host CPU topology
3537
}
3638

3739
// NewManager creates a new instances manager
38-
func NewManager(p *paths.Paths, imageManager images.Manager, systemManager system.Manager, networkManager network.Manager, maxOverlaySize int64) Manager {
40+
func NewManager(p *paths.Paths, imageManager images.Manager, systemManager system.Manager, networkManager network.Manager, volumeManager volumes.Manager, maxOverlaySize int64) Manager {
3941
return &manager{
4042
paths: p,
4143
imageManager: imageManager,
4244
systemManager: systemManager,
4345
networkManager: networkManager,
46+
volumeManager: volumeManager,
4447
maxOverlaySize: maxOverlaySize,
4548
instanceLocks: sync.Map{},
4649
hostTopology: detectHostTopology(), // Detect and cache host topology

lib/instances/manager_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/onkernel/hypeman/lib/paths"
1717
"github.com/onkernel/hypeman/lib/system"
1818
"github.com/onkernel/hypeman/lib/vmm"
19+
"github.com/onkernel/hypeman/lib/volumes"
1920
"github.com/stretchr/testify/assert"
2021
"github.com/stretchr/testify/require"
2122
)
@@ -37,8 +38,9 @@ func setupTestManager(t *testing.T) (*manager, string) {
3738

3839
systemManager := system.NewManager(p)
3940
networkManager := network.NewManager(p, cfg)
41+
volumeManager := volumes.NewManager(p)
4042
maxOverlaySize := int64(100 * 1024 * 1024 * 1024)
41-
mgr := NewManager(p, imageManager, systemManager, networkManager, maxOverlaySize).(*manager)
43+
mgr := NewManager(p, imageManager, systemManager, networkManager, volumeManager, maxOverlaySize).(*manager)
4244

4345
// Register cleanup to kill any orphaned Cloud Hypervisor processes
4446
t.Cleanup(func() {
@@ -340,8 +342,9 @@ func TestStorageOperations(t *testing.T) {
340342
imageManager, _ := images.NewManager(p, 1)
341343
systemManager := system.NewManager(p)
342344
networkManager := network.NewManager(p, cfg)
345+
volumeManager := volumes.NewManager(p)
343346
maxOverlaySize := int64(100 * 1024 * 1024 * 1024) // 100GB
344-
manager := NewManager(p, imageManager, systemManager, networkManager, maxOverlaySize).(*manager)
347+
manager := NewManager(p, imageManager, systemManager, networkManager, volumeManager, maxOverlaySize).(*manager)
345348

346349
// Test metadata doesn't exist initially
347350
_, err := manager.loadMetadata("nonexistent")

lib/instances/storage.go

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7-
"os/exec"
87
"path/filepath"
8+
9+
"github.com/onkernel/hypeman/lib/images"
910
)
1011

1112
// Filesystem structure:
@@ -83,27 +84,7 @@ func (m *manager) saveMetadata(meta *metadata) error {
8384
// createOverlayDisk creates a sparse overlay disk for the instance
8485
func (m *manager) createOverlayDisk(id string, sizeBytes int64) error {
8586
overlayPath := m.paths.InstanceOverlay(id)
86-
87-
// Create sparse file
88-
file, err := os.Create(overlayPath)
89-
if err != nil {
90-
return fmt.Errorf("create overlay disk: %w", err)
91-
}
92-
file.Close()
93-
94-
// Truncate to specified size to create sparse file
95-
if err := os.Truncate(overlayPath, sizeBytes); err != nil {
96-
return fmt.Errorf("truncate overlay disk: %w", err)
97-
}
98-
99-
// Format as ext4 (VM will mount this as writable overlay)
100-
cmd := exec.Command("mkfs.ext4", "-F", overlayPath)
101-
output, err := cmd.CombinedOutput()
102-
if err != nil {
103-
return fmt.Errorf("mkfs.ext4 on overlay: %w, output: %s", err, output)
104-
}
105-
106-
return nil
87+
return images.CreateEmptyExt4Disk(overlayPath, sizeBytes)
10788
}
10889

10990
// deleteInstanceData removes all instance data from disk

0 commit comments

Comments
 (0)