Skip to content

Commit f9da85c

Browse files
committed
chore: merge main into sandbox image update
# Conflicts: # crates/openshell-sandbox-backend/src/boundary_protocol.rs # e2e/rust/tests/host_gateway_alias.rs
2 parents b8f5b46 + 96c08f1 commit f9da85c

142 files changed

Lines changed: 10794 additions & 2463 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/helm-dev-environment/SKILL.md

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,31 @@ trace-context annotation. The same command exposes OTLP/gRPC on
113113
`gateway:vm` tasks detect the collector listener at startup and enable trace
114114
export only while it is reachable.
115115

116-
**HA test deploy** (two gateway replicas + external PostgreSQL Secret): uncomment
117-
`#- ci/values-high-availability.yaml` in `deploy/helm/openshell/skaffold.yaml`,
118-
create the Secret named `openshell-ha-pg` with a `uri` key, then run
119-
`mise run helm:skaffold:run` or `mise run helm:skaffold:dev`.
116+
The Skaffold profile for HA reverse-proxy development is available from
117+
`deploy/helm/openshell/`:
118+
119+
```bash
120+
# Two gateway replicas + external PostgreSQL Secret + Envoy Gateway + Gateway API route.
121+
KUBECONFIG=../../../kubeconfig skaffold run -p high-availability
122+
```
123+
124+
The `high-availability` profile expects a Secret named `openshell-ha-pg` in the `openshell`
125+
namespace with a `uri` key. For local manual testing, either create your own
126+
PostgreSQL Secret or use the e2e PostgreSQL fixture manifest in
127+
`e2e/kubernetes/postgres-fixture.yaml`.
128+
129+
For the `high-availability` profile, return to the repository root and apply the
130+
GatewayClass and BackendTrafficPolicy manifest after Skaffold has installed
131+
Envoy Gateway:
132+
133+
```bash
134+
KUBECONFIG=kubeconfig mise run helm:gateway:apply
135+
```
136+
137+
The BackendTrafficPolicy disables Envoy request and stream-duration timeouts for
138+
OpenShell's `GRPCRoute`. Keep that policy in `deploy/kube/manifests/envoy-gateway-openshell.yaml`,
139+
not in the Helm chart; it is required for long-lived gRPC create/watch/exec/relay
140+
streams during gateway rollouts and scale events.
120141

121142
### TLS behaviour
122143

@@ -187,23 +208,85 @@ but will point to a deleted cluster — safe to ignore or clean up manually.
187208

188209
## Optional Add-ons
189210

190-
Each add-on requires uncommenting the corresponding `valuesFiles` entry in
191-
`deploy/helm/openshell/skaffold.yaml` before running `helm:skaffold:dev` or `helm:skaffold:run`.
211+
Some add-ons can be enabled by uncommenting values in `skaffold.yaml`, but prefer
212+
the dedicated Skaffold profiles when they exist. Profiles avoid leaving local
213+
manual edits in the worktree.
192214

193215
### Envoy Gateway (Gateway API / GRPCRoute)
194216

195-
Envoy Gateway is already installed by Skaffold (the `envoy-gateway` Helm release in
196-
`skaffold.yaml`). To activate routing:
217+
Use the `high-availability` Skaffold profile for HA reverse-proxy testing. The
218+
profile intentionally includes Envoy Gateway so multi-replica behavior is
219+
exercised through the same Gateway API path used by reverse-proxy deployments:
197220

