Skip to content

Commit a4ec87d

Browse files
committed
feat: add kubernetes local-dev environment
* Add support for grpcRoute from Kubernetes Gateway API spec * Add pkiInitJob to initialize mTLS resources * Add sshHandshake init job * Test integration with Envoy Gateway * Add keycloak integration testing with Skaffold
1 parent 25c4fde commit a4ec87d

24 files changed

Lines changed: 1246 additions & 51 deletions
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
name: helm-dev-environment
3+
description: Start up, tear down, and configure the local Kubernetes development environment for OpenShell. Uses k3d (Docker-backed k3s) + Skaffold + Helm. Covers cluster lifecycle, optional add-ons (Keycloak OIDC, Envoy Gateway), and port mappings. Trigger keywords - local k8s, local cluster, k3d, skaffold, helm dev, start cluster, stop cluster, tear down cluster, delete cluster, create cluster, helm:k3s, helm:skaffold, local dev environment, dev cluster, k8s dev, envoy gateway local, keycloak local.
4+
---
5+
6+
# Helm Dev Environment
7+
8+
Set up, run, and tear down the local Kubernetes development environment for OpenShell.
9+
The stack is: **k3d** (Docker-backed k3s) for the cluster, **Skaffold** for image builds and Helm deploys, and the **OpenShell Helm chart** (`deploy/helm/openshell/`).
10+
11+
---
12+
13+
## Prerequisites
14+
15+
- Docker Desktop (macOS) or Docker Engine (Linux) running
16+
- `mise install` completed (provides `k3d`, `kubectl`, `skaffold`, `helm`)
17+
18+
---
19+
20+
## Startup
21+
22+
### 1. Create the cluster
23+
24+
```bash
25+
mise run helm:k3s:create
26+
```
27+
28+
Creates a k3d cluster and merges its kubeconfig into the worktree-local `kubeconfig` file.
29+
Also applies base manifests (`deploy/kube/manifests/agent-sandbox.yaml`). Traefik is
30+
disabled at cluster creation time.
31+
32+
**Multi-worktree support:** the cluster name is derived from the last component of the
33+
current git branch (e.g. branch `kube-support/local-dev/tmutch` → cluster
34+
`openshell-dev-tmutch`). Each worktree therefore gets its own isolated cluster and its
35+
own `kubeconfig` file. Override with `HELM_K3S_CLUSTER_NAME` to force a specific name
36+
or share one cluster across worktrees.
37+
38+
Port mappings created at cluster time (cannot be changed without recreating):
39+
40+
| Host port | Target | Used by |
41+
|-----------|--------|---------|
42+
| `8080` | Port `80` via k3d load balancer | Envoy Gateway LoadBalancer service (`values-gateway.yaml`) |
43+
44+
Override with env vars before running `helm:k3s:create`:
45+
- `HELM_K3S_LB_HOST_PORT` (default: `8080`)
46+
47+
### 2. Deploy OpenShell
48+
49+
**Iterative dev** (rebuilds on file changes, recommended during active development):
50+
```bash
51+
mise run helm:skaffold:dev
52+
```
53+
54+
**One-shot deploy** (build once and leave running):
55+
```bash
56+
mise run helm:skaffold:run
57+
```
58+
59+
Both commands build the `gateway` and `supervisor` images and deploy the OpenShell Helm
60+
chart. The `pkiInitJob` hook runs on first install to generate mTLS secrets. Envoy Gateway is opt-in; see the Optional Add-ons section below.
61+
62+
The gateway Service uses ClusterIP. Access is via Envoy Gateway (port `8080`) or `kubectl port-forward`.
63+
64+
---
65+
66+
## Teardown
67+
68+
### Remove the Helm releases (keep cluster)
69+
70+
```bash
71+
mise run helm:skaffold:delete
72+
```
73+
74+
### Delete the cluster entirely
75+
76+
```bash
77+
mise run helm:k3s:delete
78+
```
79+
80+
This removes the k3d cluster and all resources. Kubeconfig context is left behind
81+
but will point to a deleted cluster — safe to ignore or clean up manually.
82+
83+
---
84+
85+
## Optional Add-ons
86+
87+
Each add-on requires uncommenting the corresponding `valuesFiles` entry in
88+
`deploy/helm/openshell/skaffold.yaml` before running `helm:skaffold:dev` or `helm:skaffold:run`.
89+
90+
### Envoy Gateway (Gateway API / GRPCRoute)
91+
92+
Envoy Gateway is already installed by Skaffold (the `envoy-gateway` Helm release in
93+
`skaffold.yaml`). To activate routing:
94+
95+
1. Uncomment `#- values-gateway.yaml` in `skaffold.yaml`
96+
2. Redeploy: `mise run helm:skaffold:run`
97+
3. Apply the GatewayClass: `mise run helm:gateway:apply`
98+
4. Access: `http://127.0.0.1:8080`
99+
100+
`values-gateway.yaml` creates a `Gateway` (listener on port 80, class `eg`) and a
101+
`GRPCRoute` in the `openshell` namespace. Envoy Gateway provisions a LoadBalancer
102+
service for the proxy; klipper-lb binds it to hostPort 80, reachable via the
103+
`8080:80` load balancer port mapping.
104+
105+
### Keycloak OIDC
106+
107+
One-time setup — only needed once per cluster lifetime:
108+
109+
```bash
110+
mise run keycloak:k8s:setup
111+
```
112+
113+
This deploys Keycloak (`quay.io/keycloak/keycloak:24.0`) into the `keycloak` namespace,
114+
imports the openshell realm from `scripts/keycloak-realm.json`, and prints a port-forward
115+
command for acquiring tokens from the CLI.
116+
117+
Then activate OIDC in the OpenShell Helm chart:
118+
1. Uncomment `#- values-keycloak.yaml` in `skaffold.yaml`
119+
2. Redeploy: `mise run helm:skaffold:run`
120+
121+
To remove Keycloak:
122+
```bash
123+
mise run keycloak:k8s:teardown
124+
```
125+
126+
---
127+
128+
## Cluster Lifecycle (suspend/resume)
129+
130+
Stop the cluster without losing state (faster than delete/recreate):
131+
```bash
132+
mise run helm:k3s:stop
133+
mise run helm:k3s:start
134+
```
135+
136+
Check cluster status:
137+
```bash
138+
mise run helm:k3s:status
139+
```
140+
141+
---
142+
143+
## Key Files
144+
145+
| Path | Purpose |
146+
|------|---------|
147+
| `deploy/helm/openshell/skaffold.yaml` | Skaffold config — images, Helm releases, values overlays |
148+
| `deploy/helm/openshell/values.yaml` | Default Helm values |
149+
| `deploy/helm/openshell/values-skaffold.yaml` | Dev overrides (image pull policy, local image names) |
150+
| `deploy/helm/openshell/values-gateway.yaml` | Envoy Gateway GRPCRoute + Gateway overlay |
151+
| `deploy/helm/openshell/values-keycloak.yaml` | Keycloak OIDC overlay |
152+
| `deploy/kube/manifests/envoy-gateway-openshell.yaml` | GatewayClass for Envoy Gateway (`mise run helm:gateway:apply`) |
153+
| `tasks/scripts/helm-k3s-local.sh` | k3d cluster create/delete/start/stop/status |
154+
| `tasks/scripts/keycloak-k8s-setup.sh` | Keycloak deploy + realm import |

‎deploy/docker/Dockerfile.images‎

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
#
1414
# Rust binaries are built natively before the image build and staged at:
1515
# deploy/docker/.build/prebuilt-binaries/<arch>/openshell-{gateway,sandbox}
16+
#
17+
# For local dev (Skaffold), pass --build-arg BUILD_FROM_SOURCE=1 to compile
18+
# binaries inside Docker instead. BuildKit only executes the selected binary
19+
# staging stage, so missing prebuilt files do not cause a build failure.
1620

1721
# Pin by tag AND manifest-list digest to prevent silent upstream republishes
1822
# from breaking the build. Update both when bumping k3s versions.
@@ -22,22 +26,67 @@ ARG K3S_DIGEST=sha256:4607083d3cac07e1ccde7317297271d13ed5f60f35a78f33fcef84858a
2226
ARG K9S_VERSION=v0.50.18
2327
ARG HELM_VERSION=v3.17.3
2428
ARG NVIDIA_CONTAINER_TOOLKIT_VERSION=1.18.2-1
29+
# Controls binary source: 0 = prebuilt (release), 1 = compile in Docker (local dev).
30+
# Must be declared here (global scope) so it can be used in FROM instructions below.
31+
ARG BUILD_FROM_SOURCE=0
32+
33+
# ---------------------------------------------------------------------------
34+
# Optional in-Docker Rust build (BUILD_FROM_SOURCE=1, local dev only)
35+
# ---------------------------------------------------------------------------
36+
FROM rust:1.95.0-slim-bookworm AS rust-builder
37+
38+
RUN apt-get update && apt-get install -y --no-install-recommends \
39+
build-essential \
40+
cmake \
41+
pkg-config \
42+
libssl-dev \
43+
ca-certificates \
44+
&& rm -rf /var/lib/apt/lists/*
45+
46+
WORKDIR /build
47+
48+
COPY Cargo.toml Cargo.lock ./
49+
COPY crates/ crates/
50+
COPY proto/ proto/
51+
52+
RUN --mount=type=cache,target=/usr/local/cargo/registry \
53+
--mount=type=cache,target=/build/target \
54+
cargo build --release \
55+
--features "openshell-core/dev-settings" \
56+
--bin openshell-gateway \
57+
--bin openshell-sandbox \
58+
&& mkdir -p /build/out \
59+
&& install -m 0755 target/release/openshell-gateway /build/out/openshell-gateway \
60+
&& install -m 0755 target/release/openshell-sandbox /build/out/openshell-sandbox
2561

2662
# ---------------------------------------------------------------------------
2763
# Per-arch binary stages
2864
# ---------------------------------------------------------------------------
29-
FROM scratch AS gateway-binary
65+
66+
# Prebuilt path (release default, BUILD_FROM_SOURCE=0)
67+
FROM scratch AS gateway-binary-0
3068
ARG TARGETARCH
3169
# --chmod=755 preserves the executable bit through actions/upload-artifact +
3270
# download-artifact, which strip exec perms during the roundtrip.
3371
COPY --chmod=755 deploy/docker/.build/prebuilt-binaries/${TARGETARCH}/openshell-gateway /build/out/openshell-gateway
3472

35-
FROM scratch AS supervisor-binary
73+
# Source-built path (local dev, BUILD_FROM_SOURCE=1)
74+
FROM rust-builder AS gateway-binary-1
75+
76+
FROM gateway-binary-${BUILD_FROM_SOURCE} AS gateway-binary
77+
78+
# Prebuilt path (release default, BUILD_FROM_SOURCE=0)
79+
FROM scratch AS supervisor-binary-0
3680
ARG TARGETARCH
3781
# --chmod=755 preserves the executable bit through actions/upload-artifact +
3882
# download-artifact, which strip exec perms during the roundtrip.
3983
COPY --chmod=755 deploy/docker/.build/prebuilt-binaries/${TARGETARCH}/openshell-sandbox /build/out/openshell-sandbox
4084

85+
# Source-built path (local dev, BUILD_FROM_SOURCE=1)
86+
FROM rust-builder AS supervisor-binary-1
87+
88+
FROM supervisor-binary-${BUILD_FROM_SOURCE} AS supervisor-binary
89+
4190
# Minimal extraction stage for fast-deploy: exports only the supervisor
4291
# binary (~20-40 MB) instead of the entire build environment (~968 MB).
4392
FROM scratch AS supervisor-output

‎deploy/helm/openshell/.helmignore‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,10 @@
1616
.idea/
1717
*.tmproj
1818
.vscode/
19+
20+
# Ignore development files
21+
skaffold.yaml
22+
values-keycloak.yaml
23+
values-ingress.yaml
24+
values-gateway.yaml
25+
values-skaffold.yaml

‎deploy/helm/openshell/Chart.yaml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ apiVersion: v2
55
name: openshell
66
description: runtime environment for autonomous agents
77
type: application
8-
version: 0.1.0
9-
appVersion: "0.1.0"
8+
# Updated to the release version by CI. The appVersion doubles as the default
9+
# image tag (image.tag defaults to appVersion when empty), so a released chart
10+
# automatically pulls the matching gateway and supervisor images.
11+
version: 0.0.0
12+
appVersion: "0.0.0"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# Local dev: builds gateway + supervisor images using Dockerfile.images with
5+
# BUILD_FROM_SOURCE=1, which compiles Rust binaries inside Docker without
6+
# requiring pre-staged artifacts.
7+
#
8+
# Run from repo root:
9+
# skaffold dev -f deploy/helm/openshell/skaffold.yaml
10+
#
11+
# See https://skaffold.dev/docs/deployers/helm/ (setValueTemplates, IMAGE_* fields).
12+
apiVersion: skaffold/v4beta14
13+
kind: Config
14+
metadata:
15+
name: openshell
16+
build:
17+
local:
18+
push: false
19+
tagPolicy:
20+
gitCommit: {}
21+
artifacts:
22+
- image: openshell/gateway
23+
context: ../../..
24+
custom:
25+
buildCommand: |
26+
docker buildx build \
27+
--build-arg BUILD_FROM_SOURCE=1 \
28+
--target gateway \
29+
--tag "$IMAGE" \
30+
--load \
31+
--file deploy/docker/Dockerfile.images \
32+
.
33+
dependencies:
34+
paths:
35+
- Cargo.toml
36+
- Cargo.lock
37+
- crates/**
38+
- proto/**
39+
- deploy/docker/Dockerfile.images
40+
- crates/openshell-server/migrations/**
41+
- image: openshell/supervisor
42+
context: ../../..
43+
custom:
44+
buildCommand: |
45+
docker buildx build \
46+
--build-arg BUILD_FROM_SOURCE=1 \
47+
--target supervisor \
48+
--tag "$IMAGE" \
49+
--load \
50+
--file deploy/docker/Dockerfile.images \
51+
.
52+
dependencies:
53+
paths:
54+
- Cargo.toml
55+
- Cargo.lock
56+
- crates/**
57+
- proto/**
58+
- deploy/docker/Dockerfile.images
59+
deploy:
60+
helm:
61+
releases:
62+
# Envoy Gateway — Kubernetes Gateway API implementation.
63+
# Installs the Gateway API CRDs and the "eg" GatewayClass.
64+
# Required when grpcRoute.enabled is true in the openshell release.
65+
#- name: envoy-gateway
66+
# remoteChart: oci://docker.io/envoyproxy/gateway-helm
67+
# version: v1.7.2
68+
# namespace: envoy-gateway-system
69+
# createNamespace: true
70+
# # wait ensures Gateway API CRDs are registered before the openshell
71+
# # release attempts to create Gateway and HTTPRoute resources.
72+
# wait: true
73+
- name: openshell
74+
chartPath: .
75+
namespace: openshell
76+
createNamespace: true
77+
valuesFiles:
78+
- values.yaml
79+
- values-skaffold.yaml
80+
# To enable OIDC with a local Keycloak instance, run the one-time
81+
# setup task first, then uncomment the line below:
82+
# mise run keycloak:k8s:setup
83+
#- values-keycloak.yaml
84+
# To enable the Gateway API HTTPRoute (requires Envoy Gateway above):
85+
#- values-gateway.yaml
86+
setValueTemplates:
87+
image.repository: '{{.IMAGE_REPO_openshell_gateway}}'
88+
image.tag: '{{.IMAGE_TAG_openshell_gateway}}'
89+
supervisor.image.repository: '{{.IMAGE_REPO_openshell_supervisor}}'
90+
supervisor.image.tag: '{{.IMAGE_TAG_openshell_supervisor}}'

‎deploy/helm/openshell/templates/_helpers.tpl‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,26 @@ Create the name of the service account to use
5858
{{- default "default" .Values.serviceAccount.name }}
5959
{{- end }}
6060
{{- end }}
61+
62+
{{/*
63+
Gateway image reference. Uses image.tag when set; falls back to .Chart.AppVersion
64+
so a released chart automatically pulls the matching image without extra overrides.
65+
*/}}
66+
{{- define "openshell.image" -}}
67+
{{- printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) }}
68+
{{- end }}
69+
70+
{{/*
71+
Supervisor image reference. Same appVersion fallback as openshell.image so
72+
the supervisor and gateway images stay in sync across releases.
73+
*/}}
74+
{{- define "openshell.supervisorImage" -}}
75+
{{- printf "%s:%s" .Values.supervisor.image.repository (.Values.supervisor.image.tag | default .Chart.AppVersion) }}
76+
{{- end }}
77+
78+
{{/*
79+
Namespaced Issuer (selfSigned) for cert-manager CA bootstrap.
80+
*/}}
81+
{{- define "openshell.issuerSelfSigned" -}}
82+
{{- printf "%s-selfsigned" (include "openshell.fullname" .) | trunc 63 | trimSuffix "-" }}
83+
{{- end }}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
{{- if and .Values.grpcRoute.enabled .Values.grpcRoute.gateway.create }}
5+
apiVersion: gateway.networking.k8s.io/v1
6+
kind: Gateway
7+
metadata:
8+
name: {{ default (include "openshell.fullname" .) .Values.grpcRoute.gateway.name }}
9+
namespace: {{ .Release.Namespace }}
10+
labels:
11+
{{- include "openshell.labels" . | nindent 4 }}
12+
spec:
13+
gatewayClassName: {{ .Values.grpcRoute.gateway.className }}
14+
listeners:
15+
- name: http
16+
port: {{ .Values.grpcRoute.gateway.listener.port }}
17+
protocol: {{ .Values.grpcRoute.gateway.listener.protocol }}
18+
allowedRoutes:
19+
namespaces:
20+
from: {{ .Values.grpcRoute.gateway.listener.allowedRoutes }}
21+
{{- end }}

0 commit comments

Comments
 (0)