Skip to content

Commit 133b13a

Browse files
Implement operator-managed runtime architecture
1 parent e44a417 commit 133b13a

27 files changed

Lines changed: 2067 additions & 226 deletions

.github/kind-runtime-config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
kind: Cluster
2+
apiVersion: kind.x-k8s.io/v1alpha4
3+
nodes:
4+
- role: control-plane
5+
extraMounts:
6+
- hostPath: /dev/kvm
7+
containerPath: /dev/kvm
8+
- hostPath: /tmp/smolvm-kind-var-lib
9+
containerPath: /var/lib/smolvm
10+
- hostPath: /tmp/smolvm-kind-var-run
11+
containerPath: /var/run/smolvm

.github/workflows/ci.yml

Lines changed: 122 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ jobs:
1717
go-version-file: go.mod
1818
cache: true
1919
- name: Run controller-runtime envtest
20-
run: make test
20+
run: make test-report
21+
- name: Upload test reports
22+
if: always()
23+
uses: actions/upload-artifact@v4
24+
with:
25+
name: envtest-reports
26+
path: reports/
2127

2228
kind-e2e:
23-
name: kind e2e
29+
name: kind topology e2e
2430
runs-on: ubuntu-latest
2531
steps:
2632
- uses: actions/checkout@v4
@@ -31,56 +37,123 @@ jobs:
3137
- uses: helm/kind-action@v1
3238
with:
3339
cluster_name: kind
34-
- name: Run one deployment e2e test
40+
- name: Run deployed-topology e2e test
3541
env:
3642
RUN_E2E: "true"
3743
KIND_CLUSTER: kind
38-
run: make test-e2e
44+
run: make test-e2e-report
45+
- name: Upload e2e reports
46+
if: always()
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: e2e-reports
50+
path: reports/
3951

