diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b22d451..22b499b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: # Not necessary to upload cache on self-hosted runner(s) # ~/go/pkg/mod and ~/.cache/go-build stay on disk between runs automatically. cache: false - go-version: '1.25' + go-version: '1.25.4' - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ff9862d..2c04026b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,14 +15,16 @@ jobs: # Not necessary to upload cache on self-hosted runner(s) # ~/go/pkg/mod and ~/.cache/go-build stay on disk between runs automatically. cache: false - go-version: '1.25' + go-version: '1.25.4' - name: Install dependencies run: | set -xe - if ! command -v mkfs.erofs &> /dev/null; then + if ! command -v mkfs.erofs &> /dev/null || \ + ! command -v mkfs.ext4 &> /dev/null || \ + ! command -v iptables &> /dev/null; then sudo apt-get update - sudo apt-get install -y erofs-utils + sudo apt-get install -y erofs-utils e2fsprogs iptables fi go mod download @@ -67,7 +69,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v6 with: - go-version: '1.25' + go-version: '1.25.4' cache: false - name: Login to Docker Hub uses: docker/login-action@v3 @@ -107,7 +109,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v6 with: - go-version: '1.25' + go-version: '1.25.4' cache: false - name: Install dependencies run: brew list caddy &>/dev/null || brew install caddy diff --git a/Makefile b/Makefile index f3e1cf91..e2656e14 100644 --- a/Makefile +++ b/Makefile @@ -228,12 +228,13 @@ endif # Linux tests (as root for network capabilities) test-linux: ensure-ch-binaries ensure-caddy-binaries build-embedded @VERBOSE_FLAG=""; \ + TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$$PATH"; \ 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) $$VERBOSE_FLAG -timeout=300s ./...; \ + sudo env "PATH=$$TEST_PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp -run=$(TEST) $$VERBOSE_FLAG -timeout=300s ./...; \ else \ - sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp $$VERBOSE_FLAG -timeout=300s ./...; \ + sudo env "PATH=$$TEST_PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp $$VERBOSE_FLAG -timeout=300s ./...; \ fi # macOS tests (no sudo needed, adds e2fsprogs to PATH) diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index 10da1074..50deb885 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -72,7 +72,7 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst size = int64(sizeBytes) } - // Parse hotplug_size (default: 3GB) + // Parse hotplug_size (optional; omitted/empty means no hotplug memory) hotplugSize := int64(0) if request.Body.HotplugSize != nil && *request.Body.HotplugSize != "" { var hotplugBytes datasize.ByteSize diff --git a/cmd/api/api/instances_test.go b/cmd/api/api/instances_test.go index 81af2bd5..8c74cf89 100644 --- a/cmd/api/api/instances_test.go +++ b/cmd/api/api/instances_test.go @@ -1,10 +1,14 @@ package api import ( + "context" "os" "testing" "time" + "github.com/c2h5oh/datasize" + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances" "github.com/kernel/hypeman/lib/oapi" "github.com/kernel/hypeman/lib/paths" "github.com/kernel/hypeman/lib/system" @@ -128,6 +132,64 @@ func TestCreateInstance_InvalidSizeFormat(t *testing.T) { assert.Contains(t, badReq.Message, "invalid size format") } +type captureCreateManager struct { + instances.Manager + lastReq *instances.CreateInstanceRequest +} + +func (m *captureCreateManager) CreateInstance(ctx context.Context, req instances.CreateInstanceRequest) (*instances.Instance, error) { + reqCopy := req + m.lastReq = &reqCopy + + now := time.Now() + return &instances.Instance{ + StoredMetadata: instances.StoredMetadata{ + Id: "inst-hotplug-default", + Name: req.Name, + Image: req.Image, + Size: req.Size, + HotplugSize: req.HotplugSize, + OverlaySize: req.OverlaySize, + Vcpus: req.Vcpus, + CreatedAt: now, + HypervisorType: hypervisor.TypeCloudHypervisor, + }, + State: instances.StateRunning, + }, nil +} + +func TestCreateInstance_OmittedHotplugSizeDefaultsToZero(t *testing.T) { + svc := newTestService(t) + + origMgr := svc.InstanceManager + mockMgr := &captureCreateManager{Manager: origMgr} + svc.InstanceManager = mockMgr + + size := "1GB" + overlaySize := "10GB" + resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{ + Body: &oapi.CreateInstanceRequest{ + Name: "test-no-hotplug", + Image: "docker.io/library/alpine:latest", + Size: &size, + OverlaySize: &overlaySize, + }, + }) + require.NoError(t, err) + + created, ok := resp.(oapi.CreateInstance201JSONResponse) + require.True(t, ok, "expected 201 response") + assert.NotNil(t, mockMgr.lastReq, "CreateInstance should be called") + assert.Equal(t, int64(0), mockMgr.lastReq.HotplugSize, "omitted hotplug_size should not allocate default memory") + + instance := oapi.Instance(created) + require.NotNil(t, instance.HotplugSize) + + var hotplugBytes datasize.ByteSize + require.NoError(t, hotplugBytes.UnmarshalText([]byte(*instance.HotplugSize))) + assert.Equal(t, int64(0), int64(hotplugBytes), "response should report zero hotplug_size when omitted") +} + func TestInstanceLifecycle_StopStart(t *testing.T) { // Require KVM access for VM creation if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) { diff --git a/lib/instances/create.go b/lib/instances/create.go index 003a984c..46e9ac30 100644 --- a/lib/instances/create.go +++ b/lib/instances/create.go @@ -129,9 +129,6 @@ func (m *manager) createInstance( size = 1 * 1024 * 1024 * 1024 // 1GB default } hotplugSize := req.HotplugSize - if hotplugSize == 0 { - hotplugSize = 3 * 1024 * 1024 * 1024 // 3GB default - } overlaySize := req.OverlaySize if overlaySize == 0 { overlaySize = 10 * 1024 * 1024 * 1024 // 10GB default diff --git a/lib/instances/types.go b/lib/instances/types.go index 1b270e6a..9afbccc2 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -150,7 +150,7 @@ type CreateInstanceRequest struct { Name string // Required Image string // Required: OCI reference Size int64 // Base memory in bytes (default: 1GB) - HotplugSize int64 // Hotplug memory in bytes (default: 3GB) + HotplugSize int64 // Hotplug memory in bytes (default: 0, set explicitly to enable) OverlaySize int64 // Overlay disk size in bytes (default: 10GB) Vcpus int // Default 2 NetworkBandwidthDownload int64 // Download rate limit bytes/sec (0 = auto, proportional to CPU) diff --git a/openapi.yaml b/openapi.yaml index af4abb38..2e6977fd 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -119,8 +119,7 @@ components: example: "2GB" hotplug_size: type: string - description: Additional memory for hotplug (human-readable format like "3GB", "1G") - default: "3GB" + description: Additional memory for hotplug (human-readable format like "3GB", "1G"). Omit to disable hotplug memory. example: "2GB" overlay_size: type: string