From 62650a1bb80f474703de31f7e476f71dad3f2ae9 Mon Sep 17 00:00:00 2001 From: CMGS Date: Mon, 28 Sep 2026 16:42:49 +0800 Subject: [PATCH 1/2] cut(e2e): envdsmoke moves to sandbox-operator; sandbox keeps no envd knowledge The e2b flavor's envd smoke and its node script now live with the e2b layer (sandbox-operator test/envdsmoke, scripts/envd-e2e.sh), rewritten on the operator's own client; here the flavor is an image like any other, the pool warmup a shell command and the relay a byte pipe. guestserver's doc names no daemon, and the engine test's fixture image is a plain name. --- .gitignore | 1 - README.md | 2 +- docs/e2b.md | 24 +-- e2e/cmd/envdsmoke/main.go | 361 --------------------------------- e2e/cmd/envdsmoke/main_test.go | 68 ------- e2e/cmd/guestserver/main.go | 2 +- sandboxd/engine/engine_test.go | 4 +- scripts/envd-e2e.sh | 56 ----- 8 files changed, 11 insertions(+), 507 deletions(-) delete mode 100644 e2e/cmd/envdsmoke/main.go delete mode 100644 e2e/cmd/envdsmoke/main_test.go delete mode 100755 scripts/envd-e2e.sh diff --git a/.gitignore b/.gitignore index 57926287..5444b30a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ dist/ boot/init/target/ silkd/target/ e2e/demo -e2e/envdsmoke e2e/guestserver e2e/portsmoke e2e/smoke diff --git a/README.md b/README.md index 147ab72f..dd00f0fb 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ performance) — source in `smoke`, `meshsmoke`, `crossnode`, `coldproof`, `egresssmoke`, `interceptsmoke`, `sockssmoke`, `volumesmoke`, `lifecycle` (idle→hibernate→archive), `androidsmoke`, `browsersmoke`, `ringsmoke` (output ring cap, exec as a user), - `portsmoke` (the guest-port relay), `envdsmoke` (the e2b flavor's envd through it), + `portsmoke` (the guest-port relay), and the `pullbench`/`pushbench`/`rpcbench`/`qaab` perf drivers - `boot/kernel/` — kernel version pin (`VERSION` + matching tarball `SHA256`, bump both together) + config fragment (amd64: over `x86_64_defconfig` + diff --git a/docs/e2b.md b/docs/e2b.md index ddcc69ce..9e710997 100644 --- a/docs/e2b.md +++ b/docs/e2b.md @@ -130,23 +130,13 @@ window where that lock is absent. ## Proving it -`scripts/envd-e2e.sh` runs the guest half on a node: it builds the pool with the -warmup gate above and then drives `envdsmoke`, which claims a sandbox and -reaches the real `envd` through the relay — health, `GET`/`POST /files`, a -ConnectRPC unary, and a `process.Process/Start` whose output comes back over the -Connect server stream — while asserting silkd still answers on the same VM. - -```bash -K= TEMPLATE=ghcr.io/cocoonstack/sandbox/e2b-rt:24.04 bash scripts/envd-e2e.sh -K= TEMPLATE=ghcr.io/cocoonstack/sandbox/e2b-ci:24.04 SIZE=medium CODE_INTERPRETER=1 bash scripts/envd-e2e.sh -``` - -Both passes check the `user` account; `CODE_INTERPRETER=1` also gates warmup on -49999, curls its `/health` inside the guest, and runs `1+1` through the relay -the way the SDK's `runCode` does. - -`envdsmoke -hold` then keeps that sandbox alive for an out-of-tree harness that -puts a real edge proxy in front of it. +The flavor's daemon is proved from the consumer's side: sandbox-operator's +`scripts/envd-e2e.sh` builds the pool with the warmup gate above and drives +`test/envdsmoke`, which claims a sandbox and reaches the real `envd` through +the relay (health, the version the compat API reports, files, ConnectRPC, the +`user` account, HTTP/1.1 only, and the code-interpreter API with +`CODE_INTERPRETER=1`). sandboxd itself knows nothing of `envd`: the pool's +warmup is a shell command like any other, and the relay carries bytes. The suite does **not** cover the guard's drop. Reaching 49983 from off the guest means lifting sandboxd's tap lock, which no e2e run should do. Measured diff --git a/e2e/cmd/envdsmoke/main.go b/e2e/cmd/envdsmoke/main.go deleted file mode 100644 index de925713..00000000 --- a/e2e/cmd/envdsmoke/main.go +++ /dev/null @@ -1,361 +0,0 @@ -// envdsmoke proves the e2b flavor on real hardware: it claims a sandbox from an -// envd-carrying pool and drives the real envd through sandboxd's guest-port -// relay, the path an edge proxy takes. The guest has no NIC, so the relay is -// the only way in and nothing here can be reached by accident. -// -// It asserts what the e2b SDK depends on — the daemon is up, its version is the -// one the compat API must report, files and the ConnectRPC surface answer — and -// reports which HTTP version envd actually serves, because the proxy in front -// of it must match rather than assume. -package main - -import ( - "context" - "encoding/base64" - "encoding/binary" - "encoding/json/jsontext" - "encoding/json/v2" - "flag" - "fmt" - "io" - "net" - "net/http" - "os" - "slices" - "strings" - "time" - - "github.com/cocoonstack/sandbox/e2e/internal/harness" - sandbox "github.com/cocoonstack/sandbox/sdk/go" -) - -const ( - // envdPort is envd's fixed listener; the SDK derives its host from it. - envdPort = 49983 - // interpreterPort is the e2b code-interpreter API the e2b-ci flavor starts at boot. - interpreterPort = 49999 - readyWait = 60 * time.Second - // claimTTL outlasts the run so a lease expiry cannot look like a relay fault. - claimTTL = 30 * time.Minute - - uploadPath = "/tmp/envdsmoke-probe.txt" - uploadProbe = "envd-wrote-this" - - processProbe = "envd-process-ok" - - contentType = "Content-Type" -) - -func main() { - addr := flag.String("addr", "127.0.0.1:7777", "sandboxd address") - token := flag.String("token", "", "node api token") - template := flag.String("template", "", "e2b flavor template ref") - wantVersion := flag.String("envd-version", "", "version envd must report; empty only prints it") - hold := flag.Duration("hold", 0, "after the steps pass, print the claim and keep it alive for this long") - interpreter := flag.Bool("code-interpreter", false, "also drive the code-interpreter API on 49999 (the e2b-ci flavor)") - size := flag.String("size", "small", "size of the pool to claim from") - flag.Parse() - - if err := run(*addr, *token, *template, *wantVersion, *hold, *interpreter, sandbox.Size(*size)); err != nil { - fmt.Fprintln(os.Stderr, "envdsmoke:", err) - os.Exit(1) - } - fmt.Println("ENVDSMOKE PASS") -} - -func run(addr, token, template, wantVersion string, hold time.Duration, interpreter bool, size sandbox.Size) error { - ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second+hold) - defer cancel() - - _, sb, err := harness.Claim(ctx, addr, token, template, - sandbox.WithNetwork(sandbox.NetNone), sandbox.WithSize(size), sandbox.WithTimeout(claimTTL)) - if err != nil { - return err - } - defer func() { _ = sb.Close() }() - fmt.Printf("claimed %s on %s\n", sb.ID, sb.Owner()) - - if err := reportGuest(ctx, sb, wantVersion); err != nil { - return err - } - rt := &harness.PortRelay{Owner: sb.Owner(), ID: sb.ID, Token: sb.Token()} - if err := waitReady(ctx, rt); err != nil { - return err - } - - steps := []smokeStep{ - {"GET /health", stepHealth}, - {"GET /files", stepDownload}, - {"POST /files", stepUpload}, - {"connect unary over http/1.1", stepConnectH1}, - {"process start under -no-cgroups", stepProcessStart}, - {"envd serves http/1.1 only", stepProtocol}, - {"user account with sudo", stepUserAccount}, - } - if interpreter { - steps = append(steps, smokeStep{"interpreter runs 1+1", stepRunCode}) - } - for _, step := range steps { - t0 := time.Now() - if err := step.run(ctx, rt, sb); err != nil { - return fmt.Errorf("%s: %w", step.name, err) - } - fmt.Printf(" ok %-30s %5.1fms\n", step.name, float64(time.Since(t0).Microseconds())/1000) - } - if hold > 0 { - fmt.Printf("SANDBOX %s %s %s %d\n", sb.ID, sb.Token(), sb.Owner(), envdPort) - select { - case <-time.After(hold): - case <-ctx.Done(): - } - } - return nil -} - -type smokeStep struct { - name string - run func(context.Context, *harness.PortRelay, *sandbox.Sandbox) error -} - -// reportGuest asks the guest what it runs over silkd, the native data plane that must keep working beside envd. -func reportGuest(ctx context.Context, sb *sandbox.Sandbox, wantVersion string) error { - version, err := sb.Exec(ctx, "/usr/local/bin/envd", "-version") - if err != nil { - return fmt.Errorf("envd -version: %w", err) - } - version = strings.TrimSpace(version) - state, _ := sb.Exec(ctx, "systemctl", "is-active", "envd.service") - fmt.Printf(" envd %s (%s), silkd relay answering\n", version, strings.TrimSpace(state)) - if strings.TrimSpace(state) != "active" { - out, _ := sb.Exec(ctx, "sh", "-c", "systemctl status envd.service --no-pager 2>&1 | tail -20") - return fmt.Errorf("envd.service is %q:\n%s", strings.TrimSpace(state), out) - } - if wantVersion != "" && version != wantVersion { - return fmt.Errorf("envd reports %q, want %q — the compat API's envdVersion would be a lie", version, wantVersion) - } - return nil -} - -func waitReady(ctx context.Context, rt *harness.PortRelay) error { - deadline := time.Now().Add(readyWait) - var last error - for time.Now().Before(deadline) { - resp, err := rt.Do(ctx, envdPort, http.MethodGet, "/health", nil, nil) - if err == nil { - _ = resp.Body.Close() - return nil - } - last = err - time.Sleep(500 * time.Millisecond) - } - return fmt.Errorf("envd never answered /health: %w", last) -} - -func stepHealth(ctx context.Context, rt *harness.PortRelay, _ *sandbox.Sandbox) error { - resp, err := rt.Do(ctx, envdPort, http.MethodGet, "/health", nil, nil) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusNoContent { - return fmt.Errorf("status %s, want 204", resp.Status) - } - return nil -} - -// stepDownload takes the path the SDK's files.read takes. -func stepDownload(ctx context.Context, rt *harness.PortRelay, _ *sandbox.Sandbox) error { - resp, err := rt.Do(ctx, envdPort, http.MethodGet, "/files?path=/etc/envd-version&username=root", nil, nil) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("status %s: %s", resp.Status, body) - } - if strings.TrimSpace(string(body)) == "" { - return fmt.Errorf("empty body for /etc/envd-version") - } - return nil -} - -// stepUpload writes through envd and reads back through silkd, so the two daemons share one filesystem. -func stepUpload(ctx context.Context, rt *harness.PortRelay, sb *sandbox.Sandbox) error { - const boundary = "envdsmoke" - body := fmt.Sprintf("--%s\r\nContent-Disposition: form-data; name=\"file\"; filename=\"probe.txt\"\r\n"+ - "Content-Type: text/plain\r\n\r\n%s\r\n--%s--\r\n", boundary, uploadProbe, boundary) - headers := http.Header{contentType: []string{"multipart/form-data; boundary=" + boundary}} - resp, err := rt.Do(ctx, envdPort, http.MethodPost, "/files?path="+uploadPath+"&username=root", headers, strings.NewReader(body)) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - out, _ := io.ReadAll(resp.Body) - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - return fmt.Errorf("status %s: %s", resp.Status, out) - } - seen, err := sb.ReadFile(ctx, uploadPath) - if err != nil { - return fmt.Errorf("silkd read back: %w", err) - } - if strings.TrimSpace(string(seen)) != uploadProbe { - return fmt.Errorf("silkd sees %q at %s, envd wrote %q", seen, uploadPath, uploadProbe) - } - return nil -} - -// stepConnectH1 drives one unary call the way connect-web does; the SDK's services are all Connect. -func stepConnectH1(ctx context.Context, rt *harness.PortRelay, _ *sandbox.Sandbox) error { - headers := http.Header{ - contentType: []string{"application/json"}, - "Connect-Protocol-Version": []string{"1"}, - "X-User": []string{"root"}, - } - payload := `{"path":"/etc/envd-version"}` - resp, err := rt.Do(ctx, envdPort, http.MethodPost, "/filesystem.Filesystem/Stat", headers, strings.NewReader(payload)) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("status %s: %s", resp.Status, body) - } - var out map[string]any - if err := json.Unmarshal(body, &out); err != nil { - return fmt.Errorf("decode %q: %w", body, err) - } - if _, ok := out["entry"]; !ok { - return fmt.Errorf("stat reply carries no entry: %s", body) - } - return nil -} - -// stepProcessStart spawns through envd's process service, which the image runs with -no-cgroups, and reads its Connect server stream back. -func stepProcessStart(ctx context.Context, rt *harness.PortRelay, _ *sandbox.Sandbox) error { - headers := http.Header{ - contentType: []string{"application/connect+json"}, - "Connect-Protocol-Version": []string{"1"}, - "X-User": []string{"root"}, - } - req := fmt.Sprintf(`{"process":{"cmd":"/bin/echo","args":[%q]}}`, processProbe) - resp, err := rt.Do(ctx, envdPort, http.MethodPost, "/process.Process/Start", headers, strings.NewReader(envelope(req))) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("status %s: %s", resp.Status, body) - } - events, err := readEnvelopes(resp.Body) - if err != nil { - return err - } - joined := strings.Join(events, "\n") - if !strings.Contains(joined, `"start"`) { - return fmt.Errorf("no start event in the stream:\n%s", joined) - } - if !strings.Contains(joined, base64.StdEncoding.EncodeToString([]byte(processProbe+"\n"))) { - return fmt.Errorf("the command's stdout never arrived:\n%s", joined) - } - return nil -} - -// envelope frames one Connect streaming message: a flag byte, then a big-endian length. -func envelope(payload string) string { - head := make([]byte, 5) - binary.BigEndian.PutUint32(head[1:], uint32(len(payload))) //nolint:gosec // a probe payload is tens of bytes - return string(head) + payload -} - -// readEnvelopes unframes a Connect server stream, whose trailing envelope carries the RPC's error; the HTTP status is 200 either way. -func readEnvelopes(r io.Reader) ([]string, error) { - var out []string - head := make([]byte, 5) - for { - if _, err := io.ReadFull(r, head); err != nil { - return out, fmt.Errorf("stream ended before its end-of-stream envelope: %w", err) - } - body := make([]byte, binary.BigEndian.Uint32(head[1:])) - if _, err := io.ReadFull(r, body); err != nil { - return out, err - } - if head[0]&0x02 == 0 { - out = append(out, string(body)) - continue - } - var end struct { - Error jsontext.Value `json:"error"` - } - if err := json.Unmarshal(body, &end); err != nil { - return out, fmt.Errorf("parse end-of-stream envelope %q: %w", body, err) - } - if len(end.Error) > 0 { - return out, fmt.Errorf("rpc failed: %s", end.Error) - } - return out, nil - } -} - -// stepProtocol fails the day envd gains h2c, which is when a proxy may forward HTTP/2 upstream. -func stepProtocol(ctx context.Context, rt *harness.PortRelay, sb *sandbox.Sandbox) error { - var protocols http.Protocols - protocols.SetUnencryptedHTTP2(true) - client := &http.Client{Transport: &http.Transport{ - Protocols: &protocols, - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return rt.Dial(ctx, envdPort) }, - }} - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://guest/health", nil) - if err != nil { - return err - } - resp, err := client.Do(req) - if err == nil { - defer func() { _ = resp.Body.Close() }() - return fmt.Errorf("envd answered h2c with %s", resp.Status) - } - // an error here must mean h2c specifically, not a guest that stopped answering at all - return stepHealth(ctx, rt, sb) -} - -// stepUserAccount checks the SDK's default user: its own home and passwordless sudo. -func stepUserAccount(ctx context.Context, _ *harness.PortRelay, sb *sandbox.Sandbox) error { - out, err := sb.Exec(ctx, "sudo", "-u", "user", "sh", "-c", `id -un; echo "$HOME"; sudo -n true && echo sudo`) - if err != nil { - return err - } - if got := strings.Fields(out); !slices.Equal(got, []string{"user", "/home/user", "sudo"}) { - return fmt.Errorf("sudo -u user reports %q, want user, /home/user and passwordless sudo", out) - } - return nil -} - -// stepRunCode posts what the SDK's runCode posts and reads the result line out of the NDJSON stream. -func stepRunCode(ctx context.Context, rt *harness.PortRelay, _ *sandbox.Sandbox) error { - headers := http.Header{contentType: []string{"application/json"}} - resp, err := rt.Do(ctx, interpreterPort, http.MethodPost, "/execute", headers, strings.NewReader(`{"code":"1+1"}`)) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - out, _ := io.ReadAll(resp.Body) - return fmt.Errorf("status %s: %s", resp.Status, out) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - for line := range strings.Lines(string(body)) { - var out struct { - Type string `json:"type"` - Text string `json:"text"` - } - if json.Unmarshal([]byte(line), &out) == nil && out.Type == "result" && out.Text == "2" { - return nil - } - } - return fmt.Errorf("no result line with text 2 in:\n%s", body) -} diff --git a/e2e/cmd/envdsmoke/main_test.go b/e2e/cmd/envdsmoke/main_test.go deleted file mode 100644 index 4085379e..00000000 --- a/e2e/cmd/envdsmoke/main_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "encoding/binary" - "io" - "strings" - "testing" -) - -func TestReadEnvelopesReturnsTheDataMessages(t *testing.T) { - got, err := readEnvelopes(strings.NewReader(frame(0, `{"event":{"start":{}}}`) + frame(0, `{"event":{"data":{}}}`) + frame(2, `{}`))) - if err != nil { - t.Fatalf("readEnvelopes: %v", err) - } - want := []string{`{"event":{"start":{}}}`, `{"event":{"data":{}}}`} - if len(got) != len(want) { - t.Fatalf("got %d messages %q, want %d", len(got), got, len(want)) - } - for i := range want { - if got[i] != want[i] { - t.Errorf("message %d = %q, want %q", i, got[i], want[i]) - } - } -} - -func TestReadEnvelopesRejects(t *testing.T) { - tests := []struct { - name string - stream string - want string - }{ - {"an error in the end-of-stream envelope", frame(0, `{"event":{"start":{}}}`) + frame(2, `{"error":{"code":"internal","message":"exec failed"}}`), "rpc failed"}, - {"a stream that ends before its end-of-stream envelope", frame(0, `{"event":{"start":{}}}`), "stream ended before"}, - {"an empty stream", "", "stream ended before"}, - {"an unparsable end-of-stream envelope", frame(2, `not json`), "parse end-of-stream envelope"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := readEnvelopes(strings.NewReader(tt.stream)) - if err == nil { - t.Fatalf("readEnvelopes accepted %s", tt.name) - } - if !strings.Contains(err.Error(), tt.want) { - t.Errorf("error = %q, want it to mention %q", err, tt.want) - } - }) - } -} - -func TestReadEnvelopesRejectsATruncatedBody(t *testing.T) { - full := frame(0, `{"event":{"start":{}}}`) - if _, err := readEnvelopes(strings.NewReader(full[:len(full)-3])); err == nil { - t.Fatal("readEnvelopes accepted a truncated message body") - } -} - -func TestReadEnvelopesRejectsAShortHeader(t *testing.T) { - if _, err := readEnvelopes(io.LimitReader(strings.NewReader(frame(0, `{}`)), 3)); err == nil { - t.Fatal("readEnvelopes accepted a truncated header") - } -} - -func frame(flag byte, payload string) string { - head := make([]byte, 5) - head[0] = flag - binary.BigEndian.PutUint32(head[1:], uint32(len(payload))) - return string(head) + payload -} diff --git a/e2e/cmd/guestserver/main.go b/e2e/cmd/guestserver/main.go index 0843cfc4..23240530 100644 --- a/e2e/cmd/guestserver/main.go +++ b/e2e/cmd/guestserver/main.go @@ -1,6 +1,6 @@ // guestserver is the in-guest listener the port-relay e2e drives: a loopback // HTTP server that reports the protocol it was reached over. It stands in for -// e2b's envd during hardware tests — the relay carries bytes, so proving +// any guest daemon during hardware tests — the relay carries bytes, so proving // HTTP/1.1 and h2c both survive it needs a server that speaks both. // // It is uploaded into a claimed sandbox by portsmoke; nothing in the stock diff --git a/sandboxd/engine/engine_test.go b/sandboxd/engine/engine_test.go index f69fc6c3..1a0a7715 100644 --- a/sandboxd/engine/engine_test.go +++ b/sandboxd/engine/engine_test.go @@ -146,9 +146,9 @@ func TestListPassesNoPositionalArgsToCocoon(t *testing.T) { func TestImageIDsMapsNameToID(t *testing.T) { fakeCocoon(t, "#!/bin/sh\n[ \"$*\" = \"image list --format json\" ] || { echo \"unexpected args $*\" >&2; exit 1; }\n"+ - "echo '[{\"id\":\"sha256:afe6\",\"name\":\"ghcr.io/cocoonstack/sandbox/e2b-rt:24.04\",\"type\":\"oci\",\"size\":1}]'\n") + "echo '[{\"id\":\"sha256:afe6\",\"name\":\"ghcr.io/cocoonstack/sandbox/rt:24.04\",\"type\":\"oci\",\"size\":1}]'\n") ids, err := New("cocoon", nil, nil, false, false, "").ImageIDs(t.Context()) - if err != nil || len(ids) != 1 || ids["ghcr.io/cocoonstack/sandbox/e2b-rt:24.04"] != "sha256:afe6" { + if err != nil || len(ids) != 1 || ids["ghcr.io/cocoonstack/sandbox/rt:24.04"] != "sha256:afe6" { t.Fatalf("ImageIDs = %v, %v; want the one image cocoon printed", ids, err) } } diff --git a/scripts/envd-e2e.sh b/scripts/envd-e2e.sh deleted file mode 100755 index 38fd604d..00000000 --- a/scripts/envd-e2e.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# Bare-metal proof for the e2b image flavor: envd inside a cocoon microVM, -# reached only through sandboxd's guest-port relay. The pool's warmup gates a -# warm clone on envd answering, so a claim never hands out a sandbox whose data -# plane is still starting. -# Run as root on a node with cocoon; K names a kit holding bin/sandboxd, -# bin/envdsmoke, bin/jq and a cocoon on PATH. -set -uo pipefail -K=${K:?kit dir} -ADDR=${ADDR:-127.0.0.1:7996} -TOKEN=${TOKEN:-e2benvd} -TEMPLATE=${TEMPLATE:-e2b-rt:24.04} -ENVD_VERSION=${ENVD_VERSION:-0.8.0} -# CODE_INTERPRETER=1 is the e2b-ci pass: warmup also gates on the interpreter API, and envdsmoke drives it. -CODE_INTERPRETER=${CODE_INTERPRETER:-} -SIZE=${SIZE:-small} -HEALTH="curl -sf -m 1 -o /dev/null http://127.0.0.1:49983/health" -[[ -n $CODE_INTERPRETER ]] && HEALTH="$HEALTH && curl -sf -m 1 -o /dev/null http://127.0.0.1:49999/health" -export PATH=$K/bin:$PATH -DATA=$(mktemp -d /tmp/envd-e2e.XXXXXX); DAEMON_PID="" -cleanup() { - status=$? - echo "== daemon log tail"; tail -25 "$DATA/daemon.log" 2>/dev/null - [[ -n $DAEMON_PID ]] && kill "$DAEMON_PID" 2>/dev/null - wait 2>/dev/null - cocoon vm list --format json 2>/dev/null | - jq -r '.[] | select(.config.name | startswith("sbx-")) | .config.name' | - while read -r vm; do - cocoon vm stop --force "$vm" >/dev/null 2>&1 - cocoon vm rm --force "$vm" >/dev/null 2>&1 - done - rm -rf "$DATA"; exit "$status" -} -trap cleanup EXIT -# warmup gates a warm clone on envd answering: a clone that hands out a sandbox -# whose data plane is not up yet is worse than a slower clone. -cat >"$DATA/config.json" </dev/null)" -"$K/bin/sandboxd" -config "$DATA/config.json" >>"$DATA/daemon.log" 2>&1 & -DAEMON_PID=$! -for _ in $(seq 1 40); do curl -sf "http://$ADDR/healthz" >/dev/null && break; sleep 0.5; done -curl -sf "http://$ADDR/healthz" >/dev/null || { echo "daemon never came up"; exit 1; } -echo "== wait for golden + warm (warmup gates on envd /health)" -for i in $(seq 1 300); do - curl -sf -H "Authorization: Bearer $TOKEN" "http://$ADDR/v1/info" | - jq -e '(.pools|length)>0 and all(.pools[]; .golden and .warm >= .target)' >/dev/null 2>&1 && break - [[ $i == 300 ]] && { echo "pools never became ready"; curl -sf -H "Authorization: Bearer $TOKEN" "http://$ADDR/v1/info" | jq .; exit 1; } - sleep 1 -done -curl -sf -H "Authorization: Bearer $TOKEN" "http://$ADDR/v1/info" | jq -c '.pools' -echo "== envdsmoke" -"$K/bin/envdsmoke" -addr "$ADDR" -token "$TOKEN" -template "$TEMPLATE" -envd-version "$ENVD_VERSION" -size "$SIZE" ${HOLD:+-hold "$HOLD"} ${CODE_INTERPRETER:+-code-interpreter} From 2a718b92ad0a45bbbe0f2538a18dfc23c9628c6e Mon Sep 17 00:00:00 2001 From: CMGS Date: Mon, 28 Sep 2026 16:43:10 +0800 Subject: [PATCH 2/2] cut(e2e): portsmoke's comments name no guest daemon --- e2e/cmd/portsmoke/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/cmd/portsmoke/main.go b/e2e/cmd/portsmoke/main.go index b81c529d..0f4ae6a3 100644 --- a/e2e/cmd/portsmoke/main.go +++ b/e2e/cmd/portsmoke/main.go @@ -22,7 +22,7 @@ import ( ) const ( - // guestPort is envd's; the relay is port-agnostic either way. + // guestPort is the port an edge proxy relays to; the relay is port-agnostic either way. guestPort = 49983 // halfClosePort greets and shuts its write side, then keeps reading. halfClosePort = 49984 @@ -93,7 +93,7 @@ func run(addr, token, template, listener string, hold time.Duration) error { fmt.Printf(" ok %-24s %5.1fs\n", step.name, time.Since(t0).Seconds()) } if hold > 0 { - // the listener is live now, for the operator's envd-proxy smoke to drive + // the listener is live now, for the operator's edge smoke to drive fmt.Printf("SANDBOX %s %s %s %d\n", sb.ID, sb.Token(), sb.Owner(), guestPort) select { case <-time.After(hold): @@ -103,7 +103,7 @@ func run(addr, token, template, listener string, hold time.Duration) error { return nil } -// startGuestListener ships an envd stand-in in: the stock image has no python, netcat or HTTP server. +// startGuestListener ships a guest listener in: the stock image has no python, netcat or HTTP server. func startGuestListener(ctx context.Context, sb *sandbox.Sandbox, listener string) error { bin, err := os.ReadFile(listener) //nolint:gosec // the path is an operator-supplied flag on a test harness if err != nil {