40-
smolvm-runtime-smoke:
41-
name: smolvm runtime smoke
52+
smolvm-runtime-e2e:
53+
name: smolvm runtime lifecycle e2e
4254
runs-on: ubuntu-latest
43-
timeout-minutes: 20
55+
timeout-minutes: 45
4456
steps:
4557
- uses: actions/checkout@v4
4658
- uses: actions/setup-go@v5
4759
with:
4860
go-version-file: go.mod
4961
cache: true
62+
- name: Prepare KVM-backed kind mounts
63+
run: |
64+
set -euxo pipefail
65+
test -e /dev/kvm
66+
sudo chmod 666 /dev/kvm
67+
sudo mkdir -p /tmp/smolvm-kind-var-lib /tmp/smolvm-kind-var-run
68+
sudo chmod 777 /tmp/smolvm-kind-var-lib /tmp/smolvm-kind-var-run
5069
- uses: helm/kind-action@v1
5170
with:
5271
cluster_name: runtime
53-
- name: Install smolvm
72+
config: .github/kind-runtime-config.yaml
73+
- name: Build and load operator images
5474
run: |
55-
sudo apt-get update
56-
sudo apt-get install -y libvirglrenderer1 libepoxy0 libgbm1
57-
curl -fsSL -o /tmp/smolvm.tar.gz https://github.com/smol-machines/smolvm/releases/download/v0.8.1/smolvm-0.8.1-linux-x86_64.tar.gz
58-
mkdir -p /tmp/smolvm
59-
tar -xzf /tmp/smolvm.tar.gz -C /tmp/smolvm --strip-components=1
60-
echo /tmp/smolvm >> "$GITHUB_PATH"
61-
/tmp/smolvm/smolvm --version
62-
- name: Run operator against real smolvm runtime
75+
set -euxo pipefail
76+
make docker-build IMG=example.com/smolvm-operator:e2e
77+
make docker-build-runtime RUNTIME_IMG=example.com/smolvm-runtime:e2e
78+
docker run --rm --entrypoint smolvm example.com/smolvm-runtime:e2e --version
79+
docker run --rm --entrypoint /runtime-agent example.com/smolvm-runtime:e2e --help
80+
kind load docker-image example.com/smolvm-operator:e2e --name runtime
81+
kind load docker-image example.com/smolvm-runtime:e2e --name runtime
82+
- name: Deploy full stack and run VM lifecycle
6383
run: |
6484
set -euxo pipefail
65-
sudo chmod 666 /dev/kvm
66-
export LD_LIBRARY_PATH="/tmp/smolvm/lib:${LD_LIBRARY_PATH:-}"
67-
export LD_PRELOAD="$(find /usr/lib -name 'libvirglrenderer.so.1' | head -1)"
68-
smolvm serve start --listen 127.0.0.1:8080 > smolvm-serve.log 2>&1 &
69-
serve_pid=$!
70-
trap 'kubectl delete smolvm runtime-smoke --ignore-not-found=true --wait=false || true; kill $serve_pid || true; cat smolvm-serve.log || true' EXIT
71-
72-
for i in {1..60}; do
73-
curl -fsS http://127.0.0.1:8080/api/v1/machines && break
74-
sleep 1
75-
done
85+
mkdir -p reports
86+
current_step="install"
87+
write_runtime_report() {
88+
local status="$1"
89+
local failed_step="${2:-}"
90+
if [ "$status" = "pass" ]; then
91+
cat > reports/runtime.xml <<'EOF'
92+
<testsuite name="smolvm runtime lifecycle e2e" tests="9" failures="0" errors="0">
93+
<testcase classname="runtime" name="install CRDs"/>
94+
<testcase classname="runtime" name="deploy controller Deployment and runtime DaemonSet"/>
95+
<testcase classname="runtime" name="runtime DaemonSet rolls out"/>
96+
<testcase classname="runtime" name="SmolVMNode reports Ready and RuntimeReady"/>
97+
<testcase classname="runtime" name="create unpinned SmolVM"/>
98+
<testcase classname="runtime" name="SmolVM is scheduled and Running"/>
99+
<testcase classname="runtime" name="patch spec.running false"/>
100+
<testcase classname="runtime" name="SmolVM reaches Stopped"/>
101+
<testcase classname="runtime" name="delete CR cleans runtime machine"/>
102+
</testsuite>
103+
EOF
104+
return
105+
fi
106+
cat > reports/runtime.xml <<EOF
107+
<testsuite name="smolvm runtime lifecycle e2e" tests="1" failures="1" errors="0">
108+
<testcase classname="runtime" name="${failed_step}">
109+
<failure message="runtime lifecycle e2e failed at ${failed_step}"/>
110+
</testcase>
111+
</testsuite>
112+
EOF
113+
}
114+
cleanup() {
115+
status=$?
116+
if [ ! -f reports/runtime.xml ]; then
117+
if [ "$status" -eq 0 ]; then
118+
write_runtime_report pass
119+
else
120+
write_runtime_report fail "$current_step"
121+
fi
122+
fi
123+
kubectl delete smolvm runtime-smoke --ignore-not-found=true --wait=false || true
124+
kubectl -n operator-system get pods -o wide || true
125+
kubectl -n operator-system logs deployment/operator-controller-manager --all-containers=true || true
126+
kubectl -n operator-system logs daemonset/operator-smolvm-runtime --all-containers=true || true
127+
exit "$status"
128+
}
129+
trap cleanup EXIT
76130
131+
current_step="install CRDs"
77132
make install
78-
SMOLVM_API_URL=http://127.0.0.1:8080 go run ./cmd/main.go \
79-
--metrics-bind-address=0 \
80-
--health-probe-bind-address=:8081 > manager.log 2>&1 &
81-
manager_pid=$!
82-
trap 'kubectl delete smolvm runtime-smoke --ignore-not-found=true --wait=false || true; kill $manager_pid || true; kill $serve_pid || true; cat manager.log || true; cat smolvm-serve.log || true' EXIT
133+
current_step="deploy full operator stack"
134+
make deploy IMG=example.com/smolvm-operator:e2e RUNTIME_IMG=example.com/smolvm-runtime:e2e
135+
136+
current_step="controller Deployment rollout"
137+
kubectl rollout status deployment/operator-controller-manager -n operator-system --timeout=3m
138+
current_step="runtime DaemonSet rollout"
139+
kubectl rollout status daemonset/operator-smolvm-runtime -n operator-system --timeout=5m
83140
141+
current_step="SmolVMNode readiness"
142+
node=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
143+
for i in {1..90}; do
144+
kubectl get smolvmnode "$node" -o yaml || true
145+
ready=$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)
146+
runtime_ready=$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="RuntimeReady")].status}' 2>/dev/null || true)
147+
if [ "$ready" = "True" ] && [ "$runtime_ready" = "True" ]; then
148+
break
149+
fi
150+
sleep 2
151+
done
152+
test "$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" = "True"
153+
test "$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="RuntimeReady")].status}')" = "True"
154+
test "$(kubectl get smolvmnode "$node" -o jsonpath='{.status.endpoint.podNamespace}')" = "operator-system"
155+
156+
current_step="create unpinned SmolVM"
84157
cat <<'EOF' | kubectl apply -f -
85158
apiVersion: vm.smolvm.dev/v1alpha1
86159
kind: SmolVM
@@ -94,40 +167,40 @@ jobs:
94167
memoryMiB: 128
95168
EOF
96169
97-
ready=false
98-
for i in {1..48}; do
99-
echo "--- poll $i ---"
170+
current_step="SmolVM reaches Running"
171+
for i in {1..60}; do
100172
kubectl get smolvm runtime-smoke -o yaml || true
101-
curl -fsS http://127.0.0.1:8080/api/v1/machines || true
102173
status=$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)
103174
if [ "$status" = "True" ]; then
104-
ready=true
105175
break
106176
fi
107177
sleep 10
108178
done
109-
if [ "$ready" != "true" ]; then
110-
echo "runtime-smoke did not become Ready"
111-
kubectl get smolvm runtime-smoke -o yaml || true
112-
curl -fsS http://127.0.0.1:8080/api/v1/machines || true
113-
exit 1
114-
fi
179+
test "$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.conditions[?(@.type=="Scheduled")].reason}')" = "Bound"
115180
test "$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.conditions[?(@.type=="RuntimeReady")].status}')" = "True"
116-
test "$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.conditions[?(@.type=="GuestReady")].status}')" = "Unknown"
181+
test "$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.phase}')" = "Running"
117182
183+
current_step="patch spec.running false"
118184
kubectl patch smolvm runtime-smoke --type=merge -p '{"spec":{"running":false}}'
119-
kubectl wait smolvm runtime-smoke \
120-
--for=jsonpath='{.status.phase}'=Stopped \
121-
--timeout=5m
185+
current_step="SmolVM reaches Stopped"
186+
kubectl wait smolvm runtime-smoke --for=jsonpath='{.status.phase}'=Stopped --timeout=5m
122187
188+
current_step="delete CR cleans runtime machine"
123189
machine=$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.machineName}')
190+
runtime_pod=$(kubectl -n operator-system get pod -l app.kubernetes.io/name=smolvm-runtime -o jsonpath='{.items[0].metadata.name}')
124191
kubectl delete smolvm runtime-smoke --wait=true --timeout=5m
125192
for i in {1..30}; do
126-
if ! curl -fsS http://127.0.0.1:8080/api/v1/machines | grep -q "$machine"; then
193+
if ! kubectl -n operator-system exec "$runtime_pod" -- smolvm machine ls | grep -q "$machine"; then
127194
exit 0
128195
fi
129196
sleep 2
130197
done
131198
echo "runtime machine $machine still exists after CR deletion"
132-
curl -fsS http://127.0.0.1:8080/api/v1/machines || true
199+
kubectl -n operator-system exec "$runtime_pod" -- smolvm machine ls || true
133200
exit 1
201+
- name: Upload runtime logs
202+
if: always()
203+
uses: actions/upload-artifact@v4
204+
with:
205+
name: runtime-e2e-logs
206+
path: reports/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*.dylib
88
bin/*
99
Dockerfile.cross
10+
reports/
1011

1112
# Test binary, built with `go test -c`
1213
*.test

Dockerfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ COPY go.sum go.sum
1212
RUN go mod download
1313

1414
# Copy the go source
15-
COPY cmd/main.go cmd/main.go
15+
COPY cmd/ cmd/
1616
COPY api/ api/
1717
COPY internal/controller/ internal/controller/
1818
COPY internal/smolvm/ internal/smolvm/
@@ -22,13 +22,15 @@ COPY internal/smolvm/ internal/smolvm/
2222
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
2323
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
2424
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
25-
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
25+
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go && \
26+
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o runtime-agent cmd/runtime-agent/main.go
2627

2728
# Use distroless as minimal base image to package the manager binary
2829
# Refer to https://github.com/GoogleContainerTools/distroless for more details
2930
FROM gcr.io/distroless/static:nonroot
3031
WORKDIR /
3132
COPY --from=builder /workspace/manager .
33+
COPY --from=builder /workspace/runtime-agent .
3234
USER 65532:65532
3335

3436
ENTRYPOINT ["/manager"]

Dockerfile.runtime

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
FROM golang:1.25-bookworm AS agent-builder
2+
WORKDIR /workspace
3+
COPY go.mod go.mod
4+
COPY go.sum go.sum
5+
RUN go mod download
6+
COPY cmd/ cmd/
7+
COPY api/ api/
8+
COPY internal/smolvm/ internal/smolvm/
9+
RUN CGO_ENABLED=0 go build -a -o /runtime-agent cmd/runtime-agent/main.go
10+
11+
FROM debian:bookworm-slim AS smolvm-release
12+
ARG TARGETARCH
13+
ARG SMOLVM_VERSION=0.8.1
14+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl tar && rm -rf /var/lib/apt/lists/*
15+
WORKDIR /download
16+
RUN case "${TARGETARCH}" in \
17+
amd64) SMOLVM_ARCH=x86_64 ;; \
18+
arm64) SMOLVM_ARCH=aarch64 ;; \
19+
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
20+
esac && \
21+
curl -fsSL -o smolvm.tar.gz "https://github.com/smol-machines/smolvm/releases/download/v${SMOLVM_VERSION}/smolvm-${SMOLVM_VERSION}-linux-${SMOLVM_ARCH}.tar.gz" && \
22+
mkdir -p /opt/smolvm && \
23+
tar -xzf smolvm.tar.gz -C /opt/smolvm --strip-components=1
24+
25+
FROM ubuntu:24.04
26+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends bash ca-certificates libepoxy0 libgbm1 libgcc-s1 libvirglrenderer1 && rm -rf /var/lib/apt/lists/*
27+
28+
WORKDIR /
29+
COPY --from=agent-builder /runtime-agent /runtime-agent
30+
COPY --from=smolvm-release /opt/smolvm/ /opt/smolvm/
31+
RUN chmod 0755 /runtime-agent /opt/smolvm/smolvm /opt/smolvm/smolvm-bin && \
32+
ln -sf /opt/smolvm/smolvm /usr/local/bin/smolvm && \
33+
mkdir -p /var/lib/smolvm /var/run/smolvm
34+
35+
ENTRYPOINT ["/runtime-agent"]

0 commit comments

Comments
 (0)