Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,15 @@ dev: ensure-ch-binaries ensure-caddy-binaries lib/system/guest_agent/guest-agent
# Run tests (as root for network capabilities, enables caching and parallelism)
# Usage: make test - runs all tests
# make test TEST=TestCreateInstanceWithNetwork - runs specific test
# make test VERBOSE=1 - runs with verbose output
test: ensure-ch-binaries ensure-caddy-binaries lib/system/guest_agent/guest-agent
@if [ -n "$(TEST)" ]; then \
@VERBOSE_FLAG=""; \
if [ -n "$(VERBOSE)" ]; then VERBOSE_FLAG="-v"; fi; \
if [ -n "$(TEST)" ]; then \
echo "Running specific test: $(TEST)"; \
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp -run=$(TEST) -v -timeout=180s ./...; \
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp -run=$(TEST) $$VERBOSE_FLAG -timeout=180s ./...; \
else \
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp -v -timeout=180s ./...; \
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp $$VERBOSE_FLAG -timeout=180s ./...; \
fi

# Generate JWT token for testing
Expand Down
4 changes: 4 additions & 0 deletions cmd/api/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/onkernel/hypeman/lib/instances"
"github.com/onkernel/hypeman/lib/network"
"github.com/onkernel/hypeman/lib/oapi"
"github.com/onkernel/hypeman/lib/resources"
"github.com/onkernel/hypeman/lib/volumes"
)

Expand All @@ -20,6 +21,7 @@ type ApiService struct {
NetworkManager network.Manager
DeviceManager devices.Manager
IngressManager ingress.Manager
ResourceManager *resources.Manager
}

