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
4 changes: 2 additions & 2 deletions .github/workflows/build-os-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
all_names
all_changed=""
else
all_changed=$(git diff --name-only "$BEFORE" "$SHA" -- 'os-image/' | sort -u)
all_changed=$(git diff --name-only "$BEFORE" "$SHA" -- 'os-image/' ':!*.md' | sort -u)
declare -A names
for f in $all_changed; do
name=$(echo "$f" | cut -d'/' -f2)
Expand All @@ -112,7 +112,7 @@ jobs:
needs_no_cache="true"
elif [[ -n "$all_changed" ]]; then
while IFS= read -r f; do
[[ -z "$f" || "$f" == *.md ]] && continue
[[ -z "$f" ]] && continue
depth=$(echo "$f" | awk -F'/' '{print NF}')
base_name=$(basename "$f")
if [[ "$depth" -eq 3 ]] || { [[ "$depth" -eq 4 ]] && [[ "$base_name" != "Dockerfile" ]]; }; then
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
- ".github/workflows/build-os-images.yml"
- ".github/workflows/build-silkd.yml"
- ".github/workflows/build-boot.yml"
- "!**/*.md"
workflow_dispatch:
inputs:
no-cache:
Expand Down Expand Up @@ -58,7 +59,7 @@ jobs:
if [[ "$EVT" == "workflow_dispatch" || -z "$BEFORE" || "$BEFORE" == "0000000000000000000000000000000000000000" ]]; then
boot=true; silkd=true; osimage=true
else
changed() { git diff --name-only "$BEFORE" "$SHA" -- "$@" | grep -q .; }
changed() { git diff --name-only "$BEFORE" "$SHA" -- "$@" ':!*.md' | grep -q .; }
# Each area carries its own build workflow: changing how an image is
# built must rebuild it, or the run goes green having built nothing.
wf=.github/workflows
Expand Down
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ linters:
- modernize
- nakedret
- nestif
- nolintlint
- revive
- staticcheck
- unconvert
Expand All @@ -38,9 +39,12 @@ linters:
disable:
- fieldalignment
revive:
enable-default-rules: true
rules:
- name: exported
disabled: true
nolintlint:
allow-unused: false
misspell:
locale: US
goconst:
Expand Down
2 changes: 1 addition & 1 deletion boot/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ RUN set -e; \
fi; \
gcc usr/gen_init_cpio.c -o /usr/local/bin/gen_init_cpio

FROM rust:1.98.0-alpine@sha256:a10e64dd139b7387337c7fbe8aca31b959b57b2fd4c8ae20a02cf1d6ea424dce AS initbuild
FROM rust:1.98.1-alpine@sha256:7cc1c22d77d9432f7fe012a70e6d3e555af54c2a6832700ed7d553f1769ae89f AS initbuild
RUN apk add --no-cache musl-dev
WORKDIR /build
# See silkd/Dockerfile: the pin, not the base image's default, is the compiler.
Expand Down
2 changes: 1 addition & 1 deletion boot/init/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "sandbox-init"
version = "0.1.0"
edition = "2024"
rust-version = "1.98"
rust-version = "1.98.1"
description = "Initramfs PID 1: assemble the EROFS+overlay rootfs and switch_root"
publish = false

Expand Down
2 changes: 1 addition & 1 deletion boot/init/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "1.98.0"
channel = "1.98.1"
components = ["rustfmt", "clippy"]
profile = "minimal"
8 changes: 5 additions & 3 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,11 @@ WantedBy=multi-user.target

Each record carries its syslog level, so alerting keys on systemd priority:
`journalctl -u sandboxd -p err` lists the errors and `-p warning` the
degradations. `journalctl` answers an empty range with `-- No entries --`, so
a check that counts lines reads one problem where there are none — count
records (`-o json | wc -l`) or test the output for emptiness.
degradations. With stderr off a terminal, as under this unit, the record
itself is one JSON object; a terminal gets the console format. `journalctl`
answers an empty range with `-- No entries --`, so a check that counts lines
reads one problem where there are none — count records (`-o json | wc -l`) or
test the output for emptiness.