198-
1. Uncomment `#- values-gateway.yaml` in `skaffold.yaml`
199-
2. Redeploy: `mise run helm:skaffold:run`
200-
3. Apply the GatewayClass: `mise run helm:gateway:apply`
201-
4. Access: `http://127.0.0.1:8080`
221+
```bash
222+
cd deploy/helm/openshell
223+
KUBECONFIG=../../../kubeconfig skaffold run -p high-availability
224+
cd ../../..
225+
KUBECONFIG=kubeconfig mise run helm:gateway:apply
226+
```
227+
228+
`values-gateway.yaml` creates a `Gateway` (listener on port 80, class `eg`) and
229+
`GRPCRoute` in the `openshell` namespace. The `high-availability` profile
230+
installs the Envoy Gateway Helm chart and layers both
231+
`values-high-availability.yaml` and `values-gateway.yaml` onto the OpenShell
232+
release.
233+
234+
`deploy/kube/manifests/envoy-gateway-openshell.yaml` creates:
235+
236+
- `GatewayClass/eg`
237+
- `BackendTrafficPolicy/openshell-grpc-timeouts`
238+
239+
The Envoy Gateway proxy Service is usually exposed through the k3d load balancer
240+
at `http://127.0.0.1:8080`. If the cluster was created with a different
241+
`HELM_K3S_LB_HOST_PORT`, use that host port instead.
242+
243+
For manual tests against an existing cluster, prefer forwarding the Envoy proxy
244+
Service rather than `svc/openshell`. That keeps client traffic on the same path
245+
as a real reverse proxy while gateway pods rotate behind it:
246+
247+
```bash
248+
KUBECONFIG=kubeconfig kubectl get svc -A \
249+
-l gateway.envoyproxy.io/owning-gateway-name=openshell
250+
KUBECONFIG=kubeconfig kubectl -n <envoy-service-namespace> port-forward \
251+
svc/<envoy-service-name> 8080:80
252+
openshell gateway add http://127.0.0.1:8080 --name openshell --local
253+
```
254+
255+
When running e2e tests manually through Envoy, register gateway metadata (as
256+
above) instead of relying only on `OPENSHELL_GATEWAY_ENDPOINT`; some tests call
257+
`openshell gateway info` and expect metadata for the active gateway.
258+
259+
### Kubernetes E2E Notes
260+
261+
Use `mise run e2e:kubernetes` for the standard Helm-backed Kubernetes suite.
262+
The kube e2e wrapper creates only one port-forward, to `svc/openshell`; it no
263+
longer forwards the unauthenticated health listener or runs a `/readyz` e2e
264+
target. `/readyz` remains covered by server unit/integration tests.
265+
266+
Use `mise run e2e:kubernetes:ha-rebalancing` for full-suite HA coverage. The
267+
task creates an external PostgreSQL fixture, installs Envoy Gateway, applies
268+
`deploy/kube/manifests/envoy-gateway-openshell.yaml`, enables the chart
269+
`GRPCRoute`, and runs the full Kubernetes e2e suite, including
270+
`kubernetes_ha_rebalancing`. That coverage validates sandbox create/watch and
271+
exec through the Envoy proxy while gateway replicas scale up, scale down, and
272+
rotate. It also keeps a long-running sandbox alive and runs upload/download
273+
operations while gateway pods roll, so file sync exercises the same relay retry
274+
path as interactive sessions.
275+
276+
If you reuse an existing Skaffold cluster for the full kube suite, make sure the
277+
chart has `server.hostGatewayIP` set so sandbox pods can resolve
278+
`host.openshell.internal` back to the test host. The e2e wrapper detects this on
279+
chart installs; manual reuse may require:
280+
281+
```bash
282+
HOST_GATEWAY_IP="${OPENSHELL_E2E_HOST_GATEWAY_IP:?set host gateway IP}"
283+
KUBECONFIG=kubeconfig helm upgrade openshell deploy/helm/openshell \
284+
--namespace openshell --reuse-values \
285+
--set "server.hostGatewayIP=${HOST_GATEWAY_IP}" \
286+
--wait --timeout 5m
287+
```
202288

203-
`values-gateway.yaml` creates a `Gateway` (listener on port 80, class `eg`) and a
204-
`GRPCRoute` in the `openshell` namespace. Envoy Gateway provisions a LoadBalancer
205-
service for the proxy; klipper-lb binds it to hostPort 80, reachable via the
206-
`8080:80` load balancer port mapping.
289+
Use the IP that pods in that cluster use to reach listeners on the test host.
207290

208291
### BackendTLSPolicy (end-to-end TLS)
209292

