Skip to content

Commit a6b0905

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

27 files changed

Lines changed: 2017 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: 72 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,55 +37,76 @@ 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+
kind load docker-image example.com/smolvm-operator:e2e --name runtime
79+
kind load docker-image example.com/smolvm-runtime:e2e --name runtime
80+
- name: Deploy full stack and run VM lifecycle
6381
run: |
6482
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
83+
make install
84+
make deploy IMG=example.com/smolvm-operator:e2e RUNTIME_IMG=example.com/smolvm-runtime:e2e
7185
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
86+
cleanup() {
87+
kubectl delete smolvm runtime-smoke --ignore-not-found=true --wait=false || true
88+
kubectl -n operator-system get pods -o wide || true
89+
kubectl -n operator-system logs deployment/operator-controller-manager --all-containers=true || true
90+
kubectl -n operator-system logs daemonset/operator-smolvm-runtime --all-containers=true || true
91+
}
92+
trap cleanup EXIT
7693
77-
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
94+
kubectl rollout status deployment/operator-controller-manager -n operator-system --timeout=3m
95+
kubectl rollout status daemonset/operator-smolvm-runtime -n operator-system --timeout=5m
96+
97+
node=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
98+
for i in {1..90}; do
99+
kubectl get smolvmnode "$node" -o yaml || true
100+
ready=$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)
101+
runtime_ready=$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="RuntimeReady")].status}' 2>/dev/null || true)
102+
if [ "$ready" = "True" ] && [ "$runtime_ready" = "True" ]; then
103+
break
104+
fi
105+
sleep 2
106+
done
107+
test "$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" = "True"
108+
test "$(kubectl get smolvmnode "$node" -o jsonpath='{.status.conditions[?(@.type=="RuntimeReady")].status}')" = "True"
109+
test "$(kubectl get smolvmnode "$node" -o jsonpath='{.status.endpoint.podNamespace}')" = "operator-system"
83110
84111
cat <<'EOF' | kubectl apply -f -
85112
apiVersion: vm.smolvm.dev/v1alpha1
@@ -94,40 +121,36 @@ jobs:
94121
memoryMiB: 128
95122
EOF
96123
97-
ready=false
98-
for i in {1..48}; do
99-
echo "--- poll $i ---"
124+
for i in {1..60}; do
100125
kubectl get smolvm runtime-smoke -o yaml || true
101-
curl -fsS http://127.0.0.1:8080/api/v1/machines || true
102126
status=$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)
103127
if [ "$status" = "True" ]; then
104-
ready=true
105128
break
106129
fi
107130
sleep 10
108131
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
132+
test "$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.conditions[?(@.type=="Scheduled")].reason}')" = "Bound"
115133
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"
134+
test "$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.phase}')" = "Running"
117135
118136
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
137+
kubectl wait smolvm runtime-smoke --for=jsonpath='{.status.phase}'=Stopped --timeout=5m
122138
123139
machine=$(kubectl get smolvm runtime-smoke -o jsonpath='{.status.machineName}')
140+
runtime_pod=$(kubectl -n operator-system get pod -l app.kubernetes.io/name=smolvm-runtime -o jsonpath='{.items[0].metadata.name}')
124141
kubectl delete smolvm runtime-smoke --wait=true --timeout=5m
125142
for i in {1..30}; do
126-
if ! curl -fsS http://127.0.0.1:8080/api/v1/machines | grep -q "$machine"; then
143+
if ! kubectl -n operator-system exec "$runtime_pod" -- smolvm machine ls | grep -q "$machine"; then
127144
exit 0
128145
fi
129146
sleep 2
130147
done
131148
echo "runtime machine $machine still exists after CR deletion"
132-
curl -fsS http://127.0.0.1:8080/api/v1/machines || true
149+
kubectl -n operator-system exec "$runtime_pod" -- smolvm machine ls || true
133150
exit 1
151+
- name: Upload runtime logs
152+
if: always()
153+
uses: actions/upload-artifact@v4
154+
with:
155+
name: runtime-e2e-logs
156+
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 debian:bookworm-slim
26+
RUN apt-get update && 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"]

Makefile

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
# Image URL to use all building/pushing image targets
33
IMG ?= controller:latest
4+
RUNTIME_IMG ?= smolvm-runtime:latest
5+
SMOLVM_VERSION ?= 0.8.1
46
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
57
ENVTEST_K8S_VERSION = 1.29.0
68

@@ -64,11 +66,21 @@ vet: ## Run go vet against code.
6466
test: manifests generate fmt vet envtest ## Run tests.
6567
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out
6668

69+
.PHONY: test-report
70+
test-report: manifests generate fmt vet envtest gotestsum ## Run tests and write JUnit/coverage reports under reports/.
71+
mkdir -p reports
72+
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GOTESTSUM) --format testname --junitfile reports/unit.xml -- -coverprofile reports/cover.out $$(go list ./... | grep -v /e2e)
73+
6774
# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors.
6875
.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up.
6976
test-e2e:
7077
go test ./test/e2e/ -v -ginkgo.v
7178

79+
.PHONY: test-e2e-report
80+
test-e2e-report: gotestsum ## Run e2e tests and write a JUnit report under reports/.
81+
mkdir -p reports
82+
$(GOTESTSUM) --format testname --junitfile reports/e2e.xml -- ./test/e2e/ -v -ginkgo.v
83+
7284
.PHONY: lint
7385
lint: golangci-lint ## Run golangci-lint linter & yamllint
7486
$(GOLANGCI_LINT) run
@@ -82,6 +94,7 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
8294
.PHONY: build
8395
build: manifests generate fmt vet ## Build manager binary.
8496
go build -o bin/manager cmd/main.go
97+
go build -o bin/runtime-agent cmd/runtime-agent/main.go
8598

8699
.PHONY: run
87100
run: manifests generate fmt vet ## Run a controller from your host.
@@ -91,12 +104,17 @@ run: manifests generate fmt vet ## Run a controller from your host.
91104
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
92105
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
93106
.PHONY: docker-build
94-
docker-build: ## Build docker image with the manager.
107+
docker-build: ## Build docker image with the manager and runtime-agent.
95108
$(CONTAINER_TOOL) build -t ${IMG} .
96109

110+
.PHONY: docker-build-runtime
111+
docker-build-runtime: ## Build the smolvm runtime image, including the released smolvm binary.
112+
$(CONTAINER_TOOL) build -f Dockerfile.runtime --build-arg SMOLVM_VERSION=$(SMOLVM_VERSION) -t ${RUNTIME_IMG} .
113+
97114
.PHONY: docker-push
98-
docker-push: ## Push docker image with the manager.
115+
docker-push: ## Push docker images.
99116
$(CONTAINER_TOOL) push ${IMG}
117+
$(CONTAINER_TOOL) push ${RUNTIME_IMG}
100118

101119
# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
102120
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
@@ -123,6 +141,7 @@ build-installer: manifests generate kustomize ## Generate a consolidated YAML wi
123141
fi
124142
echo "---" >> dist/install.yaml # Add a document separator before appending
125143
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
144+
cd config/runtime && $(KUSTOMIZE) edit set image smolvm-runtime=${RUNTIME_IMG}
126145
$(KUSTOMIZE) build config/default >> dist/install.yaml
127146

128147
##@ Deployment
@@ -142,6 +161,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
142161
.PHONY: deploy
143162
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
144163
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
164+
cd config/runtime && $(KUSTOMIZE) edit set image smolvm-runtime=${RUNTIME_IMG}
145165
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
146166

147167
.PHONY: undeploy
@@ -161,12 +181,14 @@ KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION)
161181
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION)
162182
ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION)
163183
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION)
184+
GOTESTSUM ?= $(LOCALBIN)/gotestsum-$(GOTESTSUM_VERSION)
164185

165186
## Tool Versions
166187
KUSTOMIZE_VERSION ?= v5.3.0
167188
CONTROLLER_TOOLS_VERSION ?= v0.19.0
168189
ENVTEST_VERSION ?= latest
169190
GOLANGCI_LINT_VERSION ?= v1.54.2
191+
GOTESTSUM_VERSION ?= v1.13.0
170192

171193
.PHONY: kustomize
172194
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
@@ -188,6 +210,11 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
188210
$(GOLANGCI_LINT): $(LOCALBIN)
189211
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION})
190212

213+
.PHONY: gotestsum
214+
gotestsum: $(GOTESTSUM) ## Download gotestsum locally if necessary.
215+
$(GOTESTSUM): $(LOCALBIN)
216+
$(call go-install-tool,$(GOTESTSUM),gotest.tools/gotestsum,${GOTESTSUM_VERSION})
217+
191218
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
192219
# $1 - target path with name of binary (ideally with version)
193220
# $2 - package url which can be installed

0 commit comments

Comments
 (0)