Stopping sandboxd leaves VMs alive; the next start reconciles them. The unit's
`KillMode=process` is what guarantees it: systemd signals sandboxd alone, so a
Expand Down
2 changes: 1 addition & 1 deletion e2e/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/cocoonstack/sandbox/e2e

go 1.27.0
go 1.27.1

require (
github.com/aws/aws-sdk-go-v2 v1.47.0 // indirect
Expand Down
2 changes: 1 addition & 1 deletion mcp/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/cocoonstack/sandbox/mcp

go 1.27.0
go 1.27.1

require (
github.com/cocoonstack/sandbox/protocol/wire v0.1.13
Expand Down
7 changes: 6 additions & 1 deletion mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const shutdownGrace = 10 * time.Second

func main() {
ctx := context.Background()
if err := log.SetupLog(ctx, &coretypes.ServerLogConfig{Level: "error"}, ""); err != nil {
if err := log.SetupLog(ctx, &coretypes.ServerLogConfig{Level: "error", UseJSON: !stderrIsTerminal()}, ""); err != nil {
fmt.Fprintln(os.Stderr, "setup log:", err)
os.Exit(1)
}
Expand Down Expand Up @@ -56,3 +56,8 @@ func main() {
}
}
}

func stderrIsTerminal() bool {
fi, err := os.Stderr.Stat()
return err == nil && fi.Mode()&os.ModeCharDevice != 0
}
2 changes: 1 addition & 1 deletion protocol/wire/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/cocoonstack/sandbox/protocol/wire

go 1.27.0
go 1.27.1
2 changes: 1 addition & 1 deletion sandboxd/egress/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestCloseReleasesIdleUpstreamConns(t *testing.T) {

func TestForwardNeverInjectsInterceptSecret(t *testing.T) {
reached := false
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
reached = true
}))
defer upstream.Close()
Expand Down
12 changes: 6 additions & 6 deletions sandboxd/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,22 +448,22 @@ func belowFloor(v string) (below, comparable bool) {
return semver.Compare(v, RequiredCocoon) < 0, true
}

func readBufLine(r *bufio.Reader, max int) (string, error) {
func readBufLine(r *bufio.Reader, limit int) (string, error) {
line, err := r.ReadSlice('\n')
if err != nil {
return "", err
}
if len(line) > max {
return "", fmt.Errorf("reply exceeds %d bytes", max)
if len(line) > limit {
return "", fmt.Errorf("reply exceeds %d bytes", limit)
}
return string(line[:len(line)-1]), nil
}

// readLine reads byte-wise so nothing past the newline is consumed.
func readLine(conn net.Conn, max int) (string, error) {
func readLine(conn net.Conn, limit int) (string, error) {
var sb strings.Builder
var b [1]byte
for sb.Len() < max {
for sb.Len() < limit {
if _, err := io.ReadFull(conn, b[:]); err != nil {
return "", err
}
Expand All @@ -472,7 +472,7 @@ func readLine(conn net.Conn, max int) (string, error) {
}
sb.WriteByte(b[0])
}
return "", fmt.Errorf("reply exceeds %d bytes", max)
return "", fmt.Errorf("reply exceeds %d bytes", limit)
}

func tail(s string) string {
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/cocoonstack/sandbox/sandboxd

go 1.27.0
go 1.27.1

require (
github.com/aws/aws-sdk-go-v2 v1.47.0
Expand Down
22 changes: 16 additions & 6 deletions sandboxd/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ var _ zerolog.LevelWriter = journalWriter{}
type journalWriter struct {
console zerolog.ConsoleWriter
out io.Writer
json bool
}

func (j journalWriter) Write(p []byte) (int, error) { return j.WriteLevel(zerolog.NoLevel, p) }

func (j journalWriter) WriteLevel(level zerolog.Level, p []byte) (int, error) {
buf := &bytes.Buffer{}
buf.WriteString(syslogPrefix(level))
j.console.Out = buf
if _, err := j.console.Write(p); err != nil {
return 0, err
if j.json {
buf.Write(p)
} else {
j.console.Out = buf
if _, err := j.console.Write(p); err != nil {
return 0, err
}
}
if _, err := j.out.Write(buf.Bytes()); err != nil {
return 0, err
Expand All @@ -48,12 +53,12 @@ func setupLog(ctx context.Context, level string) error {
return err
}
logger := log.GetGlobalLogger()
*logger = zerolog.New(newJournalWriter(os.Stderr)).With().Timestamp().Logger().Level(logger.GetLevel())
*logger = zerolog.New(newJournalWriter(os.Stderr, !stderrIsTerminal())).With().Timestamp().Logger().Level(logger.GetLevel())
return nil
}

func newJournalWriter(out io.Writer) journalWriter {
return journalWriter{console: zerolog.ConsoleWriter{TimeFormat: time.RFC822, NoColor: true}, out: out}
func newJournalWriter(out io.Writer, json bool) journalWriter {
return journalWriter{console: zerolog.ConsoleWriter{TimeFormat: time.RFC822, NoColor: true}, out: out, json: json}
}

func syslogPrefix(level zerolog.Level) string {
Expand All @@ -70,3 +75,8 @@ func syslogPrefix(level zerolog.Level) string {
return syslogInfo
}
}

func stderrIsTerminal() bool {
fi, err := os.Stderr.Stat()
return err == nil && fi.Mode()&os.ModeCharDevice != 0
}
13 changes: 12 additions & 1 deletion sandboxd/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"encoding/json"
"io"
"strings"
"testing"
Expand Down Expand Up @@ -61,8 +62,18 @@ func TestSyslogPrefixMapsFatalToCritical(t *testing.T) {
}
}

func TestJournalWriterKeepsJSONRecordsBehindThePrefix(t *testing.T) {
counter := &countingWriter{}
logger := zerolog.New(newJournalWriter(counter, true))
logger.Error().Str("k", "v").Msg("boom")
line := counter.buf.String()
if counter.writes != 1 || !strings.HasPrefix(line, syslogErr+"{") || !json.Valid([]byte(line[len(syslogErr):])) {
t.Errorf("writes = %d, line = %q, want one write of %q and one JSON record", counter.writes, line, syslogErr)
}
}

func testLogger(out io.Writer) zerolog.Logger {
return zerolog.New(newJournalWriter(out))
return zerolog.New(newJournalWriter(out, false))
}

type countingWriter struct {
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/netfilter/nft_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ func Unlock(string) error { return errors.ErrUnsupported }

func LockedTaps() (map[string]bool, error) { return nil, errors.ErrUnsupported }

func SweepExcept(keep map[string]bool) error { return nil }
func SweepExcept(map[string]bool) error { return nil }
4 changes: 2 additions & 2 deletions sandboxd/pool/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,13 +932,13 @@ func archivedCount(m *Manager) int {
return g.Archived
}

func archivePool(delete int) config.PoolSpec {
func archivePool(deleteAfter int) config.PoolSpec {
return config.PoolSpec{
PoolKey: testKey,
Warm: 1,
IdleHibernateSeconds: 1,
ArchiveAfterSeconds: 2,
ArchiveDeleteAfterSeconds: delete,
ArchiveDeleteAfterSeconds: deleteAfter,
}
}

Expand Down
8 changes: 4 additions & 4 deletions sandboxd/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ func TestAgentErrorPaths(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mgr := &fakeManager{socket: func(id, token string) (string, error) { return "/v/sock", tt.sockErr }}
mgr := &fakeManager{socket: func(string, string) (string, error) { return "/v/sock", tt.sockErr }}
dialer := &fakeDialer{dial: func(context.Context, string) (net.Conn, error) {
if tt.dialErr != nil {
return nil, tt.dialErr
Expand Down Expand Up @@ -737,7 +737,7 @@ func TestAgentErrorPaths(t *testing.T) {
}

func TestForkFlow(t *testing.T) {
mgr := &fakeManager{fork: func(id, token string, count int, ttl time.Duration) ([]*types.Sandbox, error) {
mgr := &fakeManager{fork: func(_, token string, count int, ttl time.Duration) ([]*types.Sandbox, error) {
switch {
case token != "tok":
return nil, pool.ErrUnknownSandbox
Expand Down Expand Up @@ -808,7 +808,7 @@ func TestPromoteAndDeleteTemplateFlow(t *testing.T) {
var gotKey types.PoolKey
mgr := &fakeManager{
promoteContentDigest: "sha256:promoted-digest",
promote: func(id, token, template string) error {
promote: func(_, token, template string) error {
switch {
case token != "tok":
return pool.ErrUnknownSandbox
Expand Down Expand Up @@ -1604,7 +1604,7 @@ func TestVolumeClaimRejectsShapeBeforePlacement(t *testing.T) {
}

func TestOwnerEndpoint(t *testing.T) {
mgr := &fakeManager{socket: func(id, token string) (string, error) {
mgr := &fakeManager{socket: func(_, token string) (string, error) {
if token == "good" {
return "/v/sock", nil
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/go/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/cocoonstack/sandbox/sdk/go

go 1.27.0
go 1.27.1

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

Expand Down
2 changes: 1 addition & 1 deletion sdk/go/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func TestRunCancelKillsTheCommandItStarted(t *testing.T) {

func TestUpgradeKeepsCoalescedBytes(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/sandboxes/{id}/agent", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("GET /v1/sandboxes/{id}/agent", func(w http.ResponseWriter, _ *http.Request) {
conn, _, err := http.NewResponseController(w).Hijack()
if err != nil {
t.Errorf("hijack: %v", err)
Expand Down
1 change: 1 addition & 0 deletions sdk/go/silkd/conn.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package silkd frames a relay connection into silkd wire RPCs.
package silkd

import (
Expand Down
4 changes: 2 additions & 2 deletions sdk/go/volumes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func TestWithVolumesAttachOnlySendsFlagAndDecodesMountlessEcho(t *testing.T) {
}

func TestWithVolumesAttachOnlyRejectsMountLocally(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Error("server should not be contacted for a locally invalid claim")
}))
t.Cleanup(ts.Close)
Expand All @@ -287,7 +287,7 @@ func TestRejectsInvalidVolumeModeLocally(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Error("server should not be contacted for a locally invalid mode")
}))
t.Cleanup(ts.Close)
Expand Down
6 changes: 4 additions & 2 deletions sdk/go/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ func TestWatchDeliversEventsUntilClose(t *testing.T) {
if err := w.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
for range w.Events() {
for ev := range w.Events() {
t.Logf("drained %+v", ev)
}
if err := w.Err(); err != nil {
t.Errorf("Err after clean close: %v", err)
Expand All @@ -61,7 +62,8 @@ func TestWatchReleasesTheRelayWhenTheSandboxEnds(t *testing.T) {
if err != nil {
t.Fatalf("Watch: %v", err)
}
for range w.Events() {
for ev := range w.Events() {
t.Logf("drained %+v", ev)
}
if e, ok := errors.AsType[*wire.ErrorResp](w.Err()); !ok || e.Message != "watcher died" {
t.Fatalf("Err = %v, want the sandbox's error frame", w.Err())
Expand Down
2 changes: 1 addition & 1 deletion silkd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "silkd"
version = "0.1.13"
edition = "2024"
rust-version = "1.98"
rust-version = "1.98.1"
description = "In-guest sandbox daemon: exec with context, processes, and files over vsock"
publish = false

Expand Down
Loading
Loading