forked from wrenn/wrenn
Fix expandEnv regex, init script crash, healthcheck deadline, and test issues
- Fix envRegex: remove spurious (\$)? group that swallowed $$$, handle ${}
- wrenn-init.sh: add || true to networking commands under set -e, remove dead code
- waitForHealthcheck: use context deadline for unlimited retries instead of implicit 100 cap
- Make parseSandboxEnv a package-level function (unused receiver)
- Fix WrappedCommand test: map iteration order dependency, pre-expand env values
- Fix error wrapping: %v → %w per project conventions
- test-jupyter-kernel.py: move import to top-level, fix misleading comment
This commit is contained in:
@ -20,34 +20,14 @@ echo "+cpu +memory +io" > /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null || t
|
|||||||
# Set hostname
|
# Set hostname
|
||||||
hostname sandbox
|
hostname sandbox
|
||||||
|
|
||||||
# Configure networking from kernel cmdline (ip=client::gw:mask:host:iface:autoconf).
|
# Configure networking if the kernel ip= boot arg did not already set it up.
|
||||||
# if command -v ip >/dev/null 2>&1; then
|
|
||||||
# iparg=$(cat /proc/cmdline | tr ' ' '\n' | sed -n 's/^ip=//p')
|
|
||||||
# if [ -n "$iparg" ]; then
|
|
||||||
# client=$(echo "$iparg" | cut -d: -f1)
|
|
||||||
# gw=$(echo "$iparg" | cut -d: -f2)
|
|
||||||
# mask=$(echo "$iparg" | cut -d: -f3)
|
|
||||||
# iface=$(echo "$iparg" | cut -d: -f5)
|
|
||||||
# [ -z "$iface" ] && iface=eth0
|
|
||||||
# if [ -n "$client" ]; then
|
|
||||||
# ip addr add "$client/${mask:-30}" dev "$iface" 2>/dev/null || true
|
|
||||||
# ip link set "$iface" up 2>/dev/null || true
|
|
||||||
# if [ -n "$gw" ]; then
|
|
||||||
# ip route add default via "$gw" 2>/dev/null || true
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
#
|
|
||||||
#
|
|
||||||
if ! ip addr show eth0 2>/dev/null | grep -q "169.254.0.21"; then
|
if ! ip addr show eth0 2>/dev/null | grep -q "169.254.0.21"; then
|
||||||
ip link set lo up
|
ip link set lo up 2>/dev/null || true
|
||||||
ip link set eth0 up
|
ip link set eth0 up 2>/dev/null || true
|
||||||
ip addr add 169.254.0.21/30 dev eth0
|
ip addr add 169.254.0.21/30 dev eth0 2>/dev/null || true
|
||||||
ip route add default via 169.254.0.22
|
ip route add default via 169.254.0.22 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Configure DNS resolver.
|
# Configure DNS resolver.
|
||||||
echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
||||||
echo "nameserver 8.8.4.4" >> /etc/resolv.conf
|
echo "nameserver 8.8.4.4" >> /etc/resolv.conf
|
||||||
|
|||||||
@ -13,10 +13,10 @@ type ExecContext struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This regex matches:
|
// This regex matches:
|
||||||
// 1. $$
|
// 1. $$ (escaped dollar)
|
||||||
// 2. ${ANY_WORD}
|
// 2. ${VAR} or ${} (braced variable, possibly empty)
|
||||||
// 3. $ANY_WORD
|
// 3. $VAR (bare variable)
|
||||||
var envRegex = regexp.MustCompile(`\$\$(\$)?|\$\{([a-zA-Z0-9_]+)\}|\$([a-zA-Z0-9_]+)`)
|
var envRegex = regexp.MustCompile(`\$\$|\$\{([a-zA-Z0-9_]*)\}|\$([a-zA-Z0-9_]+)`)
|
||||||
|
|
||||||
// WrappedCommand returns the full shell command for a RUN step with context
|
// WrappedCommand returns the full shell command for a RUN step with context
|
||||||
// applied. The result is passed as the argument to /bin/sh -c.
|
// applied. The result is passed as the argument to /bin/sh -c.
|
||||||
@ -77,7 +77,7 @@ func expandEnv(s string, vars map[string]string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var name string
|
var name string
|
||||||
if match[1] == '{' {
|
if len(match) > 1 && match[1] == '{' {
|
||||||
name = match[2 : len(match)-1]
|
name = match[2 : len(match)-1]
|
||||||
} else {
|
} else {
|
||||||
name = match[1:]
|
name = match[1:]
|
||||||
|
|||||||
@ -4,10 +4,11 @@ import "testing"
|
|||||||
|
|
||||||
func TestExecContext_WrappedCommand(t *testing.T) {
|
func TestExecContext_WrappedCommand(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ctx ExecContext
|
ctx ExecContext
|
||||||
cmd string
|
cmd string
|
||||||
want string
|
want string
|
||||||
|
wantOneOf []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no context",
|
name: "no context",
|
||||||
@ -46,19 +47,34 @@ func TestExecContext_WrappedCommand(t *testing.T) {
|
|||||||
want: "MSG='it'\\''s fine' /bin/sh -c 'echo $MSG'",
|
want: "MSG='it'\\''s fine' /bin/sh -c 'echo $MSG'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "env expansion with dollar sign PATH",
|
name: "env expansion with pre-expanded PATH",
|
||||||
ctx: ExecContext{
|
ctx: ExecContext{
|
||||||
EnvVars: map[string]string{"PATH": "/usr/bin", "FOO": "/opt/venv/bin:$PATH"},
|
EnvVars: map[string]string{"PATH": "/usr/bin", "FOO": "/opt/venv/bin:/usr/bin"},
|
||||||
|
},
|
||||||
|
cmd: "make build",
|
||||||
|
// Map iteration order is non-deterministic; accept either ordering.
|
||||||
|
wantOneOf: []string{
|
||||||
|
"FOO='/opt/venv/bin:/usr/bin' PATH='/usr/bin' /bin/sh -c 'make build'",
|
||||||
|
"PATH='/usr/bin' FOO='/opt/venv/bin:/usr/bin' /bin/sh -c 'make build'",
|
||||||
},
|
},
|
||||||
cmd: "make build",
|
|
||||||
want: "FOO='/opt/venv/bin:/usr/bin' PATH='/usr/bin' /bin/sh -c 'make build'",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
got := tc.ctx.WrappedCommand(tc.cmd)
|
got := tc.ctx.WrappedCommand(tc.cmd)
|
||||||
if got != tc.want {
|
if len(tc.wantOneOf) > 0 {
|
||||||
|
matched := false
|
||||||
|
for _, w := range tc.wantOneOf {
|
||||||
|
if got == w {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Errorf("WrappedCommand(%q)\n got %q\n want one of %q", tc.cmd, got, tc.wantOneOf)
|
||||||
|
}
|
||||||
|
} else if got != tc.want {
|
||||||
t.Errorf("WrappedCommand(%q)\n got %q\n want %q", tc.cmd, got, tc.want)
|
t.Errorf("WrappedCommand(%q)\n got %q\n want %q", tc.cmd, got, tc.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -464,15 +464,18 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
// Returns nil on the first successful check, or an error if retries are
|
// Returns nil on the first successful check, or an error if retries are
|
||||||
// exhausted, the deadline passes, or the context is cancelled.
|
// exhausted, the deadline passes, or the context is cancelled.
|
||||||
func (s *BuildService) waitForHealthcheck(ctx context.Context, agent buildAgentClient, sandboxIDStr string, hc recipe.HealthcheckConfig) error {
|
func (s *BuildService) waitForHealthcheck(ctx context.Context, agent buildAgentClient, sandboxIDStr string, hc recipe.HealthcheckConfig) error {
|
||||||
maxAttempts := 100
|
|
||||||
if hc.Retries > 0 {
|
|
||||||
maxAttempts = hc.Retries
|
|
||||||
}
|
|
||||||
deadline := time.NewTimer(hc.StartPeriod + time.Duration(maxAttempts+1)*hc.Interval)
|
|
||||||
defer deadline.Stop()
|
|
||||||
ticker := time.NewTicker(hc.Interval)
|
ticker := time.NewTicker(hc.Interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// When retries > 0, set a deadline based on the retry budget.
|
||||||
|
// When retries == 0 (unlimited), rely solely on the parent context deadline.
|
||||||
|
var deadlineCh <-chan time.Time
|
||||||
|
if hc.Retries > 0 {
|
||||||
|
deadline := time.NewTimer(hc.StartPeriod + time.Duration(hc.Retries+1)*hc.Interval)
|
||||||
|
defer deadline.Stop()
|
||||||
|
deadlineCh = deadline.C
|
||||||
|
}
|
||||||
|
|
||||||
startedAt := time.Now()
|
startedAt := time.Now()
|
||||||
failCount := 0
|
failCount := 0
|
||||||
|
|
||||||
@ -480,7 +483,7 @@ func (s *BuildService) waitForHealthcheck(ctx context.Context, agent buildAgentC
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-deadline.C:
|
case <-deadlineCh:
|
||||||
return fmt.Errorf("healthcheck timed out: exceeded %d attempts over %s", failCount, time.Since(startedAt))
|
return fmt.Errorf("healthcheck timed out: exceeded %d attempts over %s", failCount, time.Since(startedAt))
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
execCtx, cancel := context.WithTimeout(ctx, hc.Timeout)
|
execCtx, cancel := context.WithTimeout(ctx, hc.Timeout)
|
||||||
@ -497,7 +500,7 @@ func (s *BuildService) waitForHealthcheck(ctx context.Context, agent buildAgentC
|
|||||||
if time.Since(startedAt) >= hc.StartPeriod {
|
if time.Since(startedAt) >= hc.StartPeriod {
|
||||||
failCount++
|
failCount++
|
||||||
if hc.Retries > 0 && failCount >= hc.Retries {
|
if hc.Retries > 0 && failCount >= hc.Retries {
|
||||||
return fmt.Errorf("healthcheck failed after %d retries: exec error: %v", failCount, err)
|
return fmt.Errorf("healthcheck failed after %d retries: exec error: %w", failCount, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@ -574,14 +577,14 @@ func (s *BuildService) fetchSandboxEnv(ctx context.Context,
|
|||||||
resp.Msg.ExitCode)
|
resp.Msg.ExitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.parseSandboxEnv(string(resp.Msg.Stdout)), nil
|
return parseSandboxEnv(string(resp.Msg.Stdout)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSandboxEnv converts the raw newline-separated output of an 'env'
|
// parseSandboxEnv converts the raw newline-separated output of an 'env'
|
||||||
// command into a map.
|
// command into a map.
|
||||||
// It skips empty lines and malformed entries, and correctly handles value
|
// It skips empty lines and malformed entries, and correctly handles values
|
||||||
// containing '='.
|
// containing '='.
|
||||||
func (s *BuildService) parseSandboxEnv(raw string) map[string]string {
|
func parseSandboxEnv(raw string) map[string]string {
|
||||||
envVars := make(map[string]string)
|
envVars := make(map[string]string)
|
||||||
|
|
||||||
for line := range strings.SplitSeq(raw, "\n") {
|
for line := range strings.SplitSeq(raw, "\n") {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.request
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -12,8 +13,6 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
def create_kernel(base_url: str, token: str) -> str:
|
def create_kernel(base_url: str, token: str) -> str:
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
url = f"{base_url}/api/kernels"
|
url = f"{base_url}/api/kernels"
|
||||||
headers = {}
|
headers = {}
|
||||||
if token:
|
if token:
|
||||||
@ -57,7 +56,7 @@ def execute_code(ws: websocket.WebSocket, code: str) -> dict:
|
|||||||
while True:
|
while True:
|
||||||
resp = json.loads(ws.recv())
|
resp = json.loads(ws.recv())
|
||||||
|
|
||||||
# CRITICAL FIX: Ignore messages left over from previous executions
|
# Filter out messages from other executions by matching msg_id
|
||||||
parent_id = resp.get("parent_header", {}).get("msg_id")
|
parent_id = resp.get("parent_header", {}).get("msg_id")
|
||||||
if parent_id != msg_id:
|
if parent_id != msg_id:
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user