@@ -356,6 +439,6 @@ for dependencies still declared in `Chart.yaml`.
356439
| `deploy/helm/openshell/ci/values-spire-stack.yaml` | SPIRE hardened chart values for local dev |
357440
| `deploy/helm/openshell/ci/values-tls-disabled.yaml` | Lint-only: TLS + auth disabled (reverse-proxy edge termination) |
358441
| `deploy/helm/openshell/ci/values-credential-driver-vault.yaml` | Vault credential-driver validation overlay with HTTPS and private-CA trust |
359-
| `deploy/kube/manifests/envoy-gateway-openshell.yaml` | GatewayClass for Envoy Gateway (`mise run helm:gateway:apply`) |
442+
| `deploy/kube/manifests/envoy-gateway-openshell.yaml` | GatewayClass and BackendTrafficPolicy for Envoy Gateway (`mise run helm:gateway:apply`) |
360443
| `tasks/scripts/helm-k3s-local.sh` | k3d cluster create/delete/start/stop/status |
361444
| `tasks/scripts/keycloak-k8s-setup.sh` | Keycloak deploy, realm import, and development TLS trust anchor |

.github/workflows/branch-e2e.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,9 @@ jobs:
426426
job-name: Kubernetes HA E2E (Rust smoke)
427427
extra-helm-values: deploy/helm/openshell/ci/values-high-availability.yaml
428428
external-postgres-secret: openshell-ha-pg
429+
test-name: kubernetes_ha_rebalancing
430+
kubernetes-features: e2e,e2e-host-gateway,e2e-kubernetes,e2e-kubernetes-ha
431+
use-envoy-gateway: true
429432
conformance-artifact-prefix: openshell-conformance
430433

431434
kubernetes-credential-drivers-e2e:

.github/workflows/e2e-kubernetes-test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ on:
5252
required: false
5353
type: string
5454
default: ""
55+
use-envoy-gateway:
56+
description: "Route the test through Envoy Gateway instead of a direct gateway Service port-forward"
57+
required: false
58+
type: boolean
59+
default: false
5560
mise-version:
5661
description: "mise version to install on the bare Kubernetes e2e runner"
5762
required: false
@@ -140,6 +145,7 @@ jobs:
140145
OPENSHELL_E2E_KUBE_EXTERNAL_POSTGRES_SECRET: ${{ inputs.external-postgres-secret }}
141146
OPENSHELL_E2E_KUBE_TEST: ${{ inputs.test-name }}
142147
OPENSHELL_E2E_KUBERNETES_FEATURES: ${{ inputs.kubernetes-features }}
148+
OPENSHELL_E2E_KUBE_USE_ENVOY: ${{ inputs.use-envoy-gateway && '1' || '0' }}
143149
IMAGE_TAG: ${{ inputs.image-tag }}
144150
OPENSHELL_REGISTRY: ghcr.io/nvidia/openshell
145151
E2E_TASK: ${{ inputs.e2e-task }}

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

architecture/compute-runtimes.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,14 @@ shortly after carrying a stale `Provisioning` or `Unknown` backend phase. The
9898
composition rule treats a connected session as the stronger signal and keeps `Ready`
9999
in that case, preventing a lagging snapshot from undoing the session-driven promotion.
100100

101-
**Known HA limitation:** Supervisor sessions are process-local while the public
102-
sandbox phase is shared. A replica that reconciles a driver snapshot without owning
103-
the active supervisor session can demote the shared phase to `Provisioning`. The
104-
session-owning replica may not receive another connection event to restore `Ready`,
105-
so a usable sandbox can remain unavailable through the public phase gate. Reliable
106-
HA readiness requires persisted or leased supervisor presence plus routing to the
107-
session-owning replica. That work is deferred to GitHub issue #1868. Until then,
108-
deployments that require reliable readiness composition must run a single gateway
109-
replica.
101+
**HA session composition:** Live relay handles remain process-local, while the
102+
session-owning gateway publishes a short-lived owner record in shared PostgreSQL.
103+
Driver reconciliation treats a fresh local or remote owner as connected, so a
104+
non-owner replica cannot demote the shared sandbox phase merely because it lacks the
105+
in-memory stream. Session-bound requests are forwarded to the owning gateway; a
106+
supervisor reconnect publishes a higher connection epoch before stale-session cleanup
107+
can demote readiness. Multi-replica deployments therefore require shared PostgreSQL
108+
and the gateway peer Service configured by the Helm chart.
110109

