mirror of
https://github.com/getcompanion-ai/computer-host.git
synced 2026-04-15 10:05:13 +00:00
host daemon touches (#4)
* feat: launch config tests * feat: readiness probe port alignment
This commit is contained in:
parent
e2f9e54970
commit
592df1e1df
10 changed files with 439 additions and 49 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
52
internal/daemon/readiness.go
Normal file
52
internal/daemon/readiness.go
Normal 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:
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue