Make vm create wait for the guest-side vsock /healthz endpoint instead of only waiting for the host socket path, so the wait_vsock_agent stage reflects actual guest readiness. Start banger-vsock-agent earlier in the Alpine OpenRC graph and report later /ports failures as guest-service waits rather than vsock-agent waits, which makes the progress output match what the guest is really doing. Validate with go test ./..., a rebuilt managed alpine image, and a fresh vm create --image alpine --name alp --nat that now progresses through wait_vsock_agent -> wait_guest_ready -> wait_opencode -> ready.
151 lines
3.8 KiB
Go
151 lines
3.8 KiB
Go
package opencode
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"banger/internal/vsockagent"
|
|
)
|
|
|
|
func TestServiceUnitContainsExpectedExecStart(t *testing.T) {
|
|
unit := ServiceUnit()
|
|
for _, snippet := range []string{
|
|
"RequiresMountsFor=/root",
|
|
"WorkingDirectory=/root",
|
|
"Environment=HOME=/root",
|
|
"ExecStart=/usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096",
|
|
"WantedBy=multi-user.target",
|
|
} {
|
|
if !strings.Contains(unit, snippet) {
|
|
t.Fatalf("service unit missing snippet %q\nunit:\n%s", snippet, unit)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunitRunScriptContainsExpectedExec(t *testing.T) {
|
|
script := RunitRunScript()
|
|
for _, snippet := range []string{
|
|
"export HOME=/root",
|
|
"cd /root",
|
|
"exec /usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096",
|
|
} {
|
|
if !strings.Contains(script, snippet) {
|
|
t.Fatalf("runit script missing snippet %q\nscript:\n%s", snippet, script)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadyMatchesTCPPort(t *testing.T) {
|
|
if Ready([]vsockagent.PortListener{{Proto: "udp", Port: Port}}) {
|
|
t.Fatal("udp listener should not satisfy readiness")
|
|
}
|
|
if Ready([]vsockagent.PortListener{{Proto: "tcp", Port: 8080}}) {
|
|
t.Fatal("wrong tcp port should not satisfy readiness")
|
|
}
|
|
if !Ready([]vsockagent.PortListener{{Proto: "tcp", Port: Port}}) {
|
|
t.Fatal("tcp listener on opencode port should satisfy readiness")
|
|
}
|
|
}
|
|
|
|
func TestWaitReadyReturnsWhenPortIsListening(t *testing.T) {
|
|
socketPath := filepath.Join(t.TempDir(), "opencode.vsock")
|
|
listener, err := net.Listen("unix", socketPath)
|
|
if err != nil {
|
|
skipIfSocketRestricted(t, err)
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = listener.Close()
|
|
_ = os.Remove(socketPath)
|
|
})
|
|
|
|
serverDone := make(chan error, 1)
|
|
go func() {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
serverDone <- err
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
buf := make([]byte, 512)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
serverDone <- err
|
|
return
|
|
}
|
|
if got := string(buf[:n]); got != "CONNECT 42070\n" {
|
|
serverDone <- fmt.Errorf("unexpected connect message %q", got)
|
|
return
|
|
}
|
|
if _, err := conn.Write([]byte("OK 1\n")); err != nil {
|
|
serverDone <- err
|
|
return
|
|
}
|
|
reqBuf := make([]byte, 0, 512)
|
|
for {
|
|
n, err = conn.Read(buf)
|
|
if err != nil {
|
|
serverDone <- err
|
|
return
|
|
}
|
|
reqBuf = append(reqBuf, buf[:n]...)
|
|
if strings.Contains(string(reqBuf), "\r\n\r\n") {
|
|
break
|
|
}
|
|
}
|
|
if !strings.Contains(string(reqBuf), "GET /ports HTTP/1.1\r\n") {
|
|
serverDone <- fmt.Errorf("unexpected ports payload %q", string(reqBuf))
|
|
return
|
|
}
|
|
body := []byte(`{"listeners":[{"proto":"tcp","bind_address":"0.0.0.0","port":4096}]}`)
|
|
_, err = conn.Write([]byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body)))
|
|
serverDone <- err
|
|
}()
|
|
|
|
if err := waitReady(context.Background(), nil, socketPath, time.Second, nil); err != nil {
|
|
t.Fatalf("waitReady: %v", err)
|
|
}
|
|
if err := <-serverDone; err != nil {
|
|
t.Fatalf("server: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWaitReadyReportsGuestServicesWhenPortsUnavailable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var reports []string
|
|
err := waitReady(
|
|
context.Background(),
|
|
nil,
|
|
filepath.Join(t.TempDir(), "missing.vsock"),
|
|
50*time.Millisecond,
|
|
func(stage, detail string) {
|
|
reports = append(reports, stage+":"+detail)
|
|
},
|
|
)
|
|
if err == nil {
|
|
t.Fatal("waitReady() error = nil, want timeout")
|
|
}
|
|
if len(reports) == 0 {
|
|
t.Fatal("waitReady() did not report progress")
|
|
}
|
|
if got := reports[0]; got != "wait_guest_ready:waiting for guest services" {
|
|
t.Fatalf("first report = %q, want guest services wait", got)
|
|
}
|
|
}
|
|
|
|
func skipIfSocketRestricted(t *testing.T, err error) {
|
|
t.Helper()
|
|
if err == nil {
|
|
return
|
|
}
|
|
if strings.Contains(strings.ToLower(err.Error()), "operation not permitted") {
|
|
t.Skipf("socket creation is restricted in this environment: %v", err)
|
|
}
|
|
}
|