111110
**Extension point:** Driver-reported readiness is a capability, not an
112111
operator-configurable hook. A driver may enable it only when it owns workload
@@ -119,9 +118,12 @@ The capability RPC reports driver identity, version, and the default sandbox
119118
image used by the gateway. GPU availability stays driver-local and is validated
120119
when a sandbox create request asks for GPU resources.
121120

122-
The gateway records driver identity and version from the startup capability
123-
response. Elevated gateway info reports that initialized driver snapshot instead
124-
of re-querying drivers on each request.
121+
The gateway sends its common extension peer metadata with the startup capability
122+
request. The driver validates that metadata before responding, and the gateway
123+
rejects a driver whose protocol major or capability requirements are
124+
incompatible. It records the negotiated protocol, implementation identity and
125+
version, capability sets, and typed resource support once. Elevated gateway info
126+
reports that immutable snapshot instead of re-querying drivers on each request.
125127

126128
## Compiled Driver Selection
127129

architecture/gateway.md

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,70 @@ authenticated sandbox ID with any sandbox ID or name resolved from the request.
354354
Supervisor control and relay streams require a matching sandbox principal before
355355
the gateway registers the session or bridges relay bytes.
356356

357+
## HA Supervisor Ownership
358+
359+
In multi-replica Kubernetes deployments, every gateway pod can accept client
360+
RPCs, but a sandbox supervisor maintains one active stream to one gateway
361+
replica at a time. The connected replica publishes a short-lived supervisor
362+
owner record in the shared Postgres object store with its replica id, peer DNS
363+
endpoint, supervisor instance id, and connection epoch. Ownership does not move
364+
because another gateway receives a client request. It changes only when the
365+
supervisor opens a new control stream, usually after the previous owner pod is
366+
terminated or the stream breaks. A reconnect from the same supervisor instance
367+
with a newer epoch can supersede the previous owner before the TTL expires, and
368+
heartbeats from the active connection renew that current owner record.
369+
Cleanup from an older connection checks the shared owner record before and
370+
after changing sandbox readiness. It cannot demote a sandbox after a newer
371+
replica has published replacement ownership.
372+
373+
Session-bound operations such as exec, TCP forwarding, file sync, and sandbox
374+
service routing first check the local session registry. If the supervisor is
375+
owned by another gateway replica, the serving gateway opens an internal
376+
`PeerRelay` stream to that owner and asks it to open the supervisor relay. This
377+
keeps client traffic working when a Kubernetes Service routes the client to a
378+
non-owner gateway pod. If a peer owner is stale or unreachable during a rollout,
379+
the serving gateway retries ownership lookup until the normal relay wait
380+
deadline. Each retry re-reads the owner record, so a supervisor reconnect or
381+
heartbeat can surface a new owner; if no fresh reachable owner appears before
382+
the deadline, the client operation fails rather than electing an owner itself.
383+
Provider-readiness reports, endpoint-status reports, and provider-status reads
384+
also follow the durable owner record through unary peer RPCs. The owning replica
385+
validates the current supervisor session and keeps the in-memory evidence; a
386+
non-owner never accepts evidence from a stale local session or projects a
387+
remote session as disconnected.
388+
389+
Nothing redistributes established sessions, so after a rolling restart the last
390+
surviving replica holds most sessions and a new replica serves none until
391+
sandboxes reconnect. That skew decays only as sandboxes churn. Client traffic
392+
stays correct throughout because a non-owner relays to the owner.
393+
394+
File upload and download use tar-over-SSH through the same relay path. A gateway
395+
pod termination drops the active SSH proxy byte stream, so the CLI retries the
396+
whole sync operation with a fresh SSH session instead of attempting mid-stream
397+
resume.
398+
399+
Gateway peer RPCs authenticate with Kubernetes ServiceAccount identity rather
400+
than a shared secret. Helm mounts a projected, pod-bound token with audience
401+
`openshell-gateway-peer`; the receiving gateway validates it through
402+
TokenReview, checks the live pod UID and chart selector labels, and authorizes
403+
only the internal peer RPC methods. When gateway TLS is enabled, peer clients
404+
also trust the chart CA, present the chart-generated client certificate for
405+
mTLS, and verify the stable gateway Service DNS name even when connecting to a
406+
Deployment pod IP.
407+
408+
`WatchSandbox` uses the local update bus for same-replica writes. On
409+
multi-replica backends one shared poller per gateway observes resource-version
410+
changes made by other replicas and feeds that bus for all local watchers,
411+
avoiding a database poll per client stream. SQLite deployments do not run the
412+
poller because they are single-replica and the local bus already sees every
413+
write.
414+
415+
Mutations whose invariants span sandbox, provider-profile, policy, or provider
416+
records take a process-local mutex and a shared PostgreSQL advisory lock. The
417+
database session remains dedicated to the request and closes when the guard is
418+
dropped, which releases the lock on normal completion, cancellation, or error.
419+
SQLite deployments use only the local mutex because they are single-replica.
420+
357421
## API Surface
358422

