Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .github/workflows/images.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# The image chain's single entry point: one push runs boot and silkd (only
# the changed ones) and then os-images, in order, so the build is
# deterministic. Previously build-os-images was triggered independently by
# each carrier's workflow_run and GitHub's concurrency queue kept only the
# newest pending run — which carrier actually built was a race, and a run
# could bake a stale carrier. Here os-images runs once, after both carriers,
# and bases FROM their sha-pinned refs.
# The image chain's single entry point: one push builds the changed carriers
# (boot, silkd), then os-images once, FROM their sha-pinned refs.
name: images

on:
Expand Down
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
bin/
dist/
dist-extra/
boot/init/target/
silkd/target/
e2e/demo
e2e/guestserver
e2e/portsmoke
e2e/pullbench
e2e/rpcbench
e2e/smoke
*.log
.DS_Store
Expand All @@ -16,6 +19,3 @@ __pycache__/
*.pyc
*.egg-info/
.pytest_cache/
e2e/pullbench
e2e/rpcbench
dist-extra/
2 changes: 1 addition & 1 deletion boot/init/src/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ fn scan_serials(ids: &[&str], found: &mut [Option<String>]) {
}
}

// cocoon's Firecracker lane names the device node itself; there is no virtio serial to match
/// cocoon's Firecracker lane names the device node itself; there is no virtio serial to match.
fn record_devices(ids: &[&str], found: &mut [Option<String>]) {
for (slot, id) in found.iter_mut().zip(ids) {
if slot.is_none() && id.starts_with("/dev/") && Path::new(id).exists() {
Expand Down
6 changes: 3 additions & 3 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -1181,9 +1181,9 @@ reap → restart reconcile):
TEMPLATE=rt:24.04 scripts/sandboxd-e2e.sh
```

Set `BRIDGE=<dev>` to include the egress lane: it adds an egress pool and the
lane-detect check. A plain `ip link add br0 type bridge` with no uplink is
enough, since the check needs a NIC, not a network. On a node without a Go
Set `BRIDGE=<dev>` to include the egress lane: it adds an egress pool and a
smoke step that claims from it. A plain `ip link add br0 type bridge` with no
uplink is enough, since the step needs a NIC, not a network. On a node without a Go
toolchain, point `SANDBOXD_BIN`, `DEMO_BIN`, and `SMOKE_BIN` at prebuilt
binaries.

Expand Down
2 changes: 2 additions & 0 deletions docs/sandboxd-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ capacity, with the pool state unharmed. The causes:
([`POST /v1/cordon`](#post-v1cordon))
- the disk is under `disk_reserve`
- memory is over `memory_limit`
- the node is [fenced](cluster.md#running-claims-across-node-loss) and the
claim's policy pool has `replicate_seconds`

On a cluster, a claim without `volumes`, `no_redirect` or `require_promoted`
that meets one of these redirects to a peer with warm capacity or, failing
Expand Down
6 changes: 3 additions & 3 deletions docs/sdk-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def main() -> None:
with client.new("node:24.04", size="medium", ttl_seconds=600) as sb:
print(f"claimed {sb.id} on {sb.owner}")

sb.push("/work", project_tar()) # only ingestion path on the no-network lane
sb.push("/work", project_tar()) # needs no egress policy
try:
print(sb.exec("sh", "-c", "cd /work && make build 2>&1"), end="")
except ExitError as e:
Expand Down Expand Up @@ -477,8 +477,8 @@ sb.push("/work", tar_bytes) # extract a tar stream under /work (atomic)
tar = sb.pull("/work") # /work back as tar bytes
```

`push` is atomic against a truncated stream and the only project-ingestion
path on the no-network lane.
`push` is atomic against a truncated stream and needs no egress policy, so it
brings a project into a no-network sandbox whose pool allows no git host.

## Search

Expand Down
5 changes: 3 additions & 2 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func main() {
defer sb.Close()
fmt.Printf("claimed %s on %s\n", sb.ID, sb.Owner())

// Push is the only ingestion path on the no-network lane.
// Push needs no egress policy, so it works on the no-network lane.
if err := sb.Push(ctx, "/work", projectTar()); err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -607,7 +607,8 @@ err = sb.Push(ctx, "/work", tarReader) // extract a tar stream under /work
err = sb.Pull(ctx, "/work", tarWriter) // stream /work back as a tar
```

`Push` is the only project-ingestion path on the no-network lane.
`Push` needs no egress policy, so it brings a project into a no-network
sandbox whose pool allows no git host.

## Search

Expand Down
15 changes: 7 additions & 8 deletions e2e/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ module github.com/cocoonstack/sandbox/e2e

go 1.27.1

require (
github.com/cocoonstack/sandbox/protocol/wire v0.1.24
github.com/cocoonstack/sandbox/sandboxd v0.0.0
github.com/cocoonstack/sandbox/sdk/go v0.0.0
)

require (
github.com/cockroachdb/errors v1.14.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
Expand Down Expand Up @@ -34,14 +40,7 @@ require (
)

replace (
github.com/cocoonstack/sandbox/protocol/wire => ../protocol/wire
github.com/cocoonstack/sandbox/sandboxd => ../sandboxd
github.com/cocoonstack/sandbox/sdk/go => ../sdk/go
)

require (
github.com/cocoonstack/sandbox/protocol/wire v0.1.24
github.com/cocoonstack/sandbox/sandboxd v0.0.0
github.com/cocoonstack/sandbox/sdk/go v0.0.0
)

replace github.com/cocoonstack/sandbox/protocol/wire => ../protocol/wire
2 changes: 1 addition & 1 deletion mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func (s *server) checkpointArg(raw jsontext.Value) (*sandbox.Checkpoint, error)
return ckpt, nil
}
if args.CheckpointID == "" {
return nil, fmt.Errorf("checkpoint_id is required")
return nil, errors.New("checkpoint_id is required")
}
return s.client.Checkpoint(args.CheckpointID), nil
}
Expand Down
2 changes: 1 addition & 1 deletion os-image/android/silkd.rc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ service silkd /system/bin/silkd
group root
setenv SILKD_PORT 2048
setenv SHELL /system/bin/sh
# Same relay URLs as the base:24.04 unit; forwarded into execs only on the no-NIC lane.
# Same relay URLs as the base:24.04 unit; forwarded into execs only where nothing routes directly.
setenv http_proxy http://127.0.0.1:3128
setenv https_proxy http://127.0.0.1:3128
setenv no_proxy localhost,127.0.0.1,::1
Expand Down
4 changes: 2 additions & 2 deletions os-image/base/install-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Bake cocoon-agent (vsock exec daemon) into a sandbox image.
# Mirrors cocoon/os-image/ubuntu/install-agent.sh with two deltas:
# - the unit starts at sysinit (DefaultDependencies=no): the agent listening
# on vsock IS the sandbox readiness signal, so it must not wait for
# on vsock is cocoon's VM readiness signal, so it must not wait for
# network or multi-user; the sandbox kernel builds vsock in (no modprobe).
# - sshd stays socket-activated (Dockerfile enables ssh.socket), so there is
# no `systemctl enable ssh` here.
Expand Down Expand Up @@ -35,7 +35,7 @@ cat > /etc/systemd/system/cocoon-agent.service <<'EOF'
[Unit]
Description=Cocoon agent (vsock command exec)
Documentation=https://github.com/cocoonstack/cocoon-agent
# Readiness signal for the whole sandbox: start as early as systemd allows.
# cocoon's VM readiness signal: start as early as systemd allows.
# No After= at all: sandbox-init hands over a fully-assembled rootfs before
# PID 1 runs and /dev/vsock is devtmpfs — measured, waiting for
# local-fs.target cost ~140ms of pure ordering delay on the critical chain.
Expand Down
4 changes: 2 additions & 2 deletions sandboxd/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

func runCA(args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: sandboxd ca {init|issue-intermediate}")
return errors.New("usage: sandboxd ca {init|issue-intermediate}")
}
switch args[0] {
case "init":
Expand Down Expand Up @@ -54,7 +54,7 @@ func caIssueIntermediate(args []string) error {
return err
}
if *rootCert == "" || *rootKey == "" || *node == "" {
return fmt.Errorf("-root-cert, -root-key, and -node are required")
return errors.New("-root-cert, -root-key, and -node are required")
}
rc, err := os.ReadFile(*rootCert)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions sandboxd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ func (s *PoolSpec) ValidateLimits() error {
return validateArchiveWindow(s.IdleHibernateSeconds, s.ArchiveAfterSeconds, s.ArchiveDeleteAfterSeconds)
}

type ConfigField struct {
type Field struct {
Name string
Value reflect.Value
}

// ConfigOwned lists s's config-only fields; PUT /v1/pools refuses them, the seed hash drops them, and reload diffs them.
func (s *PoolSpec) ConfigOwned() []ConfigField {
return []ConfigField{
func (s *PoolSpec) ConfigOwned() []Field {
return []Field{
{"egress", reflect.ValueOf(&s.Egress).Elem()},
{"warmup", reflect.ValueOf(&s.Warmup).Elem()},
{"capture_trim", reflect.ValueOf(&s.CaptureTrim).Elem()},
Expand Down
1 change: 1 addition & 0 deletions sandboxd/egress/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ func testCA(tb testing.TB) (*CA, []byte) {
}

func testCAOnly(tb testing.TB) *CA {
tb.Helper()
ca, _ := testCA(tb)
return ca
}
2 changes: 1 addition & 1 deletion sandboxd/outbound/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ func (h *Host) DropTunnels(ids []string) {
var proxies []*egress.Proxy
h.mu.Lock()
for _, id := range ids {
if d := h.doors[id]; d != nil && d.proxy != nil {
if d := h.doors[id]; d != nil {
proxies = append(proxies, d.proxy)
}
}
Expand Down
4 changes: 2 additions & 2 deletions sandboxd/pool/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ func TestIdleOnceSkipsArchived(t *testing.T) {

sweepIdle(t, m)
if got := eng.hibernateCount(); got != hibBefore {
t.Errorf("idle sweep hibernated an archived claim: %d→%d", hibBefore, got)
t.Errorf("idle sweep hibernated an archived claim: %d->%d", hibBefore, got)
}
if archivedCount(m) != 1 {
t.Error("idle sweep disturbed the archived claim")
Expand Down Expand Up @@ -839,7 +839,7 @@ func TestArchiveOnceSkipsInFlight(t *testing.T) {
m.archiveOnce(t.Context())
waitFor(t, func() bool { return !m.sweeps.archive.Load() })
if got := eng.exportCount(); got != exportsBefore {
t.Errorf("archiveOnce double-exported an in-flight sandbox: %d→%d", exportsBefore, got)
t.Errorf("archiveOnce double-exported an in-flight sandbox: %d->%d", exportsBefore, got)
}
if archivedCount(m) != 0 {
t.Error("archiveOnce archived a sandbox already being archived elsewhere")
Expand Down
14 changes: 0 additions & 14 deletions sandboxd/pool/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,20 +379,6 @@ func TestReconcileAdoptsLiveClaimsAndDestroysTheRest(t *testing.T) {
}
}

func TestRefillTopsUpToTarget(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
eng := newFakeEngine()
m := newTestManager(t, eng, config.PoolSpec{PoolKey: testKey, Warm: 2})
m.pools[testKey].goldenDir = "/goldens/x"

m.refillOnce(t.Context())
waitWarm(t, m, 2)
if n := eng.cloneCount(); n != 2 {
t.Errorf("clones=%d, want 2", n)
}
})
}

func TestSetPoolsGrowShrinkAndDrain(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
eng := newFakeEngine()
Expand Down
6 changes: 1 addition & 5 deletions sandboxd/tunnet/gro.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import (
"encoding/binary"
)

const (
tcpAck = 0x10

ipTTL = 8
)
const ipTTL = 8

type tcpSegment struct {
seq uint32
Expand Down
3 changes: 3 additions & 0 deletions sandboxd/tunnet/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const (
tcpFin = 0x01
tcpSyn = 0x02
tcpRst = 0x04
tcpPsh = 0x08
tcpAck = 0x10
tcpCwr = 0x80
)

var (
Expand Down
9 changes: 8 additions & 1 deletion sandboxd/tunnet/pg/pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,14 @@ func (s *Store) Confirm(ctx context.Context, held map[string]netip.Addr) ([]stri
if err != nil {
return nil, down(err)
}
lost := slices.DeleteFunc(claims, func(c string) bool { return slices.Contains(confirmed, c) })
done := make(map[string]struct{}, len(confirmed))
for _, c := range confirmed {
done[c] = struct{}{}
}
lost := slices.DeleteFunc(claims, func(c string) bool {
_, ok := done[c]
return ok
})
return lost, nil
}

Expand Down
3 changes: 0 additions & 3 deletions sandboxd/tunnet/segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ const (
gsoTCPv4 = 1
gsoUDPL4 = 5
gsoECN = 0x80

tcpPsh = 0x08
tcpCwr = 0x80
)

// appendSegments appends frame's packets to out behind hdr, splitting a GSO frame and finishing a partial checksum; it returns out, the length of every datagram but the last, and the count, zero for a dropped frame.
Expand Down
1 change: 1 addition & 0 deletions sandboxd/types/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func TestValidateSizes(t *testing.T) {
{SizeMedium4G, true},
{SizeLarge, true},
{SizeXLarge, true},
{Size2XLarge, true},
{Size("huge"), false},
{Size(""), false},
} {
Expand Down
10 changes: 4 additions & 6 deletions scripts/sandboxd-e2e.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Bare-metal e2e for sandboxd v0: warm pool, claim tiers, reap, reconcile.
# Bare-metal e2e for sandboxd: warm pool, claim tiers, reap, reconcile.
# Needs a silkd-baked template (os-image/) and a cocoon root no other sandboxd serves; cleanup removes every sbx-* VM.
set -euo pipefail

Expand Down Expand Up @@ -109,10 +109,8 @@ else
fi
fi

# BRIDGE (optional) attaches the node to an egress bridge, adds a small
# egress pool, and turns on the smoke's egress-lane check; lane detection
# needs a NIC, not connectivity, so a plain `ip link add ... type bridge`
# with no uplink is enough.
# BRIDGE (optional) adds a small egress pool and the smoke's egress-lane step;
# a bridge with no uplink is enough.
BRIDGE_LINE=""
EGRESS_POOL=""
if [[ -n ${BRIDGE:-} ]]; then
Expand Down Expand Up @@ -179,7 +177,7 @@ else
"$DATA/demo" -addr "$ADDR" -token "$TOKEN" -template "$TEMPLATE" -n 3
fi

echo "== v2 smoke: files/session/find/replace/watch/git/pty through the relay"
echo "== smoke: files/session/find/replace/watch/git/pty through the relay"
# LSP_TEMPLATE (optional, a python-flavor ref) adds the LSP broker step.
"$DATA/smoke" -addr "$ADDR" -token "$TOKEN" -template "$TEMPLATE" ${BRIDGE:+-egress} ${LSP_TEMPLATE:+-lsp-template "$LSP_TEMPLATE"}

Expand Down
7 changes: 4 additions & 3 deletions sdk/go/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sandbox
import (
"cmp"
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
Expand All @@ -16,7 +17,7 @@ func (c *Client) configureTLS() error {
tr, ok := cmp.Or(c.hc.Transport, http.DefaultTransport).(*http.Transport)
switch {
case !ok && c.tlsConfig != nil:
return fmt.Errorf("tls configuration requires an http.Transport")
return errors.New("tls configuration requires an http.Transport")
case !ok:
c.tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
case c.tlsConfig == nil && c.hc.Transport != nil:
Expand Down Expand Up @@ -53,12 +54,12 @@ func endpointURL(addr, scheme string) (*url.URL, error) {
}
if (u.Scheme != "http" && u.Scheme != httpsScheme) || u.Hostname() == "" || u.User != nil ||
(u.Path != "" && u.Path != "/") || strings.ContainsAny(addr, "?#") {
return nil, fmt.Errorf("sandboxd endpoint must be an http or https origin")
return nil, errors.New("sandboxd endpoint must be an http or https origin")
}
if port := u.Port(); port != "" {
n, err := strconv.Atoi(port)
if err != nil || n < 1 || n > 65535 {
return nil, fmt.Errorf("sandboxd endpoint port must be between 1 and 65535")
return nil, errors.New("sandboxd endpoint port must be between 1 and 65535")
}
}
u.Path = ""
Expand Down
4 changes: 1 addition & 3 deletions sdk/go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ go 1.27.1

replace github.com/cocoonstack/sandbox/protocol/wire => ../../protocol/wire

// Downstream consumers ignore the local replace, so this must name a real,
// resolvable version: cut a protocol/wire tag alongside every sdk/go release
// and keep this pinned to it.
// Consumers ignore the replace: pin the protocol/wire tag cut with each sdk/go release.
require github.com/cocoonstack/sandbox/protocol/wire v0.1.24
Loading
Loading