var _ oapi.StrictServerInterface = (*ApiService)(nil)
Expand All @@ -33,6 +35,7 @@ func New(
networkManager network.Manager,
deviceManager devices.Manager,
ingressManager ingress.Manager,
resourceManager *resources.Manager,
) *ApiService {
return &ApiService{
Config: config,
Expand All @@ -42,5 +45,6 @@ func New(
NetworkManager: networkManager,
DeviceManager: deviceManager,
IngressManager: ingressManager,
ResourceManager: resourceManager,
}
}
8 changes: 6 additions & 2 deletions cmd/api/api/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ func TestCpToAndFromInstance(t *testing.T) {
Name: "cp-test",
Image: "docker.io/library/nginx:alpine",
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down Expand Up @@ -182,7 +184,9 @@ func TestCpDirectoryToInstance(t *testing.T) {
Name: "cp-dir-test",
Image: "docker.io/library/nginx:alpine",
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down
8 changes: 6 additions & 2 deletions cmd/api/api/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ func TestExecInstanceNonTTY(t *testing.T) {
Name: "exec-test",
Image: "docker.io/library/nginx:alpine",
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down Expand Up @@ -200,7 +202,9 @@ func TestExecWithDebianMinimal(t *testing.T) {
Name: "debian-exec-test",
Image: "docker.io/library/debian:12-slim",
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down
121 changes: 104 additions & 17 deletions cmd/api/api/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/c2h5oh/datasize"
"github.com/onkernel/hypeman/lib/guest"
Expand All @@ -15,6 +16,7 @@ import (
mw "github.com/onkernel/hypeman/lib/middleware"
"github.com/onkernel/hypeman/lib/network"
"github.com/onkernel/hypeman/lib/oapi"
"github.com/onkernel/hypeman/lib/resources"
"github.com/samber/lo"
)

Expand Down Expand Up @@ -82,6 +84,23 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
overlaySize = int64(overlayBytes)
}

// Parse disk_io_bps (0 = auto/unlimited)
diskIOBps := int64(0)
if request.Body.DiskIoBps != nil && *request.Body.DiskIoBps != "" {
var ioBpsBytes datasize.ByteSize
// Remove "/s" suffix if present
ioStr := *request.Body.DiskIoBps
ioStr = strings.TrimSuffix(ioStr, "/s")
ioStr = strings.TrimSuffix(ioStr, "ps")
if err := ioBpsBytes.UnmarshalText([]byte(ioStr)); err != nil {
return oapi.CreateInstance400JSONResponse{
Code: "invalid_disk_io_bps",
Message: fmt.Sprintf("invalid disk_io_bps format: %v", err),
}, nil
}
diskIOBps = int64(ioBpsBytes)
}

vcpus := 2
if request.Body.Vcpus != nil {
vcpus = *request.Body.Vcpus
Expand All @@ -98,6 +117,33 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
networkEnabled = *request.Body.Network.Enabled
}

// Parse network bandwidth limits (0 = auto)
// Supports both bit-based (e.g., "1Gbps") and byte-based (e.g., "125MB/s") formats
var networkBandwidthDownload int64
var networkBandwidthUpload int64
if request.Body.Network != nil {
if request.Body.Network.BandwidthDownload != nil && *request.Body.Network.BandwidthDownload != "" {
bw, err := resources.ParseBandwidth(*request.Body.Network.BandwidthDownload)
if err != nil {
return oapi.CreateInstance400JSONResponse{
Code: "invalid_bandwidth_download",
Message: fmt.Sprintf("invalid bandwidth_download format: %v", err),
}, nil
}
networkBandwidthDownload = bw
}
if request.Body.Network.BandwidthUpload != nil && *request.Body.Network.BandwidthUpload != "" {
bw, err := resources.ParseBandwidth(*request.Body.Network.BandwidthUpload)
if err != nil {
return oapi.CreateInstance400JSONResponse{
Code: "invalid_bandwidth_upload",
Message: fmt.Sprintf("invalid bandwidth_upload format: %v", err),
}, nil
}
networkBandwidthUpload = bw
}
}

// Parse devices (GPU passthrough)
var deviceRefs []string
if request.Body.Devices != nil {
Expand Down Expand Up @@ -144,18 +190,36 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
hvType = hypervisor.Type(*request.Body.Hypervisor)
}

// Calculate default resource limits when not specified (0 = auto)
// Uses proportional allocation based on CPU: (vcpus / cpuCapacity) * resourceCapacity
if diskIOBps == 0 {
diskIOBps, _ = s.ResourceManager.DefaultDiskIOBandwidth(vcpus)
}
if networkBandwidthDownload == 0 || networkBandwidthUpload == 0 {
defaultDown, defaultUp := s.ResourceManager.DefaultNetworkBandwidth(vcpus)
if networkBandwidthDownload == 0 {
networkBandwidthDownload = defaultDown
}
if networkBandwidthUpload == 0 {
networkBandwidthUpload = defaultUp
}
}

domainReq := instances.CreateInstanceRequest{
Name: request.Body.Name,
Image: request.Body.Image,
Size: size,
HotplugSize: hotplugSize,
OverlaySize: overlaySize,
Vcpus: vcpus,
Env: env,
NetworkEnabled: networkEnabled,
Devices: deviceRefs,
Volumes: volumes,
Hypervisor: hvType,
Name: request.Body.Name,
Image: request.Body.Image,
Size: size,
HotplugSize: hotplugSize,
OverlaySize: overlaySize,
Vcpus: vcpus,
DiskIOBps: diskIOBps,
NetworkBandwidthDownload: networkBandwidthDownload,
NetworkBandwidthUpload: networkBandwidthUpload,
Env: env,
NetworkEnabled: networkEnabled,
Devices: deviceRefs,
Volumes: volumes,
Hypervisor: hvType,
}

inst, err := s.InstanceManager.CreateInstance(ctx, domainReq)
Expand Down Expand Up @@ -539,14 +603,29 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
hotplugSizeStr := datasize.ByteSize(inst.HotplugSize).HR()
overlaySizeStr := datasize.ByteSize(inst.OverlaySize).HR()

// Build network object with ip/mac nested inside
// Format bandwidth as human-readable (bytes/s to rate string)
var downloadBwStr, uploadBwStr *string
if inst.NetworkBandwidthDownload > 0 {
s := datasize.ByteSize(inst.NetworkBandwidthDownload).HR() + "/s"
downloadBwStr = &s
}
if inst.NetworkBandwidthUpload > 0 {
s := datasize.ByteSize(inst.NetworkBandwidthUpload).HR() + "/s"
uploadBwStr = &s
}

// Build network object with ip/mac and bandwidth nested inside
netObj := &struct {
Enabled *bool `json:"enabled,omitempty"`
Ip *string `json:"ip"`
Mac *string `json:"mac"`
Name *string `json:"name,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Ip *string `json:"ip"`
Mac *string `json:"mac"`
Name *string `json:"name,omitempty"`
}{
Enabled: lo.ToPtr(inst.NetworkEnabled),
Enabled: lo.ToPtr(inst.NetworkEnabled),
BandwidthDownload: downloadBwStr,
BandwidthUpload: uploadBwStr,
}
if inst.NetworkEnabled {
netObj.Name = lo.ToPtr("default")
Expand All @@ -557,6 +636,13 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
// Convert hypervisor type
hvType := oapi.InstanceHypervisor(inst.HypervisorType)

// Format disk I/O as human-readable
var diskIoBpsStr *string
if inst.DiskIOBps > 0 {
s := datasize.ByteSize(inst.DiskIOBps).HR() + "/s"
diskIoBpsStr = &s
}

oapiInst := oapi.Instance{
Id: inst.Id,
Name: inst.Name,
Expand All @@ -567,6 +653,7 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
HotplugSize: lo.ToPtr(hotplugSizeStr),
OverlaySize: lo.ToPtr(overlaySizeStr),
Vcpus: lo.ToPtr(inst.Vcpus),
DiskIoBps: diskIoBpsStr,
Network: netObj,
CreatedAt: inst.CreatedAt,
StartedAt: inst.StartedAt,
Expand Down
22 changes: 14 additions & 8 deletions cmd/api/api/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ func TestCreateInstance_ParsesHumanReadableSizes(t *testing.T) {
HotplugSize: &hotplugSize,
OverlaySize: &overlaySize,
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down Expand Up @@ -109,7 +111,9 @@ func TestCreateInstance_InvalidSizeFormat(t *testing.T) {
Image: "docker.io/library/alpine:latest",
Size: &invalidSize,
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down Expand Up @@ -150,7 +154,9 @@ func TestInstanceLifecycle_StopStart(t *testing.T) {
Name: "test-lifecycle",
Image: "docker.io/library/nginx:alpine",
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down Expand Up @@ -208,11 +214,11 @@ func waitForState(t *testing.T, svc *ApiService, instanceID string, expectedStat
inst, err := svc.InstanceManager.GetInstance(ctx(), instanceID)
require.NoError(t, err)

if string(inst.State) == expectedState {
t.Logf("Instance reached %s state", expectedState)
return
}
t.Logf("Instance state: %s (waiting for %s)", inst.State, expectedState)
if string(inst.State) == expectedState {
t.Logf("Instance reached %s state", expectedState)
return
}
t.Logf("Instance state: %s (waiting for %s)", inst.State, expectedState)
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("Timeout waiting for instance to reach %s state", expectedState)
Expand Down
4 changes: 3 additions & 1 deletion cmd/api/api/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ func TestRegistryPushAndCreateInstance(t *testing.T) {
Name: "test-pushed-image",
Image: imageName,
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
Expand Down
Loading
Loading