359423
The gateway API is organized around platform objects and operational streams:
@@ -753,7 +817,7 @@ migrations backfill existing rows with version 1.
753817
Provider profile imports, updates, and deletes hold the sandbox synchronization
754818
guard while checking attached-sandbox dynamic token grant ambiguity or in-use
755819
state and writing the profile record. Sandbox creation with initial providers and
756-
sandbox provider attach/detach use the same guard, so one gateway process cannot
820+
sandbox provider attach/detach use the same guard, so gateway replicas cannot
757821
interleave a profile mutation with a sandbox provider-set mutation that would
758822
leave an ambiguous final dynamic-token state or a deleted custom profile that is
759823
still referenced by a sandbox.
@@ -782,6 +846,15 @@ resolution and again by the sandbox placeholder resolver. This keeps expired
782846
credentials from resolving even when a running sandbox still has retained
783847
placeholder generations from an earlier provider credential snapshot.
784848

849+
All gateway-owned extension registries negotiate the same peer metadata envelope
850+
before accepting work. Compute drivers, credential drivers, gateway interceptors,
851+
and supervisor middleware retain their typed family manifests. Both the gateway
852+
and extension run the shared validator against the startup exchange, enforcing
853+
protocol-major compatibility and mutual required-capability sets before either
854+
peer accepts the other. The gateway aggregates immutable, non-secret startup
855+
snapshots for the protected gateway-info API; it does not publish transport,
856+
authentication, or backend configuration.
857+
785858
Static credential delivery is capability-negotiated and endpoint-bound. The
786859
gateway classifies each returned environment entry as either a credential or
787860
non-secret provider configuration and associates every credential key with the
@@ -810,7 +883,13 @@ Provider receipts, installation status, and common operations represent absolute
810883

811884
Provider installation reports belong to the existing `ConnectSupervisor` session. Each report names that session, has an increasing sequence, and expires unless the supervisor reports again. Reconnection or disconnect invalidates prior observations; stored change records survive a gateway restart, but runtime evidence does not. Replaying an identical report cannot extend its lifetime.
812885

813-
Reports and status also compare the supervisor instance with the sandbox's persisted current instance. A different supervisor becoming current invalidates an older connection, including one retained by another gateway replica. Observations stay local to the gateway holding the supervisor session; a status request reaching a replica without that session returns pending. Multi-replica deployments therefore retain the existing supervisor-session routing requirement.
886+
Reports and status also compare the supervisor instance with the sandbox's
887+
persisted current instance. A different supervisor becoming current invalidates
888+
an older connection, including one retained by another gateway replica.
889+
Observations stay local to the gateway holding the supervisor session. A status
890+
request or report reaching another replica follows the shared owner record to
891+
that gateway, which remains the sole authority for accepting and projecting the
892+
session's evidence.
814893

815894
The supervisor reports success only after it installs the matching credentials, activates the effective policy, and receives an acknowledgment from the authenticated workload boundary that it installed the environment for future processes. Environment synchronization shares the process-launch lock, and its acknowledgment identifies the exact publication, including retries at the same provider revision. Failed policy installation cannot reuse evidence for a different installed policy. Ready and revoked statuses also recheck the requested sandbox, provider, attachment and configuration identities; revision fingerprints are compared only for equality. Revocation applies to future credential resolution and future processes. Requests already forwarded upstream can still finish.
816895

0 commit comments

Comments
 (0)