host daemon touches (#4)

* feat: launch config tests

* feat: readiness probe port alignment
This commit is contained in:
Hari 2026-04-08 12:56:07 -04:00 committed by GitHub
parent e2f9e54970
commit 592df1e1df
10 changed files with 439 additions and 49 deletions

View file

@ -77,6 +77,12 @@ func (d *Daemon) CreateMachine(ctx context.Context, req contracthost.CreateMachi
return nil, err
}
ports := defaultMachinePorts()
if err := waitForGuestReady(ctx, state.RuntimeHost, ports); err != nil {
_ = d.runtime.Delete(context.Background(), *state)
return nil, err
}
now := time.Now().UTC()
systemVolumeRecord := model.VolumeRecord{
ID: d.systemVolumeID(req.MachineID),
@ -117,7 +123,7 @@ func (d *Daemon) CreateMachine(ctx context.Context, req contracthost.CreateMachi
UserVolumeIDs: append([]contracthost.VolumeID(nil), attachedUserVolumeIDs...),
RuntimeHost: state.RuntimeHost,
TapDevice: state.TapName,
Ports: defaultMachinePorts(),
Ports: ports,
Phase: contracthost.MachinePhaseRunning,
PID: state.PID,
SocketPath: state.SocketPath,

View file

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"sync"
"time"
appconfig "github.com/getcompanion-ai/computer-host/internal/config"
"github.com/getcompanion-ai/computer-host/internal/firecracker"
@ -13,12 +14,15 @@ import (
)
const (
defaultGuestKernelArgs = "console=ttyS0 reboot=k panic=1 pci=off"
defaultGuestMemoryMiB = int64(512)
defaultGuestVCPUs = int64(1)
defaultSSHPort = uint16(2222)
defaultVNCPort = uint16(6080)
defaultCopyBufferSize = 1024 * 1024
defaultGuestKernelArgs = "console=ttyS0 reboot=k panic=1 pci=off"
defaultGuestMemoryMiB = int64(512)
defaultGuestVCPUs = int64(1)
defaultSSHPort = uint16(2222)
defaultVNCPort = uint16(6080)
defaultCopyBufferSize = 1024 * 1024
defaultGuestDialTimeout = 500 * time.Millisecond
defaultGuestReadyPollInterval = 100 * time.Millisecond
defaultGuestReadyTimeout = 30 * time.Second
)
type Runtime interface {

View file

@ -2,10 +2,14 @@ package daemon
import (
"context"
"errors"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
@ -40,8 +44,6 @@ func (f *fakeRuntime) Delete(_ context.Context, state firecracker.MachineState)
}
func TestCreateMachineStagesArtifactsAndPersistsState(t *testing.T) {
t.Parallel()
root := t.TempDir()
cfg := testConfig(root)
fileStore, err := store.NewFileStore(cfg.StatePath, cfg.OperationsPath)
@ -49,13 +51,18 @@ func TestCreateMachineStagesArtifactsAndPersistsState(t *testing.T) {
t.Fatalf("create file store: %v", err)
}
sshListener := listenTestPort(t, int(defaultSSHPort))
defer sshListener.Close()
vncListener := listenTestPort(t, int(defaultVNCPort))
defer vncListener.Close()
startedAt := time.Unix(1700000005, 0).UTC()
runtime := &fakeRuntime{
bootState: firecracker.MachineState{
ID: "vm-1",
Phase: firecracker.PhaseRunning,
PID: 4321,
RuntimeHost: "172.16.0.2",
RuntimeHost: "127.0.0.1",
SocketPath: filepath.Join(cfg.RuntimeDir, "machines", "vm-1", "root", "run", "firecracker.sock"),
TapName: "fctap0",
StartedAt: &startedAt,
@ -95,12 +102,15 @@ func TestCreateMachineStagesArtifactsAndPersistsState(t *testing.T) {
if response.Machine.Phase != contracthost.MachinePhaseRunning {
t.Fatalf("machine phase mismatch: got %q", response.Machine.Phase)
}
if response.Machine.RuntimeHost != "172.16.0.2" {
if response.Machine.RuntimeHost != "127.0.0.1" {
t.Fatalf("runtime host mismatch: got %q", response.Machine.RuntimeHost)
}
if len(response.Machine.Ports) != 2 {
t.Fatalf("machine ports mismatch: got %d want 2", len(response.Machine.Ports))
}
if response.Machine.Ports[0].Port != defaultSSHPort || response.Machine.Ports[1].Port != defaultVNCPort {
t.Fatalf("machine ports mismatch: got %#v", response.Machine.Ports)
}
if runtime.bootCalls != 1 {
t.Fatalf("boot call count mismatch: got %d want 1", runtime.bootCalls)
}
@ -209,3 +219,27 @@ func testConfig(root string) appconfig.Config {
JailerBinaryPath: "/usr/bin/jailer",
}
}
func listenTestPort(t *testing.T, port int) net.Listener {
t.Helper()
listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
if err != nil {
var bindErr *net.OpError
if errors.As(err, &bindErr) && strings.Contains(strings.ToLower(err.Error()), "address already in use") {
t.Skipf("port %d already in use", port)
}
t.Fatalf("listen on port %d: %v", port, err)
}
go func() {
for {
connection, err := listener.Accept()
if err != nil {
return
}
_ = connection.Close()
}
}()
return listener
}

View file

@ -0,0 +1,52 @@
package daemon
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"time"
contracthost "github.com/getcompanion-ai/computer-host/contract"
)
func waitForGuestReady(ctx context.Context, host string, ports []contracthost.MachinePort) error {
host = strings.TrimSpace(host)
if host == "" {
return fmt.Errorf("guest runtime host is required")
}
waitContext, cancel := context.WithTimeout(ctx, defaultGuestReadyTimeout)
defer cancel()
for _, port := range ports {
if err := waitForGuestPort(waitContext, host, port); err != nil {
return err
}
}
return nil
}
func waitForGuestPort(ctx context.Context, host string, port contracthost.MachinePort) error {
address := net.JoinHostPort(host, strconv.Itoa(int(port.Port)))
dialer := net.Dialer{Timeout: defaultGuestDialTimeout}
ticker := time.NewTicker(defaultGuestReadyPollInterval)
defer ticker.Stop()
var lastErr error
for {
connection, err := dialer.DialContext(ctx, string(port.Protocol), address)
if err == nil {
_ = connection.Close()
return nil
}
lastErr = err
select {
case <-ctx.Done():
return fmt.Errorf("wait for guest port %q on %s: %w (last_err=%v)", port.Name, address, ctx.Err(), lastErr)
case <-ticker.C:
}
}
}