Skip to content

Commit 05ec052

Browse files
mjudeikisclaude
andauthored
refactor(infra): move sandbox runner image to the infrastructure provider (#375)
The runner is consumed by the infrastructure `sandbox-runner` template but lived under providers/app-studio and was published off the app-studio release tag (added in #374). The runner is fully self-contained — main.go + main_test.go are stdlib-only and nothing imports them — so it belongs with the template it serves, exactly like the `application` template's example app images are owned and built by the infrastructure provider. - Move providers/app-studio/runner -> providers/infrastructure/sandbox-runner as a standalone module (own go.mod, no requires) with a self-contained Dockerfile (drops the needless `COPY provider-sdk` + `COPY providers/app-studio`). - New infrastructure-sandbox-runner.yaml workflow mirroring infrastructure-examples.yaml: tests + builds on every change under the dir; on push to main publishes :latest + immutable :sha-<short> (PRs build-only). This decouples the runner image cadence from app-studio releases. - Revert the runner bits added to provider-release.yaml + images.yaml in #374. - Repoint Makefile target and both Tiltfiles at the new path. - Document the image: a package doc comment in main.go and a README covering what the binary does (dev-process supervisor + :7070 control API for sync/restart/logs), its env config, and the security posture. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e93bc94 commit 05ec052

12 files changed

Lines changed: 251 additions & 76 deletions

File tree

.github/workflows/images.yaml

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -121,32 +121,6 @@ jobs:
121121
tags: ${{ steps.meta.outputs.tags }}
122122
labels: ${{ steps.meta.outputs.labels }}
123123

124-
# PR-only validation that the sandbox runner Dockerfile + build context still
125-
# builds. The runner ships with app-studio (built from its ./runner) and is
126-
# PUBLISHED — with the app-studio version tags + :latest — by
127-
# provider-release.yaml on a `providers/app-studio/v*` tag, not here. Its
128-
# Dockerfile.runner COPYs provider-sdk + providers/app-studio, so the context
129-
# is the repo root (unlike the provider matrix above).
130-
build-sandbox-runner-image:
131-
if: github.event_name == 'pull_request'
132-
runs-on: ubuntu-latest
133-
permissions:
134-
contents: read
135-
steps:
136-
- name: Checkout repository
137-
uses: actions/checkout@v4
138-
139-
- name: Set up Docker Buildx
140-
uses: docker/setup-buildx-action@v3
141-
142-
- name: Build sandbox runner image (validation only)
143-
uses: docker/build-push-action@v6
144-
with:
145-
context: .
146-
file: ./providers/app-studio/Dockerfile.runner
147-
platforms: linux/amd64
148-
push: false
149-
150124
build-and-push-agent-image:
151125
runs-on: ubuntu-latest
152126
permissions:
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Infrastructure Sandbox Runner
2+
3+
# Builds the sandbox runner image for the infrastructure provider's
4+
# `sandbox-runner` template (spec.runnerImage defaults to :latest, so a new
5+
# sandbox always pulls a current build). The runner lives WITH the template it
6+
# serves — the infrastructure provider owns both — mirroring how the example
7+
# app images for the `application` template are built (infrastructure-examples.yaml).
8+
#
9+
# On push to main the image is tested + pushed (:latest + immutable short-sha);
10+
# on PRs it is tested + built single-arch but NOT pushed, so a broken runner is
11+
# caught before it reaches :latest.
12+
on:
13+
push:
14+
branches: [main]
15+
paths:
16+
- 'providers/infrastructure/sandbox-runner/**'
17+
- '.github/workflows/infrastructure-sandbox-runner.yaml'
18+
pull_request:
19+
branches: [main]
20+
paths:
21+
- 'providers/infrastructure/sandbox-runner/**'
22+
- '.github/workflows/infrastructure-sandbox-runner.yaml'
23+
workflow_dispatch:
24+
25+
permissions:
26+
contents: read
27+
28+
env:
29+
REGISTRY: ghcr.io
30+
IMAGE: ${{ github.repository_owner }}/kedge-sandbox-runner
31+
32+
jobs:
33+
test:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Checkout repository
37+
uses: actions/checkout@v4
38+
39+
- name: Set up Go
40+
uses: actions/setup-go@v5
41+
with:
42+
go-version: v1.26.1
43+
44+
- name: Test
45+
working-directory: providers/infrastructure/sandbox-runner
46+
run: go test ./...
47+
48+
build-and-push:
49+
needs: test
50+
runs-on: ubuntu-latest
51+
permissions:
52+
contents: read
53+
packages: write
54+
steps:
55+
- name: Checkout repository
56+
uses: actions/checkout@v4
57+
58+
- name: Log in to the Container registry
59+
if: github.event_name != 'pull_request'
60+
uses: docker/login-action@v3
61+
with:
62+
registry: ${{ env.REGISTRY }}
63+
username: ${{ github.actor }}
64+
password: ${{ secrets.GITHUB_TOKEN }}
65+
66+
- name: Set up Docker Buildx
67+
uses: docker/setup-buildx-action@v3
68+
69+
- name: Extract metadata (tags, labels) for Docker
70+
id: meta
71+
uses: docker/metadata-action@v5
72+
with:
73+
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
74+
# :latest on the default branch, plus an immutable short-sha tag.
75+
tags: |
76+
type=raw,value=latest,enable={{is_default_branch}}
77+
type=sha,format=short
78+
79+
- name: Build and push Docker image
80+
uses: docker/build-push-action@v6
81+
with:
82+
context: ./providers/infrastructure/sandbox-runner
83+
file: ./providers/infrastructure/sandbox-runner/Dockerfile
84+
# Multi-arch on push to main; single-arch build-only on PRs.
85+
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
86+
push: ${{ github.event_name != 'pull_request' }}
87+
tags: ${{ steps.meta.outputs.tags }}
88+
labels: ${{ steps.meta.outputs.labels }}

.github/workflows/provider-release.yaml

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,6 @@ jobs:
7878
${{ env.REGISTRY }}/${{ steps.tag.outputs.image }}:${{ steps.tag.outputs.version }}
7979
${{ env.REGISTRY }}/${{ steps.tag.outputs.image }}:${{ steps.tag.outputs.semver }}
8080
81-
# The sandbox runner image is part of app-studio: it is built from
82-
# app-studio's ./runner and is what the infrastructure SandboxRunner
83-
# template runs. Publish it on every app-studio release with the SAME
84-
# version tags as the provider image, plus :latest (the tag the template
85-
# defaults to). Dockerfile.runner COPYs provider-sdk + providers/app-studio,
86-
# so its build context is the repo root, not ./providers/app-studio.
87-
- name: Build and push sandbox runner image
88-
if: steps.tag.outputs.provider == 'app-studio'
89-
uses: docker/build-push-action@v6
90-
with:
91-
context: .
92-
file: ./providers/app-studio/Dockerfile.runner
93-
platforms: linux/amd64,linux/arm64
94-
push: true
95-
tags: |
96-
${{ env.REGISTRY }}/${{ github.repository_owner }}/kedge-sandbox-runner:${{ steps.tag.outputs.version }}
97-
${{ env.REGISTRY }}/${{ github.repository_owner }}/kedge-sandbox-runner:${{ steps.tag.outputs.semver }}
98-
${{ env.REGISTRY }}/${{ github.repository_owner }}/kedge-sandbox-runner:latest
99-
10081
- name: Install Helm
10182
uses: azure/setup-helm@v3
10283
with:

Makefile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,16 +1111,19 @@ uninstall-provider-app-studio: ## Delete App Studio CatalogEntry
11111111
--insecure-skip-tls-verify \
11121112
delete -f $(APP_STUDIO_MANIFEST) -f $(APP_STUDIO_PROVIDER_MANIFEST)
11131113

1114-
# --- App Studio sandbox runner image (live development runtimes) ---
1114+
# --- Sandbox runner image (live development runtimes) ---
1115+
# Owned by the infrastructure provider alongside the `sandbox-runner` template
1116+
# it serves. Self-contained build context (its own go.mod, stdlib-only).
1117+
SANDBOX_RUNNER_DIR ?= providers/infrastructure/sandbox-runner
11151118
SANDBOX_RUNNER_IMAGE ?= ghcr.io/faroshq/kedge-sandbox-runner:latest
11161119
SANDBOX_TOKEN_GENERATOR_IMAGE ?= docker.io/bitnami/kubectl@sha256:b9f4412e53f09d76b0991cdd29c0feff4c1d1e112b307e0ab155e5b050a9f4ec
11171120
SANDBOX_RUNNER_PLATFORM ?= linux/$(ARCH)
11181121

1119-
docker-build-sandbox-runner: ## Build the App Studio sandbox runner image used by SandboxRunner pods
1120-
docker build -f providers/app-studio/Dockerfile.runner \
1122+
docker-build-sandbox-runner: ## Build the sandbox runner image used by SandboxRunner pods
1123+
docker build -f $(SANDBOX_RUNNER_DIR)/Dockerfile \
11211124
--platform $(SANDBOX_RUNNER_PLATFORM) \
11221125
--provenance=false \
1123-
-t $(SANDBOX_RUNNER_IMAGE) .
1126+
-t $(SANDBOX_RUNNER_IMAGE) $(SANDBOX_RUNNER_DIR)
11241127

11251128
load-sandbox-runner-image: docker-build-sandbox-runner ## Load the sandbox runner image into the local kind runtime cluster
11261129
@echo ">>> loading $(SANDBOX_RUNNER_IMAGE) into kind cluster $(KRO_KIND_NAME)"

Tiltfile

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,6 @@ local_resource(
265265
'providers/app-studio/client',
266266
'providers/app-studio/store',
267267
'providers/app-studio/tenant',
268-
'providers/app-studio/runner',
269-
'providers/app-studio/Dockerfile.runner',
270268
'providers/app-studio/go.mod',
271269
'providers/app-studio/go.sum',
272270
'providers/app-studio/portal/src',
@@ -321,15 +319,13 @@ local_resource(
321319
labels=['providers-app-studio'],
322320
)
323321

324-
# --- App Studio sandbox runner image (live development runtimes) ---
322+
# --- Sandbox runner image (live development runtimes) ---
323+
# Owned by the infrastructure provider alongside the sandbox-runner template.
325324
local_resource(
326325
'sandbox-runner-image',
327326
cmd='make load-sandbox-runner-image',
328327
deps=[
329-
'providers/app-studio/Dockerfile.runner',
330-
'providers/app-studio/go.mod',
331-
'providers/app-studio/go.sum',
332-
'providers/app-studio/runner',
328+
'providers/infrastructure/sandbox-runner',
333329
],
334330
resource_deps=['kro-mgmt-up'],
335331
labels=['providers-app-studio'],

Tiltfile.cluster

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -741,8 +741,6 @@ local_resource(
741741
'providers/app-studio/client',
742742
'providers/app-studio/store',
743743
'providers/app-studio/tenant',
744-
'providers/app-studio/runner',
745-
'providers/app-studio/Dockerfile.runner',
746744
'providers/app-studio/go.mod',
747745
'providers/app-studio/go.sum',
748746
'providers/app-studio/portal/src',
@@ -1118,16 +1116,14 @@ local_resource(
11181116
labels=['providers-code'],
11191117
)
11201118

1121-
# --- App Studio sandbox runner image (live development runtimes) ---
1119+
# --- Sandbox runner image (live development runtimes) ---
1120+
# Owned by the infrastructure provider alongside the sandbox-runner template.
11221121
local_resource(
11231122
'sandbox-runner-image',
11241123
cmd=('KRO_KIND_NAME={name} make load-sandbox-runner-image').format(
11251124
name=cluster_name),
11261125
deps=[
1127-
'providers/app-studio/Dockerfile.runner',
1128-
'providers/app-studio/go.mod',
1129-
'providers/app-studio/go.sum',
1130-
'providers/app-studio/runner',
1126+
'providers/infrastructure/sandbox-runner',
11311127
],
11321128
labels=['providers-app-studio'],
11331129
)

providers/app-studio/Dockerfile.runner

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Sandbox runner for the infrastructure provider's `sandbox-runner` template.
2+
# The runner is a self-contained, stdlib-only Go binary (its own go.mod, no
3+
# requires), so the build context is just this dir — no cross-provider COPY.
4+
# The node base is intentional: the runner supervises a user dev process that
5+
# shells out to npm/vite, and git is needed for workspace operations.
6+
FROM golang:1.26 AS build
7+
WORKDIR /src
8+
COPY . .
9+
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/sandbox-runner .
10+
11+
FROM node:22-bookworm
12+
RUN apt-get update && apt-get install -y ca-certificates git && rm -rf /var/lib/apt/lists/*
13+
COPY --from=build /out/sandbox-runner /usr/local/bin/sandbox-runner
14+
WORKDIR /workspace
15+
EXPOSE 3000 7070
16+
ENTRYPOINT ["/usr/local/bin/sandbox-runner"]
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# kedge-sandbox-runner
2+
3+
The container image that runs inside every **SandboxRunner** pod — an App Studio
4+
*live development environment*. It is owned by the **infrastructure provider**,
5+
alongside the [`sandbox-runner` template](../install/templates/sandbox-runner.yaml)
6+
whose `spec.runnerImage` defaults to it (`ghcr.io/faroshq/kedge-sandbox-runner:latest`).
7+
8+
## What it is
9+
10+
A single, dependency-free Go program (`main.go`, standard library only) that acts
11+
as the pod's entrypoint and **control plane for one workspace**. It lets the
12+
platform sync code, restart the app, and read logs **without** shelling into the
13+
pod or handing anyone a runtime-cluster credential.
14+
15+
The image is built `FROM node:22-bookworm` (plus `git` + `ca-certificates`)
16+
because the supervised dev process typically runs `npm` / `vite`. The Go binary
17+
is the `ENTRYPOINT`; the user's app is a child process it manages.
18+
19+
```
20+
┌─ SandboxRunner pod ────────────────────────────────────────┐
21+
│ sandbox-runner (Go) user dev process │
22+
│ ── control API :7070 ──┐ ── npm run dev :3000 ──┐ │
23+
│ /healthz /sync │ start/stop/restart │ │
24+
│ /restart /logs └──────────► (child) ◄── logs ┘ │
25+
└──────────▲─────────────────────────────────────────────────┘
26+
│ reverse-proxied by the infra provider data-plane
27+
│ (caller's logs/sync/restart, control token injected)
28+
```
29+
30+
## What the Go app does
31+
32+
1. **Supervises the dev process.** Runs `SANDBOX_START_COMMAND` (e.g.
33+
`npm install && npm run dev`) in the workspace, streams its stdout/stderr into
34+
a 500-line in-memory ring buffer, and can stop/start it on demand.
35+
36+
2. **Serves an HTTP control API on `:7070`:**
37+
38+
| Method & path | Auth | Purpose |
39+
|---|---|---|
40+
| `GET /healthz` | none | Liveness probe. |
41+
| `POST /sync` | token | Write/delete workspace files, optionally restart. Body: `{files:[{path,content}], deletePaths:[], restart:"auto"\|"always"\|""}`. |
42+
| `POST /restart` | token | Stop + start the dev process. |
43+
| `GET /logs` | token | Buffered dev-process output (`text/plain`). |
44+
45+
`restart:"auto"` restarts **only** when a startup-affecting file actually
46+
changed (`package.json`, a lockfile, `vite.config.*`, `server.js`) or the
47+
process isn't currently running — so editing a component doesn't bounce the
48+
server, but changing dependencies does.
49+
50+
## How it's driven
51+
52+
App Studio never talks to this pod directly. The infrastructure provider's
53+
data-plane handler authorizes the caller against their workload instance, then
54+
**reverse-proxies** the `logs` / `sync` / `restart` verb to this runner's `:7070`
55+
through the runtime cluster's Service, injecting the per-instance control token.
56+
Consumers therefore hold no runtime credential.
57+
58+
## Configuration (environment)
59+
60+
Set by the template's ResourceGraphDefinition on the pod:
61+
62+
| Variable | Default | Meaning |
63+
|---|---|---|
64+
| `SANDBOX_WORKDIR` | `/workspace` | Workspace root; all file ops are confined here. |
65+
| `SANDBOX_START_COMMAND` || Command to launch the dev process (run via `sh -c`). |
66+
| `SANDBOX_PORT` || Port the dev app binds (surfaced for the start command). |
67+
| `SANDBOX_CONTROL_TOKEN` || Bearer for the control API. Read once, then **unset** from the environment. |
68+
| `SANDBOX_ALLOW_INSECURE_CONTROL` | `false` | Dev-only escape hatch: serve the control API with no token. |
69+
70+
## Security posture
71+
72+
- **Workspace confinement.** All `/sync` writes/deletes go through `os.Root` rooted
73+
at `SANDBOX_WORKDIR`, plus path cleaning that rejects absolute paths and `../`
74+
escapes — a forged path can't write outside the workspace.
75+
- **Authenticated control.** Every endpoint except `/healthz` requires
76+
`X-Sandbox-Control-Token`, constant-time compared against `SANDBOX_CONTROL_TOKEN`.
77+
- **Non-root.** The template runs the pod as `runAsNonRoot` (uid 1000, seccomp
78+
`RuntimeDefault`).
79+
80+
This runs user-provided code; it is a **development** runtime, not a hardened
81+
multi-tenant sandbox (no per-pod network policy or CPU/mem quotas are enforced
82+
by the runner itself).
83+
84+
## Build & publish
85+
86+
Self-contained module (its own `go.mod`, no external requires), so the build
87+
context is just this directory.
88+
89+
```bash
90+
# build the image locally and load it into the local kind runtime cluster
91+
make load-sandbox-runner-image # → docker-build-sandbox-runner + kind load
92+
93+
# run the tests
94+
go test ./...
95+
```
96+
97+
CI: [`.github/workflows/infrastructure-sandbox-runner.yaml`](../../../.github/workflows/infrastructure-sandbox-runner.yaml)
98+
tests + builds on every change under this directory, and on push to `main`
99+
publishes `ghcr.io/<owner>/kedge-sandbox-runner` as `:latest` plus an immutable
100+
`:sha-<short>` tag (PRs build single-arch without pushing). This mirrors how the
101+
`application` template's example app images are published.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/faroshq/kedge/providers/infrastructure/sandbox-runner
2+
3+
go 1.26.3

0 commit comments

Comments
 (0)