diff --git a/.agents/skills/debug-navigator-cluster/SKILL.md b/.agents/skills/debug-navigator-cluster/SKILL.md index f3eb51531..714dbcd94 100644 --- a/.agents/skills/debug-navigator-cluster/SKILL.md +++ b/.agents/skills/debug-navigator-cluster/SKILL.md @@ -13,8 +13,8 @@ Diagnose why a openshell cluster failed to start after `openshell gateway start` 1. **Pre-deploy check**: `openshell gateway start` in interactive mode prompts to **reuse** (keep volume, clean stale nodes) or **recreate** (destroy everything, fresh start). `mise run cluster` always recreates before deploy. 2. Ensure cluster image is available (local build or remote pull) -3. Create Docker network (`navigator-cluster`) and volume (`navigator-cluster-{name}`) -4. Create and start a privileged Docker container (`navigator-cluster-{name}`) +3. Create Docker network (`openshell-cluster`) and volume (`openshell-cluster-{name}`) +4. Create and start a privileged Docker container (`openshell-cluster-{name}`) 5. Wait for k3s to generate kubeconfig (up to 60s) 6. **Clean stale nodes**: Remove any `NotReady` k3s nodes left over from previous container instances that reused the same persistent volume 7. **Prepare local images** (if `OPENSHELL_PUSH_IMAGES` is set): In `internal` registry mode, bootstrap waits for the in-cluster registry and pushes tagged images there. In `external` mode, bootstrap uses legacy `ctr -n k8s.io images import` push-mode behavior. @@ -35,7 +35,7 @@ The host port is configurable via `--port` on `openshell gateway start` (default The TCP host is also added as an extra gateway TLS SAN so mTLS hostname validation succeeds. -The default cluster name is `openshell`. The container is `navigator-cluster-{name}`. +The default cluster name is `openshell`. The container is `openshell-cluster-{name}`. ## Prerequisites @@ -51,7 +51,7 @@ When the user asks to debug a cluster failure, **run diagnostics automatically** Before running commands, establish: -1. **Cluster name**: Default is `openshell`, giving container name `navigator-cluster-openshell` +1. **Cluster name**: Default is `openshell`, giving container name `openshell-cluster-openshell` 2. **Remote or local**: If the user deployed with `--remote `, all Docker commands must target that host 3. **Config directory**: `~/.config/openshell/clusters/{name}/` @@ -62,14 +62,14 @@ For remote clusters, prefix Docker commands with SSH: ssh docker # Remote kubectl inside the container -ssh docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl ' +ssh docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl ' ``` For local clusters, run Docker commands directly: ```bash docker -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl ' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl ' ``` ### Step 1: Check Docker Container State @@ -77,14 +77,14 @@ docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yam First, determine if the container exists and its state: ```bash -docker ps -a --filter name=navigator-cluster- --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}' +docker ps -a --filter name=openshell-cluster- --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}' ``` If the container does not exist: ```bash # Check if the image is available -docker images 'navigator/cluster*' --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' +docker images 'openshell/cluster*' --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' ``` If the image is missing, re-deploy so bootstrap can pull the published cluster image (or set `OPENSHELL_CLUSTER_IMAGE` explicitly). @@ -92,7 +92,7 @@ If the image is missing, re-deploy so bootstrap can pull the published cluster i If the container exists but is not running, inspect it: ```bash -docker inspect navigator-cluster- --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}' +docker inspect openshell-cluster- --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}' ``` - **OOMKilled=true**: The host doesn't have enough memory. @@ -103,7 +103,7 @@ docker inspect navigator-cluster- --format '{{.State.Status}} exit={{.Stat Get recent container logs to identify startup failures: ```bash -docker logs navigator-cluster- --tail 100 +docker logs openshell-cluster- --tail 100 ``` Look for: @@ -119,13 +119,13 @@ Verify k3s itself is functional: ```bash # API server readiness -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get --raw="/readyz"' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get --raw="/readyz"' # Node status -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes -o wide' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes -o wide' # All pods -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A -o wide' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A -o wide' ``` If `/readyz` fails, k3s is still starting or has crashed. Check container logs (Step 2). @@ -138,16 +138,16 @@ The OpenShell server is deployed via a HelmChart CR as a StatefulSet with persis ```bash # StatefulSet status -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get statefulset/navigator -o wide' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get statefulset/navigator -o wide' # OpenShell pod logs -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator logs statefulset/navigator --tail=100' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator logs statefulset/navigator --tail=100' # Describe statefulset for events -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator describe statefulset/navigator' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator describe statefulset/navigator' # Helm install job logs (the job that installs the OpenShell chart) -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n kube-system logs -l job-name=helm-install-navigator --tail=200' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n kube-system logs -l job-name=helm-install-navigator --tail=200' ``` Common issues: @@ -162,15 +162,15 @@ The Envoy Gateway provides HTTP/gRPC ingress: ```bash # Gateway status -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get gateway/navigator-gateway' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get gateway/navigator-gateway' # Check port bindings on the host -docker port navigator-cluster- +docker port openshell-cluster- ``` Expected ports: `6443/tcp`, `30051/tcp` (mapped to configurable host port, default 8080; set via `--port` on deploy). Only one local cluster can run on a Docker host at a time because `6443` is fixed. -`mise run cluster` handles this by removing conflicting local `navigator-cluster-*` containers first. +`mise run cluster` handles this by removing conflicting local `openshell-cluster-*` containers first. If ports are missing or conflicting, another process may be using them. Check with: @@ -185,37 +185,37 @@ If using Docker-in-Docker (`DOCKER_HOST=tcp://docker:2375`), verify metadata poi Component images (server, sandbox, pki-job) can reach kubelet via two paths: -**Local/external pull mode** (default local via `mise run cluster` / `mise run cluster:build`): Local images are tagged to the configured local registry base (default `127.0.0.1:5000/navigator/*`), pushed to that registry, and pulled by k3s via `registries.yaml` mirror endpoint (typically `host.docker.internal:5000`). `cluster:build` builds then pushes images; `cluster` pushes prebuilt local tags (`navigator/*:dev`, falling back to `localhost:5000/navigator/*:dev` or `127.0.0.1:5000/navigator/*:dev`). +**Local/external pull mode** (default local via `mise run cluster`): Local images are tagged to the configured local registry base (default `127.0.0.1:5000/openshell/*`), pushed to that registry, and pulled by k3s via `registries.yaml` mirror endpoint (typically `host.docker.internal:5000`). The `cluster` task pushes prebuilt local tags (`openshell/*:dev`, falling back to `localhost:5000/openshell/*:dev` or `127.0.0.1:5000/openshell/*:dev`). ```bash # Verify image refs currently used by openshell deployment -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get deploy navigator -o jsonpath="{.spec.template.spec.containers[*].image}"' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get deploy navigator -o jsonpath="{.spec.template.spec.containers[*].image}"' # Verify registry mirror/auth endpoint configuration -docker exec navigator-cluster- cat /etc/rancher/k3s/registries.yaml +docker exec openshell-cluster- cat /etc/rancher/k3s/registries.yaml ``` -**Legacy push mode** (`mise run cluster:push`): Images are imported into the k3s containerd `k8s.io` namespace. +**Legacy push mode**: Images are imported into the k3s containerd `k8s.io` namespace. ```bash # Check if images were imported into containerd (k3s default namespace is k8s.io) -docker exec navigator-cluster- ctr -a /run/k3s/containerd/containerd.sock images ls | grep navigator +docker exec openshell-cluster- ctr -a /run/k3s/containerd/containerd.sock images ls | grep navigator ``` If images are missing, re-import with: ```bash -docker save | docker exec -i navigator-cluster- ctr -a /run/k3s/containerd/containerd.sock images import - +docker save | docker exec -i openshell-cluster- ctr -a /run/k3s/containerd/containerd.sock images import - ``` **External pull mode** (remote deploy, or local with `OPENSHELL_REGISTRY_HOST`/`IMAGE_REPO_BASE` pointing at a non-local registry): Images are pulled from an external registry at runtime. The entrypoint generates `/etc/rancher/k3s/registries.yaml`. ```bash # Verify registries.yaml exists and has credentials -docker exec navigator-cluster- cat /etc/rancher/k3s/registries.yaml +docker exec openshell-cluster- cat /etc/rancher/k3s/registries.yaml # Test pulling an image manually from inside the cluster -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml crictl pull ghcr.io/nvidia/nemoclaw/server:latest' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml crictl pull ghcr.io/nvidia/nemoclaw/server:latest' ``` If `registries.yaml` is missing or has wrong values, verify env wiring (`OPENSHELL_REGISTRY_HOST`, `OPENSHELL_REGISTRY_INSECURE`, username/password for authenticated registries). @@ -226,10 +226,10 @@ TLS certificates are generated by the `navigator-bootstrap` crate (using `rcgen` ```bash # Check if the three TLS secrets exist -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls navigator-server-client-ca navigator-client-tls' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls navigator-server-client-ca navigator-client-tls' # Inspect server cert expiry (if openssl is available in the container) -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls -o jsonpath="{.data.tls\.crt}" | base64 -d | openssl x509 -noout -dates 2>/dev/null || echo "openssl not available"' +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n navigator get secret navigator-server-tls -o jsonpath="{.data.tls\.crt}" | base64 -d | openssl x509 -noout -dates 2>/dev/null || echo "openssl not available"' # Check if CLI-side mTLS files exist locally ls -la ~/.config/openshell/clusters//mtls/ @@ -247,7 +247,7 @@ Common mTLS issues: Events catch scheduling failures, image pull errors, and resource issues: ```bash -docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get events -A --sort-by=.lastTimestamp' | tail -n 50 +docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get events -A --sort-by=.lastTimestamp' | tail -n 50 ``` Look for: @@ -264,13 +264,13 @@ DNS misconfiguration is a common root cause, especially on remote/Linux hosts: ```bash # Check the resolv.conf k3s is using -docker exec navigator-cluster- cat /etc/rancher/k3s/resolv.conf +docker exec openshell-cluster- cat /etc/rancher/k3s/resolv.conf # Test DNS resolution from inside the container -docker exec navigator-cluster- sh -c 'nslookup google.com || wget -q -O /dev/null http://google.com && echo "network ok" || echo "network unreachable"' +docker exec openshell-cluster- sh -c 'nslookup google.com || wget -q -O /dev/null http://google.com && echo "network ok" || echo "network unreachable"' # Check the entrypoint's DNS decision (in container logs) -docker logs navigator-cluster- 2>&1 | head -20 +docker logs openshell-cluster- 2>&1 | head -20 ``` The entrypoint script selects DNS resolvers in this priority: @@ -317,15 +317,15 @@ For clusters deployed with `--remote `, all commands must target the remot ```bash ssh docker ps -a -ssh docker logs navigator-cluster- -ssh docker exec navigator-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A' +ssh docker logs openshell-cluster- +ssh docker exec openshell-cluster- sh -lc 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get pods -A' ``` **Option B: Docker SSH context**: ```bash docker -H ssh:// ps -a -docker -H ssh:// logs navigator-cluster- +docker -H ssh:// logs openshell-cluster- ``` **Setting up kubectl access** (requires tunnel): @@ -344,7 +344,7 @@ Run all diagnostics at once for a comprehensive report: ```bash HOST="" # leave empty for local, or set to SSH destination NAME="openshell" # cluster name -CONTAINER="navigator-cluster-${NAME}" +CONTAINER="openshell-cluster-${NAME}" KCFG="KUBECONFIG=/etc/rancher/k3s/k3s.yaml" # Helper: run docker command locally or remotely diff --git a/.agents/skills/nemoclaw-cli/SKILL.md b/.agents/skills/nemoclaw-cli/SKILL.md index b7db60e52..02e383ae2 100644 --- a/.agents/skills/nemoclaw-cli/SKILL.md +++ b/.agents/skills/nemoclaw-cli/SKILL.md @@ -436,11 +436,11 @@ openshell sandbox delete work-session --- -## Workflow 7: Cluster Inference +## Workflow 7: Gateway Inference -Configure the cluster's managed inference route for `inference.local`. +Configure the gateway's managed inference route for `inference.local`. -### Set cluster inference +### Set gateway inference First ensure the provider record exists: @@ -448,20 +448,20 @@ First ensure the provider record exists: openshell provider list ``` -Then point cluster inference at that provider and model: +Then point gateway inference at that provider and model: ```bash -openshell cluster inference set \ +openshell inference set \ --provider nvidia \ --model nvidia/nemotron-3-nano-30b-a3b ``` -This updates the cluster-managed `inference.local` route. There is no per-route create/list/update/delete workflow for sandbox inference. +This updates the gateway-managed `inference.local` route. There is no per-route create/list/update/delete workflow for sandbox inference. ### Inspect current inference config ```bash -openshell cluster inference get +openshell inference get ``` ### How sandboxes use it @@ -549,8 +549,8 @@ $ openshell sandbox upload --help | Download files from sandbox | `openshell sandbox download ` | | Create provider | `openshell provider create --name N --type T --from-existing` | | List providers | `openshell provider list` | -| Configure cluster inference | `openshell cluster inference set --provider P --model M` | -| View cluster inference | `openshell cluster inference get` | +| Configure gateway inference | `openshell inference set --provider P --model M` | +| View gateway inference | `openshell inference get` | | Delete sandbox | `openshell sandbox delete ` | | Destroy cluster | `openshell gateway destroy` | | Self-teach any command | `openshell --help` | diff --git a/.agents/skills/nemoclaw-cli/cli-reference.md b/.agents/skills/nemoclaw-cli/cli-reference.md index af12ca0f0..82a261139 100644 --- a/.agents/skills/nemoclaw-cli/cli-reference.md +++ b/.agents/skills/nemoclaw-cli/cli-reference.md @@ -9,13 +9,13 @@ Quick-reference for the `openshell` command-line interface. For workflow guidanc | Flag | Description | |------|-------------| | `-v`, `--verbose` | Increase verbosity (`-v` = info, `-vv` = debug, `-vvv` = trace) | -| `-g`, `--gateway ` | Gateway to operate on. Also settable via `OPENSHELL_CLUSTER` env var. Falls back to active gateway in `~/.config/openshell/active_cluster`. | +| `-g`, `--gateway ` | Gateway to operate on. Also settable via `OPENSHELL_GATEWAY` env var. Falls back to active gateway in `~/.config/openshell/active_gateway`. | ## Environment Variables | Variable | Description | |----------|-------------| -| `OPENSHELL_CLUSTER` | Override active gateway name (same as `--gateway`) | +| `OPENSHELL_GATEWAY` | Override active gateway name (same as `--gateway`) | | `OPENSHELL_SANDBOX_POLICY` | Path to default sandbox policy YAML (fallback when `--policy` is not provided) | --- @@ -122,7 +122,7 @@ Print or start an SSH tunnel for kubectl access to a remote cluster. ### `openshell gateway select [name]` -Set the active gateway. Writes to `~/.config/openshell/active_cluster`. When called without arguments, lists all provisioned gateways with the active one marked with `*`. +Set the active gateway. Writes to `~/.config/openshell/active_gateway`. When called without arguments, lists all provisioned gateways with the active one marked with `*`. --- @@ -319,29 +319,29 @@ Delete one or more providers by name. --- -## Cluster Inference Commands +## Inference Commands -### `openshell cluster inference set` +### `openshell inference set` -Configure the managed cluster inference route used by `inference.local`. Both flags are required. +Configure the managed gateway inference route used by `inference.local`. Both flags are required. | Flag | Default | Description | |------|---------|-------------| | `--provider ` | -- | Provider record name (required) | | `--model ` | -- | Model identifier to use for generation requests (required) | -### `openshell cluster inference update` +### `openshell inference update` -Partially update the cluster inference configuration. Fetches the current config and applies only the provided overrides. At least one flag is required. +Partially update the gateway inference configuration. Fetches the current config and applies only the provided overrides. At least one flag is required. | Flag | Default | Description | |------|---------|-------------| | `--provider ` | unchanged | Provider record name | | `--model ` | unchanged | Model identifier | -### `openshell cluster inference get` +### `openshell inference get` -Show the current cluster inference configuration. +Show the current gateway inference configuration. --- diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md index f1ec47906..3eaa6d7c7 100644 --- a/.agents/skills/tui-development/SKILL.md +++ b/.agents/skills/tui-development/SKILL.md @@ -24,22 +24,22 @@ The OpenShell TUI is a ratatui-based terminal UI for the OpenShell platform. It ## 2. Domain Object Hierarchy -The data model follows a strict hierarchy: **Cluster > Sandboxes > Logs**. +The data model follows a strict hierarchy: **Gateway > Sandboxes > Logs**. ``` -Cluster (discovered via navigator_bootstrap::list_clusters()) +Gateway (discovered via navigator_bootstrap::list_gateways()) └── Sandboxes (fetched via gRPC ListSandboxes) └── Logs (fetched via GetSandboxLogs + streamed via WatchSandbox) ``` -- **Clusters** are discovered from on-disk config via `navigator_bootstrap::list_clusters()`. Each cluster has a name, gateway endpoint, and local/remote flag. +- **Gateways** are discovered from on-disk config via `navigator_bootstrap::list_gateways()`. Each gateway has a name, endpoint, and local/remote flag. - **Sandboxes** belong to the active cluster. Fetched via `ListSandboxes` gRPC call with a periodic tick refresh. Each sandbox has: `id`, `name`, `phase`, `created_at_ms`, and `spec.template.image`. - **Logs** belong to a single sandbox. Initial batch fetched via `GetSandboxLogs` (500 lines), then live-tailed via `WatchSandbox` with `follow_logs: true`. The **title bar** always reflects this hierarchy, reading left-to-right from general to specific: ``` - OpenShell │ Current Cluster: () │ + OpenShell │ Current Gateway: () │ ``` ## 3. Navigation & Screen Architecture @@ -50,7 +50,7 @@ Top-level layouts that own the full content area. Each has its own nav bar hints | Screen | Description | Module | | --- | --- | --- | -| `Dashboard` | Cluster list (top) + sandbox table (bottom) | `ui/dashboard.rs` | +| `Dashboard` | Gateway list (top) + sandbox table (bottom) | `ui/dashboard.rs` | | `Sandbox` | Single-sandbox view — detail or logs depending on `Focus` | `ui/sandbox_detail.rs`, `ui/sandbox_logs.rs` | ### Focus (`Focus` enum) @@ -59,7 +59,7 @@ Tracks which panel currently receives keyboard input. | Focus | Screen | Description | | --- | --- | --- | -| `Clusters` | Dashboard | Cluster list panel has input focus | +| `Gateways` | Dashboard | Gateway list panel has input focus | | `Sandboxes` | Dashboard | Sandbox table panel has input focus | | `SandboxDetail` | Sandbox | Sandbox detail view (name, status, image, age) | | `SandboxLogs` | Sandbox | Log viewer with structured rendering | @@ -104,8 +104,8 @@ Every frame renders four vertical regions: ### Title bar examples -- Dashboard: ` OpenShell │ Current Cluster: openshell (Healthy) │ Dashboard` -- Sandbox detail: ` OpenShell │ Current Cluster: openshell (Healthy) │ Sandbox: my-sandbox` +- Dashboard: ` OpenShell │ Current Gateway: openshell (Healthy) │ Dashboard` +- Sandbox detail: ` OpenShell │ Current Gateway: openshell (Healthy) │ Sandbox: my-sandbox` ### Adding a new screen @@ -205,7 +205,7 @@ All colors and styles are defined in `crates/navigator-tui/src/theme.rs`. #### Visual conventions -- **Selected row**: Green `▌` left-border marker on the selected row. Active cluster also gets a green `●` dot. +- **Selected row**: Green `▌` left-border marker on the selected row. Active gateway also gets a green `●` dot. - **Focused panel**: Border changes from `EVERGLADE` to `NVIDIA_GREEN`. - **Status indicators**: Green for healthy/ready/info, yellow for degraded/provisioning/warn, red for unhealthy/error. - **Separators**: Muted `│` characters between title bar segments and nav bar sections. @@ -290,7 +290,7 @@ All actions are accessible via keyboard shortcuts displayed in the nav bar. The ### Screen-specific key hints -**Dashboard (Clusters focus):** +**Dashboard (Gateways focus):** `[Tab] Switch Panel [Enter] Select [j/k] Navigate │ [:] Command [q] Quit` **Dashboard (Sandboxes focus):** @@ -307,12 +307,12 @@ Same as above. | File | Purpose | | --- | --- | | `crates/navigator-tui/Cargo.toml` | Crate manifest — dependencies on `navigator-core`, `navigator-bootstrap`, `ratatui`, `crossterm`, `tonic`, `tokio` | -| `crates/navigator-tui/src/lib.rs` | Entry point. Event loop, gRPC calls (`refresh_health`, `refresh_sandboxes`, `spawn_log_stream`, `handle_sandbox_delete`), cluster switching, mTLS channel building | -| `crates/navigator-tui/src/app.rs` | `App` state struct, `Screen`/`Focus`/`InputMode`/`LogSourceFilter` enums, `LogLine` struct, `ClusterEntry`, all key handling logic | +| `crates/navigator-tui/src/lib.rs` | Entry point. Event loop, gRPC calls (`refresh_health`, `refresh_sandboxes`, `spawn_log_stream`, `handle_sandbox_delete`), gateway switching, mTLS channel building | +| `crates/navigator-tui/src/app.rs` | `App` state struct, `Screen`/`Focus`/`InputMode`/`LogSourceFilter` enums, `LogLine` struct, `GatewayEntry`, all key handling logic | | `crates/navigator-tui/src/event.rs` | `Event` enum (`Key`, `Mouse`, `Tick`, `Resize`, `LogLines`), `EventHandler` with mpsc channels and crossterm polling | | `crates/navigator-tui/src/theme.rs` | `colors` module (NVIDIA_GREEN, EVERGLADE, BG, FG) and `styles` module (all `Style` constants) | | `crates/navigator-tui/src/ui/mod.rs` | Top-level `draw()` dispatcher, `draw_title_bar`, `draw_nav_bar`, `draw_command_bar`, screen routing | -| `crates/navigator-tui/src/ui/dashboard.rs` | Dashboard screen — cluster list table (top) + sandbox table (bottom) | +| `crates/navigator-tui/src/ui/dashboard.rs` | Dashboard screen — gateway list table (top) + sandbox table (bottom) | | `crates/navigator-tui/src/ui/sandboxes.rs` | Reusable sandbox table widget with columns: Name, Status, Created, Age, Image | | `crates/navigator-tui/src/ui/sandbox_detail.rs` | Sandbox detail view — name, status, image, created, age, delete confirmation dialog | | `crates/navigator-tui/src/ui/sandbox_logs.rs` | Structured log viewer — timestamp, source, level, target, message, key=value fields, scroll position, source filter | @@ -336,8 +336,8 @@ lib.rs (event loop, gRPC, async tasks) ### Dependency constraints -- **`navigator-tui` cannot depend on `navigator-cli`** — this would create a circular dependency. TLS channel building for cluster switching is done directly in `lib.rs` using `tonic::transport` primitives (`Certificate`, `Identity`, `ClientTlsConfig`, `Endpoint`). -- mTLS certs are read from `~/.config/openshell/clusters//mtls/` (ca.crt, tls.crt, tls.key). +- **`navigator-tui` cannot depend on `navigator-cli`** — this would create a circular dependency. TLS channel building for gateway switching is done directly in `lib.rs` using `tonic::transport` primitives (`Certificate`, `Identity`, `ClientTlsConfig`, `Endpoint`). +- mTLS certs are read from `~/.config/openshell/gateways//mtls/` (ca.crt, tls.crt, tls.key). ### Proto generated code @@ -389,12 +389,12 @@ The connect timeout for cluster switching is 10 seconds with HTTP/2 keepalive at 6. Auto-scroll kicks in if the user is near the bottom (within 5 lines) 7. Stream is cancelled when user presses `Esc` or navigates away (handle is `.abort()`ed) -### Cluster switching lifecycle +### Gateway switching lifecycle -1. User selects a different cluster and presses `Enter` → `pending_cluster_switch = Some(name)` -2. Event loop calls `handle_cluster_switch()` -3. New mTLS channel is built via `connect_to_cluster()` -4. On success: `app.client` is replaced, `reset_sandbox_state()` clears all sandbox data, `refresh_data()` fetches health + sandboxes for the new cluster +1. User selects a different gateway and presses `Enter` → `pending_gateway_switch = Some(name)` +2. Event loop calls `handle_gateway_switch()` +3. New mTLS channel is built via `connect_to_gateway()` +4. On success: `app.client` is replaced, `reset_sandbox_state()` clears all sandbox data, `refresh_data()` fetches health + sandboxes for the new gateway 5. On failure: `status_text` shows the error ## 9. Development Workflow @@ -426,9 +426,9 @@ Always run before committing: mise run pre-commit ``` -### Cluster changes +### Gateway changes -If you change sandbox or server code that affects the backend, redeploy the cluster: +If you change sandbox or server code that affects the backend, redeploy the gateway: ```bash mise run cluster:deploy all diff --git a/architecture/build-containers.md b/architecture/build-containers.md index bffc3adbb..b5ac8aa8b 100644 --- a/architecture/build-containers.md +++ b/architecture/build-containers.md @@ -56,7 +56,7 @@ tasks/ The project produces three runtime container images and two build-only wheel images. -### Sandbox Image (`navigator/sandbox`) +### Sandbox Image (`openshell/sandbox`) The sandbox container runs inside each sandbox pod. It contains the sandbox supervisor binary, Python runtime, AI agent tooling, and a virtual environment for the Python SDK. @@ -74,7 +74,7 @@ The sandbox container runs inside each sandbox pod. It contains the sandbox supe - Policy files are mounted at `/var/navigator/policy.rego` (rules) and `/var/navigator/data.yaml` (data) when running in file-based policy mode. - The Python SDK is copied directly into the venv's site-packages at `/app/.venv/lib/python3.12/site-packages/navigator/`. -### Gateway Image (`navigator/server`) +### Gateway Image (`openshell/server`) The gateway container runs the control plane / orchestration service. @@ -93,7 +93,7 @@ The gateway container runs the control plane / orchestration service. - No Docker HEALTHCHECK -- health checks are handled by Kubernetes liveness/readiness probes (`tcpSocket` on the gRPC port). - Entrypoint: `navigator-server`, default args: `--port 8080`. -### Cluster Image (`navigator/cluster`) +### Cluster Image (`openshell/cluster`) A k3s image with bundled Helm charts and Kubernetes manifests for single-container deployment. Component images (sandbox, gateway) are **pulled at runtime** from the distribution registry -- they are not bundled as tarballs in this image. @@ -317,7 +317,6 @@ All builds use mise tasks defined in `tasks/*.toml` (included from `mise.toml`). | Task | Description | |---|---| | `mise run cluster` | Bootstrap or incremental deploy: creates cluster if needed, rebuilds changed components | -| `mise run cluster:build:full` | Full build + deploy path (advanced/CI) | ### Other Tasks @@ -425,11 +424,8 @@ In CI pipelines: # Bootstrap or incremental deploy (creates cluster if needed, rebuilds changed components) mise run cluster -# Full build + deploy path (advanced/CI) -mise run cluster:build:full - # Run sandbox container interactively (for testing sandbox code) -mise run cluster:sandbox +mise run sandbox ``` ### Multi-Arch Publishing diff --git a/architecture/cluster-single-node.md b/architecture/gateway-single-node.md similarity index 84% rename from architecture/cluster-single-node.md rename to architecture/gateway-single-node.md index bda6f829e..45f9eed9d 100644 --- a/architecture/cluster-single-node.md +++ b/architecture/gateway-single-node.md @@ -1,15 +1,15 @@ -# Cluster Bootstrap Architecture +# Gateway Bootstrap Architecture -This document describes how OpenShell bootstraps a single-node k3s cluster inside a Docker container, for both local and remote (SSH) targets. +This document describes how OpenShell bootstraps a single-node k3s gateway inside a Docker container, for both local and remote (SSH) targets. ## Goals and Scope -- Provide a single bootstrap flow through `navigator-bootstrap` for local and remote cluster lifecycle. +- Provide a single bootstrap flow through `navigator-bootstrap` for local and remote gateway lifecycle. - Keep Docker as the only runtime dependency for provisioning and lifecycle operations. -- Package the OpenShell cluster as one container image, transferred to the target host via registry pull. +- Package the OpenShell gateway as one container image, transferred to the target host via registry pull. - Support idempotent `deploy` behavior (safe to re-run). -- Persist cluster access artifacts (kubeconfig, metadata, mTLS certs) in the local XDG config directory. -- Track the active cluster so most CLI commands resolve their target automatically. +- Persist gateway access artifacts (kubeconfig, metadata, mTLS certs) in the local XDG config directory. +- Track the active gateway so most CLI commands resolve their target automatically. Out of scope: @@ -19,13 +19,13 @@ Out of scope: - `crates/navigator-cli/src/main.rs`: CLI entry point; `clap`-based command parsing. - `crates/navigator-cli/src/run.rs`: CLI command implementations (`gateway_start`, `gateway_stop`, `gateway_destroy`, `gateway_info`, `gateway_tunnel`). -- `crates/navigator-cli/src/bootstrap.rs`: Auto-bootstrap helpers for `sandbox create` (offers to deploy a cluster when one is unreachable). -- `crates/navigator-bootstrap/src/lib.rs`: Cluster lifecycle orchestration (`deploy_cluster`, `deploy_cluster_with_logs`, `cluster_handle`, `check_existing_deployment`). +- `crates/navigator-cli/src/bootstrap.rs`: Auto-bootstrap helpers for `sandbox create` (offers to deploy a gateway when one is unreachable). +- `crates/navigator-bootstrap/src/lib.rs`: Gateway lifecycle orchestration (`deploy_gateway`, `deploy_gateway_with_logs`, `gateway_handle`, `check_existing_deployment`). - `crates/navigator-bootstrap/src/docker.rs`: Docker API wrappers (network, volume, container, image operations). - `crates/navigator-bootstrap/src/image.rs`: Remote image registry pull with XOR-obfuscated distribution credentials. - `crates/navigator-bootstrap/src/runtime.rs`: In-container operations via `docker exec` (kubeconfig readiness, health polling, stale node cleanup, deployment restart). - `crates/navigator-bootstrap/src/kubeconfig.rs`: Kubeconfig rewriting, storage, and merge into local `~/.kube/config`. -- `crates/navigator-bootstrap/src/metadata.rs`: Cluster metadata creation, storage, and active cluster tracking. +- `crates/navigator-bootstrap/src/metadata.rs`: Gateway metadata creation, storage, and active gateway tracking. - `crates/navigator-bootstrap/src/mtls.rs`: Gateway TLS detection and CLI mTLS bundle extraction. - `crates/navigator-bootstrap/src/push.rs`: Local development image push into k3s containerd. - `crates/navigator-bootstrap/src/paths.rs`: XDG path resolution. @@ -39,20 +39,20 @@ Out of scope: ## CLI Commands -All cluster lifecycle commands live under `openshell gateway`: +All gateway lifecycle commands live under `openshell gateway`: | Command | Description | |---|---| -| `openshell gateway start [--name NAME] [--remote user@host] [--ssh-key PATH]` | Provision or update a cluster | +| `openshell gateway start [--name NAME] [--remote user@host] [--ssh-key PATH]` | Provision or update a gateway | | `openshell gateway stop [--name NAME] [--remote user@host]` | Stop the container (preserves state) | | `openshell gateway destroy [--name NAME] [--remote user@host]` | Destroy container, attached volumes, kubeconfig directory, metadata, and network | | `openshell gateway info [--name NAME]` | Show deployment details (endpoint, kubeconfig path, SSH host) | | `openshell gateway tunnel [--name NAME] [--remote user@host] [--print-command]` | Start or print SSH tunnel for kubectl access | | `openshell status` | Show gateway health via gRPC/HTTP | -| `openshell gateway select ` | Set the active cluster | -| `openshell gateway select` | List all clusters with metadata | +| `openshell gateway select ` | Set the active gateway | +| `openshell gateway select` | List all gateways with metadata | -The `--name` flag defaults to `"openshell"`. When omitted on commands that accept it, the CLI resolves the active cluster via: `--gateway` flag, then `OPENSHELL_CLUSTER` env, then `~/.config/openshell/active_cluster` file. +The `--name` flag defaults to `"openshell"`. When omitted on commands that accept it, the CLI resolves the active gateway via: `--gateway` flag, then `OPENSHELL_GATEWAY` env, then `~/.config/openshell/active_gateway` file. For remote dev/test deploys from a local checkout, `scripts/remote-deploy.sh` wraps a different workflow: it rsyncs the repository to a remote host, builds @@ -66,10 +66,9 @@ Development task entrypoints split bootstrap behavior: | Task | Behavior | |---|---| -| `mise run cluster` | Bootstrap or incremental deploy: creates cluster if needed (fast recreate), then detects changed files and rebuilds/pushes only impacted components | -| `mise run cluster:build:full` | Full build path: builds cluster/server/sandbox images, pushes components, then deploys (preferred in CI) | +| `mise run cluster` | Bootstrap or incremental deploy: creates gateway if needed (fast recreate), then detects changed files and rebuilds/pushes only impacted components | -For `mise run cluster`, `.env` acts as local source-of-truth for `CLUSTER_NAME`, `GATEWAY_PORT`, and `OPENSHELL_CLUSTER`. Missing keys are appended; existing values are preserved. If `GATEWAY_PORT` is missing, the task selects a free local port and persists it. +For `mise run cluster`, `.env` acts as local source-of-truth for `GATEWAY_NAME`, `GATEWAY_PORT`, and `OPENSHELL_GATEWAY`. Missing keys are appended; existing values are preserved. If `GATEWAY_PORT` is missing, the task selects a free local port and persists it. Fast mode ensures a local registry (`127.0.0.1:5000`) is running and configures k3s to mirror pulls via `host.docker.internal:5000`, so the cluster task can push/pull local component images consistently. ## Bootstrap Sequence Diagram @@ -83,7 +82,7 @@ sequenceDiagram participant R as Remote Docker daemon (SSH) U->>C: openshell gateway start --remote user@host - C->>B: deploy_cluster(DeployOptions) + C->>B: deploy_gateway(DeployOptions) B->>B: create_ssh_docker_client (ssh://, 600s timeout) B->>R: pull_remote_image (registry auth, platform-aware) @@ -99,13 +98,13 @@ sequenceDiagram B->>R: wait_for_kubeconfig (30 attempts, 2s apart) B->>B: rewrite and store kubeconfig B->>R: clean_stale_nodes (kubectl delete node) - B->>R: wait_for_cluster_ready (180 attempts, 2s apart) + B->>R: wait_for_gateway_ready (180 attempts, 2s apart) B->>R: poll for secret navigator-cli-client (90 attempts, 2s apart) R-->>B: ca.crt, tls.crt, tls.key B->>B: atomically store mTLS bundle - B->>B: create and persist cluster metadata JSON - B->>B: save_active_cluster - B-->>C: ClusterHandle (kubeconfig_path, metadata, docker client) + B->>B: create and persist gateway metadata JSON + B->>B: save_active_gateway + B-->>C: GatewayHandle (kubeconfig_path, metadata, docker client) ``` ## End-State Connectivity Diagram @@ -115,14 +114,14 @@ flowchart LR subgraph WS[User workstation] NAV[navigator-cli] KUBECTL[kubectl] - KC[stored kubeconfig ~/.config/openshell/clusters/NAME/kubeconfig] + KC[stored kubeconfig ~/.config/openshell/gateways/NAME/kubeconfig] MTLS[mTLS bundle ca.crt, tls.crt, tls.key] TUN[ssh -L 6443:127.0.0.1:6443 user@host] end subgraph HOST[Target host] DOCKER[Docker daemon] - K3S[navigator-cluster-NAME single k3s container] + K3S[openshell-cluster-NAME single k3s container] KAPI[Kubernetes API :6443] G8080[Gateway :port -> :30051 mTLS default 8080] SBX[Sandbox runtime] @@ -146,41 +145,41 @@ flowchart LR ### 1) Entry and client selection -`deploy_cluster(DeployOptions)` in `crates/navigator-bootstrap/src/lib.rs` chooses execution mode: +`deploy_gateway(DeployOptions)` in `crates/navigator-bootstrap/src/lib.rs` chooses execution mode: - `DeployOptions` fields: `name: String`, `image_ref: Option`, `remote: Option`, `port: u16` (default 8080). - `RemoteOptions` fields: `destination: String`, `ssh_key: Option`. - **Local deploy**: Create one Docker client with `Docker::connect_with_local_defaults()`. - **Remote deploy**: Create SSH Docker client via `Docker::connect_with_ssh()` with a 600-second timeout (for large image transfers). The destination is prefixed with `ssh://` if not already present. -The `deploy_cluster_with_logs` variant accepts an `FnMut(String)` callback for progress reporting. The CLI wraps this in a `ClusterDeployLogPanel` for interactive terminals. +The `deploy_gateway_with_logs` variant accepts an `FnMut(String)` callback for progress reporting. The CLI wraps this in a `GatewayDeployLogPanel` for interactive terminals. -**Pre-deploy check** (CLI layer in `gateway_start`): In interactive terminals, `check_existing_deployment` inspects whether a container or volume already exists. If found, the user is prompted to destroy and recreate or reuse the existing cluster. +**Pre-deploy check** (CLI layer in `gateway_start`): In interactive terminals, `check_existing_deployment` inspects whether a container or volume already exists. If found, the user is prompted to destroy and recreate or reuse the existing gateway. ### 2) Image readiness -Image ref resolution in `default_cluster_image_ref()`: +Image ref resolution in `default_gateway_image_ref()`: 1. If `OPENSHELL_CLUSTER_IMAGE` is set and non-empty, use it verbatim. -2. Otherwise, use the published distribution image base (`/navigator/cluster`) with its default tag behavior. +2. Otherwise, use the published distribution image base (`/openshell/cluster`) with its default tag behavior. - **Local deploy**: `ensure_image()` inspects the image on the local daemon and pulls from the configured registry if missing (using built-in distribution credentials when pulling from the default distribution host). -- **Remote deploy**: `pull_remote_image()` queries the remote daemon's architecture via `Docker::version()`, pulls the matching platform variant from the distribution registry (with XOR-decoded credentials), and tags the pulled image to the expected local ref (for example `navigator/cluster:dev` when an explicit local tag is requested). +- **Remote deploy**: `pull_remote_image()` queries the remote daemon's architecture via `Docker::version()`, pulls the matching platform variant from the distribution registry (with XOR-decoded credentials), and tags the pulled image to the expected local ref (for example `openshell/cluster:dev` when an explicit local tag is requested). ### 3) Runtime infrastructure For the target daemon (local or remote): -1. **Ensure bridge network** `navigator-cluster` (attachable, bridge driver) via `ensure_network()`. -2. **Ensure volume** `navigator-cluster-{name}` via `ensure_volume()`. +1. **Ensure bridge network** `openshell-cluster` (attachable, bridge driver) via `ensure_network()`. +2. **Ensure volume** `openshell-cluster-{name}` via `ensure_volume()`. 3. **Compute extra TLS SANs**: - For **local deploys**: Check `DOCKER_HOST` for a non-loopback `tcp://` endpoint (e.g., `tcp://docker:2375` in CI). If found, extract the host as an extra SAN. The function `local_gateway_host_from_docker_host()` skips `localhost`, `127.0.0.1`, and `::1`. - For **remote deploys**: Extract the host from the SSH destination (handles `user@host`, `ssh://user@host`), resolve via `ssh -G` to get the canonical hostname/IP. Include both the resolved host and original SSH host (if different) as extra SANs. -4. **Ensure container** `navigator-cluster-{name}` via `ensure_container()`: +4. **Ensure container** `openshell-cluster-{name}` via `ensure_container()`: - k3s server command: `server --disable=traefik --tls-san=127.0.0.1 --tls-san=localhost --tls-san=host.docker.internal` plus computed extra SANs. - Privileged mode. - - Volume bind mount: `navigator-cluster-{name}:/var/lib/rancher/k3s`. - - Network: `navigator-cluster`. + - Volume bind mount: `openshell-cluster-{name}:/var/lib/rancher/k3s`. + - Network: `openshell-cluster`. - Extra host: `host.docker.internal:host-gateway`. - Port mappings: @@ -198,11 +197,11 @@ For the target daemon (local or remote): After the container starts: 1. **Poll kubeconfig**: `wait_for_kubeconfig()` runs `cat /etc/rancher/k3s/k3s.yaml` via `docker exec` up to 30 times, 2 seconds apart (60s total). Each attempt first checks that the container is still running. Validates the output contains `apiVersion:` and `clusters:`. -2. **Rewrite kubeconfig**: Replace the server address with `https://127.0.0.1:6443`. Rename `default` entries (cluster, context, user) to the cluster name. Remote mode appends `-remote` suffix (e.g., `{name}-remote`) to avoid collisions with local contexts. -3. **Store kubeconfig** at `~/.config/openshell/clusters/{name}/kubeconfig` (or `$XDG_CONFIG_HOME/openshell/clusters/{name}/kubeconfig`). +2. **Rewrite kubeconfig**: Replace the server address with `https://127.0.0.1:6443`. Rename `default` entries (cluster, context, user) to the gateway name. Remote mode appends `-remote` suffix (e.g., `{name}-remote`) to avoid collisions with local contexts. +3. **Store kubeconfig** at `~/.config/openshell/gateways/{name}/kubeconfig` (or `$XDG_CONFIG_HOME/openshell/gateways/{name}/kubeconfig`). 4. **Clean stale nodes**: `clean_stale_nodes()` finds `NotReady` nodes via `kubectl get nodes` and deletes them. This is needed when a container is recreated but reuses the persistent volume -- k3s registers a new node (using the container ID as hostname) while old node entries persist in etcd. Non-fatal on error; returns the count of removed nodes. 5. **Push local images** (optional, local deploy only): If `OPENSHELL_PUSH_IMAGES` is set, the comma-separated image refs are exported from the local Docker daemon as a single tar, uploaded into the container via `docker put_archive`, and imported into containerd via `ctr images import` in the `k8s.io` namespace. After import, `kubectl rollout restart deployment/navigator -n navigator` is run, followed by `kubectl rollout status --timeout=180s` to wait for completion. See `crates/navigator-bootstrap/src/push.rs`. -6. **Wait for cluster health**: `wait_for_cluster_ready()` polls the Docker HEALTHCHECK status up to 180 times, 2 seconds apart (6 min total). A background task streams container logs during this wait. Failure modes: +6. **Wait for gateway health**: `wait_for_gateway_ready()` polls the Docker HEALTHCHECK status up to 180 times, 2 seconds apart (6 min total). A background task streams container logs during this wait. Failure modes: - Container exits during polling: error includes recent log lines. - Container has no HEALTHCHECK instruction: fails immediately. - HEALTHCHECK reports unhealthy on final attempt: error includes recent logs. @@ -211,13 +210,13 @@ After the container starts: TLS is always required. `fetch_and_store_cli_mtls()` polls for Kubernetes secret `navigator-cli-client` in namespace `navigator` (90 attempts, 2 seconds apart, 3 min total). Each attempt checks the container is still running. The secret's base64-encoded `ca.crt`, `tls.crt`, and `tls.key` fields are decoded and stored. -Storage location: `~/.config/openshell/clusters/{name}/mtls/` +Storage location: `~/.config/openshell/gateways/{name}/mtls/` Write is atomic: write to `.tmp` directory, validate all three files are non-empty, rename existing directory to `.bak`, rename `.tmp` to final path, then remove `.bak`. ### 6) Metadata persistence -`create_cluster_metadata()` produces a `ClusterMetadata` struct: +`create_gateway_metadata()` produces a `GatewayMetadata` struct: - **Local**: endpoint `https://127.0.0.1:{port}` by default, or `https://{docker_host}:{port}` when `DOCKER_HOST` is a non-loopback `tcp://` endpoint. `is_remote=false`. - **Remote**: endpoint `https://{resolved_host}:{port}`, `is_remote=true`, plus SSH destination and resolved host. @@ -226,22 +225,22 @@ Metadata fields: | Field | Type | Description | |---|---|---| -| `name` | `String` | Cluster name | +| `name` | `String` | Gateway name | | `gateway_endpoint` | `String` | HTTPS endpoint with port (e.g., `https://127.0.0.1:8080`) | -| `is_remote` | `bool` | Whether cluster is remote | +| `is_remote` | `bool` | Whether gateway is remote | | `gateway_port` | `u16` | Host port mapped to the gateway NodePort | | `remote_host` | `Option` | SSH destination (e.g., `user@host`) | | `resolved_host` | `Option` | Resolved hostname/IP from `ssh -G` | -Metadata location: `~/.config/openshell/clusters/{name}_metadata.json` +Metadata location: `~/.config/openshell/gateways/{name}_metadata.json` -Note: metadata is stored at the `clusters/` level (not nested inside `{name}/` like kubeconfig and mTLS). +Note: metadata is stored at the `gateways/` level (not nested inside `{name}/` like kubeconfig and mTLS). -After deploy, the CLI calls `save_active_cluster(name)`, writing the cluster name to `~/.config/openshell/active_cluster`. Subsequent commands that don't specify `--gateway` or `OPENSHELL_CLUSTER` resolve to this active cluster. +After deploy, the CLI calls `save_active_gateway(name)`, writing the gateway name to `~/.config/openshell/active_gateway`. Subsequent commands that don't specify `--gateway` or `OPENSHELL_GATEWAY` resolve to this active gateway. ## Container Image -The cluster image is defined in `deploy/docker/Dockerfile.cluster`: +The gateway image is defined in `deploy/docker/Dockerfile.cluster`: ``` Base: rancher/k3s:v1.35.2-k3s1 @@ -313,7 +312,7 @@ flowchart LR RP --> Auth[Authenticate with distribution registry] Auth --> Pull[Pull platform-specific image layers] Pull --> Tag[Tag to local image ref] - Tag --> OK[Image available as navigator/cluster:TAG] + Tag --> OK[Image available as openshell/cluster:TAG] ``` - Remote platform is queried via `Docker::version()` and normalized (e.g., `x86_64` -> `amd64`, `aarch64` -> `arm64`). @@ -325,7 +324,7 @@ flowchart LR ### Kubernetes API access - Kubeconfig always targets `https://127.0.0.1:6443`. -- For remote clusters, the user must open an SSH local-forward tunnel: +- For remote gateways, the user must open an SSH local-forward tunnel: ```bash ssh -L 6443:127.0.0.1:6443 -N user@host @@ -337,7 +336,7 @@ CLI helper: openshell gateway tunnel --name ``` -The `--remote` flag is optional; the CLI resolves the SSH destination from stored cluster metadata. Pass `--print-command` to print the SSH command without executing it. +The `--remote` flag is optional; the CLI resolves the SSH destination from stored gateway metadata. Pass `--print-command` to print the SSH command without executing it. ### Gateway endpoint exposure @@ -349,11 +348,11 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store ### stop -`ClusterHandle::stop()` calls `stop_container()`, which tolerates 404 (not found) and 409 (already stopped). +`GatewayHandle::stop()` calls `stop_container()`, which tolerates 404 (not found) and 409 (already stopped). ### destroy -**Bootstrap layer** (`ClusterHandle::destroy()` -> `destroy_cluster_resources()`): +**Bootstrap layer** (`GatewayHandle::destroy()` -> `destroy_gateway_resources()`): 1. Stop the container. 2. Remove the container (`force=true`). Tolerates 404. @@ -363,8 +362,8 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store **CLI layer** (`gateway_destroy()` in `run.rs` additionally): -6. Remove the metadata JSON file via `remove_cluster_metadata()`. -7. Clear the active cluster reference if it matches the destroyed cluster. +6. Remove the metadata JSON file via `remove_gateway_metadata()`. +7. Clear the active gateway reference if it matches the destroyed gateway. ## Idempotency and Error Behavior @@ -372,7 +371,7 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store - Existing network/volume are reused (inspect before create). - If a container exists with the same image ID, it is reused; if the image changed, the container is recreated. - `start_container` tolerates already-running state (409). -- In interactive terminals, the CLI prompts the user to optionally destroy and recreate an existing cluster before redeploying. +- In interactive terminals, the CLI prompts the user to optionally destroy and recreate an existing gateway before redeploying. - Error handling surfaces: - Docker API failures from inspect/create/start/remove. - SSH connection failures when creating the remote Docker client. @@ -384,11 +383,11 @@ The `--remote` flag is optional; the CLI resolves the SSH destination from store ## Auto-Bootstrap from `sandbox create` -When `openshell sandbox create` cannot connect to a cluster (connection refused, DNS error, missing default TLS certs), the CLI offers to bootstrap one automatically: +When `openshell sandbox create` cannot connect to a gateway (connection refused, DNS error, missing default TLS certs), the CLI offers to bootstrap one automatically: 1. `should_attempt_bootstrap()` in `crates/navigator-cli/src/bootstrap.rs` checks the error type. It returns `true` for connectivity errors and missing default TLS materials, but `false` for TLS handshake/auth errors. 2. If running in a terminal, the user is prompted to confirm. -3. `run_bootstrap()` deploys a cluster named `"openshell"`, sets it as active, and returns fresh `TlsOptions` pointing to the newly-written mTLS certs. +3. `run_bootstrap()` deploys a gateway named `"openshell"`, sets it as active, and returns fresh `TlsOptions` pointing to the newly-written mTLS certs. ## Container Environment Variables @@ -422,7 +421,7 @@ Environment variables that affect bootstrap behavior when set on the host: | `XDG_CONFIG_HOME` | Base config directory (default: `$HOME/.config`) | | `KUBECONFIG` | Target kubeconfig path for merge (first colon-separated path; default: `$HOME/.kube/config`) | | `DOCKER_HOST` | When `tcp://` and non-loopback, the host is added as a TLS SAN and used as the gateway endpoint | -| `OPENSHELL_PUSH_IMAGES` | Comma-separated image refs to push into the cluster's containerd (local deploy only) | +| `OPENSHELL_PUSH_IMAGES` | Comma-separated image refs to push into the gateway's containerd (local deploy only) | | `OPENSHELL_REGISTRY_HOST` | Override the distribution registry host | | `OPENSHELL_REGISTRY_NAMESPACE` | Override the registry namespace (default: `"navigator"`) | | `IMAGE_REPO_BASE` / `OPENSHELL_IMAGE_REPO_BASE` | Override the image repository base path | @@ -430,7 +429,7 @@ Environment variables that affect bootstrap behavior when set on the host: | `OPENSHELL_REGISTRY_ENDPOINT` | Custom registry mirror endpoint | | `OPENSHELL_REGISTRY_USERNAME` | Override registry auth username | | `OPENSHELL_REGISTRY_PASSWORD` | Override registry auth password | -| `OPENSHELL_CLUSTER` | Set the active cluster name for CLI commands | +| `OPENSHELL_GATEWAY` | Set the active gateway name for CLI commands | ## File System Layout @@ -438,9 +437,9 @@ Artifacts stored under `$XDG_CONFIG_HOME/openshell/` (default `~/.config/openshe ``` openshell/ - active_cluster # plain text: active cluster name - clusters/ - {name}_metadata.json # ClusterMetadata JSON + active_gateway # plain text: active gateway name + gateways/ + {name}_metadata.json # GatewayMetadata JSON {name}/ kubeconfig # rewritten kubeconfig mtls/ # mTLS bundle (when TLS enabled) @@ -456,7 +455,7 @@ openshell/ - `crates/navigator-bootstrap/src/image.rs` -- registry pull, XOR credentials - `crates/navigator-bootstrap/src/runtime.rs` -- exec, health polling, stale node cleanup - `crates/navigator-bootstrap/src/kubeconfig.rs` -- kubeconfig rewrite and merge -- `crates/navigator-bootstrap/src/metadata.rs` -- metadata CRUD, active cluster, SSH resolution +- `crates/navigator-bootstrap/src/metadata.rs` -- metadata CRUD, active gateway, SSH resolution - `crates/navigator-bootstrap/src/mtls.rs` -- TLS detection, secret extraction, atomic write - `crates/navigator-bootstrap/src/push.rs` -- local image push into k3s containerd - `crates/navigator-bootstrap/src/constants.rs` -- naming conventions diff --git a/crates/navigator-bootstrap/src/build.rs b/crates/navigator-bootstrap/src/build.rs index 91967a1fe..9624e01b3 100644 --- a/crates/navigator-bootstrap/src/build.rs +++ b/crates/navigator-bootstrap/src/build.rs @@ -1,11 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Build and push container images into a k3s cluster. +//! Build and push container images into a k3s gateway. //! //! This module wraps bollard's `build_image()` API to build a container image //! from a Dockerfile and build context, then reuses the existing push pipeline -//! to import the image into the cluster's containerd runtime. +//! to import the image into the gateway's containerd runtime. use std::collections::HashMap; use std::io::Read; @@ -19,19 +19,19 @@ use miette::{IntoDiagnostic, Result, WrapErr}; use crate::constants::container_name; use crate::push::push_local_images; -/// Build a container image from a Dockerfile and push it into the cluster. +/// Build a container image from a Dockerfile and push it into the gateway. /// /// This is used by `openshell sandbox create --from `. It: /// 1. Creates a tar archive of the build context directory. /// 2. Sends it to the local Docker daemon via `build_image()`. -/// 3. Pushes the resulting image into the cluster's containerd via the +/// 3. Pushes the resulting image into the gateway's containerd via the /// existing `push_local_images()` pipeline. #[allow(clippy::implicit_hasher)] pub async fn build_and_push_image( dockerfile_path: &Path, tag: &str, context_dir: &Path, - cluster_name: &str, + gateway_name: &str, build_args: &HashMap, on_log: &mut impl FnMut(String), ) -> Result<()> { @@ -43,18 +43,18 @@ pub async fn build_and_push_image( build_image(dockerfile_path, tag, context_dir, build_args, on_log).await?; on_log(format!("Built image {tag}")); - // 2. Push into the cluster. + // 2. Push into the gateway. on_log(format!( - "Pushing image {tag} into cluster \"{cluster_name}\"" + "Pushing image {tag} into gateway \"{gateway_name}\"" )); let local_docker = Docker::connect_with_local_defaults() .into_diagnostic() .wrap_err("failed to connect to local Docker daemon")?; - let container = container_name(cluster_name); + let container = container_name(gateway_name); let images: Vec<&str> = vec![tag]; push_local_images(&local_docker, &local_docker, &container, &images, on_log).await?; - on_log(format!("Image {tag} is available in the cluster.")); + on_log(format!("Image {tag} is available in the gateway.")); Ok(()) } diff --git a/crates/navigator-bootstrap/src/constants.rs b/crates/navigator-bootstrap/src/constants.rs index 47641dae8..e174e516e 100644 --- a/crates/navigator-bootstrap/src/constants.rs +++ b/crates/navigator-bootstrap/src/constants.rs @@ -1,20 +1,20 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -pub const NETWORK_NAME: &str = "navigator-cluster"; +pub const NETWORK_NAME: &str = "openshell-cluster"; pub const KUBECONFIG_PATH: &str = "/etc/rancher/k3s/k3s.yaml"; /// K8s secret holding the server's TLS certificate and private key. -pub const SERVER_TLS_SECRET_NAME: &str = "navigator-server-tls"; +pub const SERVER_TLS_SECRET_NAME: &str = "openshell-server-tls"; /// K8s secret holding the CA certificate used to verify client certificates. -pub const SERVER_CLIENT_CA_SECRET_NAME: &str = "navigator-server-client-ca"; +pub const SERVER_CLIENT_CA_SECRET_NAME: &str = "openshell-server-client-ca"; /// K8s secret holding the client TLS certificate, key, and CA cert (shared by CLI and sandboxes). -pub const CLIENT_TLS_SECRET_NAME: &str = "navigator-client-tls"; +pub const CLIENT_TLS_SECRET_NAME: &str = "openshell-client-tls"; pub fn container_name(name: &str) -> String { - format!("navigator-cluster-{name}") + format!("openshell-cluster-{name}") } pub fn volume_name(name: &str) -> String { - format!("navigator-cluster-{name}") + format!("openshell-cluster-{name}") } diff --git a/crates/navigator-bootstrap/src/docker.rs b/crates/navigator-bootstrap/src/docker.rs index 907f34d14..a3e5ef729 100644 --- a/crates/navigator-bootstrap/src/docker.rs +++ b/crates/navigator-bootstrap/src/docker.rs @@ -22,7 +22,7 @@ use miette::{IntoDiagnostic, Result, WrapErr}; use std::collections::HashMap; use std::path::Path; -const REGISTRY_NAMESPACE_DEFAULT: &str = "navigator"; +const REGISTRY_NAMESPACE_DEFAULT: &str = "openshell"; const REGISTRY_MODE_EXTERNAL: &str = "external"; @@ -202,7 +202,7 @@ pub async fn ensure_image( if image::is_local_image_ref(image_ref) { return Err(miette::miette!( "Image '{}' not found locally. This looks like a locally-built image \ - (no registry prefix). Build it first with `mise run docker:build:cluster`.", + (no registry prefix). Build it first with `mise run docker:build:gateway`.", image_ref, )); } @@ -476,7 +476,7 @@ pub async fn ensure_container( ) .await .into_diagnostic() - .wrap_err("failed to create cluster container")?; + .wrap_err("failed to create gateway container")?; Ok(()) } @@ -504,7 +504,7 @@ pub async fn start_container(docker: &Docker, name: &str) -> Result<()> { Err(err) => { return Err(err) .into_diagnostic() - .wrap_err("failed to start cluster container"); + .wrap_err("failed to start gateway container"); } } } @@ -521,7 +521,7 @@ pub async fn stop_container(docker: &Docker, container_name: &str) -> Result<()> } } -pub async fn destroy_cluster_resources( +pub async fn destroy_gateway_resources( docker: &Docker, name: &str, kubeconfig_path: &Path, @@ -569,12 +569,12 @@ pub async fn destroy_cluster_resources( return Err(err).into_diagnostic(); } - // Remove the cluster image so the next deploy always pulls the latest + // Remove the gateway image so the next deploy always pulls the latest // version from the registry instead of reusing a stale local copy. // Docker may briefly report the container as still running after a // force-remove, so retry a few times on conflict (409) errors. if let Some(ref image_id) = container_image { - tracing::debug!("Removing cluster image: {}", image_id); + tracing::debug!("Removing gateway image: {}", image_id); let mut last_err = None; for attempt in 0..5 { if attempt > 0 { @@ -610,7 +610,7 @@ pub async fn destroy_cluster_resources( } } if let Some(err) = last_err { - tracing::warn!("Failed to remove cluster image {}: {}", image_id, err); + tracing::warn!("Failed to remove gateway image {}: {}", image_id, err); } } @@ -632,7 +632,7 @@ pub async fn destroy_cluster_resources( Ok(()) } -/// Forcefully remove the cluster network, disconnecting any remaining +/// Forcefully remove the gateway network, disconnecting any remaining /// containers first. This ensures that stale Docker network endpoints /// cannot prevent port bindings from being released. async fn force_remove_network(docker: &Docker) -> Result<()> { @@ -723,9 +723,9 @@ fn truncate_id(id: &str) -> &str { } } -/// Information about an existing cluster deployment. +/// Information about an existing gateway deployment. #[derive(Debug, Clone)] -pub struct ExistingClusterInfo { +pub struct ExistingGatewayInfo { /// Whether the container exists. pub container_exists: bool, /// Whether the container is currently running. @@ -736,14 +736,14 @@ pub struct ExistingClusterInfo { pub container_image: Option, } -/// Check whether a cluster with the given name already exists. +/// Check whether a gateway with the given name already exists. /// -/// Returns `None` if no cluster resources exist, or `Some(info)` with +/// Returns `None` if no gateway resources exist, or `Some(info)` with /// details about the existing deployment. -pub async fn check_existing_cluster( +pub async fn check_existing_gateway( docker: &Docker, name: &str, -) -> Result> { +) -> Result> { let container_name = container_name(name); let vol_name = volume_name(name); @@ -770,7 +770,7 @@ pub async fn check_existing_cluster( return Ok(None); } - Ok(Some(ExistingClusterInfo { + Ok(Some(ExistingGatewayInfo { container_exists, container_running, volume_exists, diff --git a/crates/navigator-bootstrap/src/edge_token.rs b/crates/navigator-bootstrap/src/edge_token.rs index 1ca1ea2d6..12b294d39 100644 --- a/crates/navigator-bootstrap/src/edge_token.rs +++ b/crates/navigator-bootstrap/src/edge_token.rs @@ -4,26 +4,26 @@ //! Edge authentication token storage. //! //! Stores the bearer token for edge-authenticated gateways at -//! `$XDG_CONFIG_HOME/openshell/clusters//edge_token`. +//! `$XDG_CONFIG_HOME/openshell/gateways//edge_token`. //! The token is a plain-text JWT string with `0600` permissions. -use crate::paths::clusters_dir; +use crate::paths::gateways_dir; use miette::{IntoDiagnostic, Result, WrapErr}; use std::path::PathBuf; -/// Path to the stored edge auth token for a cluster. -pub fn edge_token_path(cluster_name: &str) -> Result { - Ok(clusters_dir()?.join(cluster_name).join("edge_token")) +/// Path to the stored edge auth token for a gateway. +pub fn edge_token_path(gateway_name: &str) -> Result { + Ok(gateways_dir()?.join(gateway_name).join("edge_token")) } /// Legacy path used before the rename to `edge_token`. -fn legacy_token_path(cluster_name: &str) -> Result { - Ok(clusters_dir()?.join(cluster_name).join("cf_token")) +fn legacy_token_path(gateway_name: &str) -> Result { + Ok(gateways_dir()?.join(gateway_name).join("cf_token")) } -/// Store an edge authentication token for a cluster. -pub fn store_edge_token(cluster_name: &str, token: &str) -> Result<()> { - let path = edge_token_path(cluster_name)?; +/// Store an edge authentication token for a gateway. +pub fn store_edge_token(gateway_name: &str, token: &str) -> Result<()> { + let path = edge_token_path(gateway_name)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .into_diagnostic() @@ -43,27 +43,27 @@ pub fn store_edge_token(cluster_name: &str, token: &str) -> Result<()> { Ok(()) } -/// Load a stored edge authentication token for a cluster. +/// Load a stored edge authentication token for a gateway. /// /// Returns `None` if no token file exists or the file is empty. /// Falls back to the legacy `cf_token` path for backwards compatibility. -pub fn load_edge_token(cluster_name: &str) -> Option { +pub fn load_edge_token(gateway_name: &str) -> Option { // Try the new path first, then fall back to legacy. - let path = edge_token_path(cluster_name) + let path = edge_token_path(gateway_name) .ok() .filter(|p| p.exists()) - .or_else(|| legacy_token_path(cluster_name).ok().filter(|p| p.exists()))?; + .or_else(|| legacy_token_path(gateway_name).ok().filter(|p| p.exists()))?; let contents = std::fs::read_to_string(&path).ok()?; let token = contents.trim().to_string(); if token.is_empty() { None } else { Some(token) } } /// Remove a stored edge authentication token. -pub fn remove_edge_token(cluster_name: &str) -> Result<()> { +pub fn remove_edge_token(gateway_name: &str) -> Result<()> { // Remove both new and legacy paths. for path in [ - edge_token_path(cluster_name)?, - legacy_token_path(cluster_name)?, + edge_token_path(gateway_name)?, + legacy_token_path(gateway_name)?, ] { if path.exists() { std::fs::remove_file(&path) @@ -102,9 +102,9 @@ mod tests { fn store_and_load_edge_token_roundtrip() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - store_edge_token("test-cluster", "eyJhbGciOiJSUzI1NiJ9.test.sig").unwrap(); + store_edge_token("test-gateway", "eyJhbGciOiJSUzI1NiJ9.test.sig").unwrap(); assert_eq!( - load_edge_token("test-cluster"), + load_edge_token("test-gateway"), Some("eyJhbGciOiJSUzI1NiJ9.test.sig".to_string()) ); }); @@ -114,7 +114,7 @@ mod tests { fn load_edge_token_returns_none_when_not_set() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - assert_eq!(load_edge_token("no-such-cluster"), None); + assert_eq!(load_edge_token("no-such-gateway"), None); }); } @@ -153,10 +153,10 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { // Write manually with whitespace. - let path = edge_token_path("ws-cluster").unwrap(); + let path = edge_token_path("ws-gateway").unwrap(); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, " my-token \n").unwrap(); - assert_eq!(load_edge_token("ws-cluster"), Some("my-token".to_string())); + assert_eq!(load_edge_token("ws-gateway"), Some("my-token".to_string())); }); } @@ -164,10 +164,10 @@ mod tests { fn load_edge_token_returns_none_for_empty_file() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - let path = edge_token_path("empty-cluster").unwrap(); + let path = edge_token_path("empty-gateway").unwrap(); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, " \n").unwrap(); - assert_eq!(load_edge_token("empty-cluster"), None); + assert_eq!(load_edge_token("empty-gateway"), None); }); } @@ -176,11 +176,11 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { // Write to the legacy cf_token path. - let path = legacy_token_path("legacy-cluster").unwrap(); + let path = legacy_token_path("legacy-gateway").unwrap(); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, "legacy-jwt").unwrap(); assert_eq!( - load_edge_token("legacy-cluster"), + load_edge_token("legacy-gateway"), Some("legacy-jwt".to_string()) ); }); diff --git a/crates/navigator-bootstrap/src/image.rs b/crates/navigator-bootstrap/src/image.rs index 3955bb546..57736a1c2 100644 --- a/crates/navigator-bootstrap/src/image.rs +++ b/crates/navigator-bootstrap/src/image.rs @@ -24,8 +24,8 @@ pub const DEFAULT_REGISTRY: &str = "ghcr.io"; /// Default image repository base on GHCR (without component name or tag). pub const DEFAULT_IMAGE_REPO_BASE: &str = "ghcr.io/nvidia/nemoclaw"; -/// Default full cluster image path on GHCR (without tag). -pub const DEFAULT_CLUSTER_IMAGE: &str = "ghcr.io/nvidia/nemoclaw/cluster"; +/// Default full gateway image path on GHCR (without tag). +pub const DEFAULT_GATEWAY_IMAGE: &str = "ghcr.io/nvidia/nemoclaw/cluster"; /// Default username for token-based GHCR authentication. /// @@ -156,11 +156,11 @@ pub(crate) fn ghcr_credentials(token: Option<&str>) -> Option }) } -/// Pull the cluster image directly on a remote Docker daemon from ghcr.io, +/// Pull the gateway image directly on a remote Docker daemon from ghcr.io, /// authenticating with the provided registry token. /// /// After pulling, the image is tagged to the expected local image ref (e.g., -/// `navigator/cluster:dev`) so that all downstream container creation logic works +/// `openshell/cluster:dev`) so that all downstream container creation logic works /// without changes. /// /// The remote host's platform is queried so the correct architecture variant is @@ -186,7 +186,7 @@ pub async fn pull_remote_image( // and already points at a registry image, honour its tag. Otherwise use // the distribution registry default tag — the local build tag (e.g. "dev") // is a build-time convention that doesn't exist in the registry. - let registry_image_base = DEFAULT_CLUSTER_IMAGE.to_string(); + let registry_image_base = DEFAULT_GATEWAY_IMAGE.to_string(); let tag = if is_local_image_ref(image_ref) { PULL_REGISTRY_DEFAULT_TAG.to_string() @@ -201,7 +201,7 @@ pub async fn pull_remote_image( registry_image, DEFAULT_REGISTRY ); on_progress(format!( - "[status] Pulling navigator/cluster:{tag} ({platform_str}) on remote host" + "[status] Pulling openshell/cluster:{tag} ({platform_str}) on remote host" )); let credentials = ghcr_credentials(registry_token); @@ -234,7 +234,7 @@ pub async fn pull_remote_image( // Tag the pulled image to the expected local image ref so downstream code // (container creation, image ID checks) works unchanged. - // e.g., tag "ghcr.io/nvidia/nemoclaw/cluster:latest" as "navigator/cluster:dev" + // e.g., tag "ghcr.io/nvidia/nemoclaw/cluster:latest" as "openshell/cluster:dev" let (target_repo, target_tag) = parse_image_ref(image_ref); info!( "Tagging {} as {}:{}", @@ -382,8 +382,8 @@ mod tests { #[test] fn default_constants_are_consistent() { assert!( - DEFAULT_CLUSTER_IMAGE.starts_with(DEFAULT_IMAGE_REPO_BASE), - "cluster image should be under the default repo base" + DEFAULT_GATEWAY_IMAGE.starts_with(DEFAULT_IMAGE_REPO_BASE), + "gateway image should be under the default repo base" ); assert!( DEFAULT_IMAGE_REPO_BASE.starts_with(DEFAULT_REGISTRY), diff --git a/crates/navigator-bootstrap/src/kubeconfig.rs b/crates/navigator-bootstrap/src/kubeconfig.rs index c877739fe..01db49bf6 100644 --- a/crates/navigator-bootstrap/src/kubeconfig.rs +++ b/crates/navigator-bootstrap/src/kubeconfig.rs @@ -11,7 +11,7 @@ pub fn stored_kubeconfig_path(name: &str) -> Result { let base = xdg_config_dir()?; Ok(base .join("openshell") - .join("clusters") + .join("gateways") .join(name) .join("kubeconfig")) } @@ -100,14 +100,14 @@ pub fn store_kubeconfig(path: &Path, contents: &str) -> Result<()> { /// The kubeconfig points to `127.0.0.1:`, which works with an SSH tunnel: /// `ssh -L :127.0.0.1:6443 user@host` /// -/// The cluster name includes "-remote" suffix to distinguish from local clusters. +/// The gateway name includes "-remote" suffix to distinguish from local gateways. pub fn rewrite_kubeconfig_remote( contents: &str, - cluster_name: &str, + gateway_name: &str, _destination: &str, kube_port: Option, ) -> String { - let remote_name = format!("{cluster_name}-remote"); + let remote_name = format!("{gateway_name}-remote"); rewrite_kubeconfig(contents, &remote_name, kube_port) } @@ -116,7 +116,7 @@ pub fn rewrite_kubeconfig_remote( /// When `kube_port` is `Some`, the server URL is rewritten to /// `https://127.0.0.1:`. When `None`, the original server URL /// is left intact (the control plane is not exposed on the host). -pub fn rewrite_kubeconfig(contents: &str, cluster_name: &str, kube_port: Option) -> String { +pub fn rewrite_kubeconfig(contents: &str, gateway_name: &str, kube_port: Option) -> String { let mut replaced = Vec::new(); for line in contents.lines() { let trimmed = line.trim_start(); @@ -128,24 +128,24 @@ pub fn rewrite_kubeconfig(contents: &str, cluster_name: &str, kube_port: Option< replaced.push(format!("{indent}server: https://127.0.0.1:{kp}")); continue; } - // Rename default cluster/context/user to the cluster name + // Rename default cluster/context/user to the gateway name // Handle both "name: default" and "- name: default" (YAML list item) if trimmed == "name: default" || trimmed == "- name: default" { let indent_len = line.len() - trimmed.len(); let indent = &line[..indent_len]; let prefix = if trimmed.starts_with("- ") { "- " } else { "" }; - replaced.push(format!("{indent}{prefix}name: {cluster_name}")); + replaced.push(format!("{indent}{prefix}name: {gateway_name}")); continue; } if trimmed == "cluster: default" || trimmed == "user: default" { let indent_len = line.len() - trimmed.len(); let indent = &line[..indent_len]; let key = trimmed.split(':').next().unwrap_or("cluster"); - replaced.push(format!("{indent}{key}: {cluster_name}")); + replaced.push(format!("{indent}{key}: {gateway_name}")); continue; } if trimmed == "current-context: default" { - replaced.push(format!("current-context: {cluster_name}")); + replaced.push(format!("current-context: {gateway_name}")); continue; } replaced.push(line.to_string()); diff --git a/crates/navigator-bootstrap/src/lib.rs b/crates/navigator-bootstrap/src/lib.rs index d1a0f7cd8..214178c74 100644 --- a/crates/navigator-bootstrap/src/lib.rs +++ b/crates/navigator-bootstrap/src/lib.rs @@ -31,30 +31,30 @@ use crate::constants::{ volume_name, }; use crate::docker::{ - check_existing_cluster, create_ssh_docker_client, destroy_cluster_resources, ensure_container, + check_existing_gateway, create_ssh_docker_client, destroy_gateway_resources, ensure_container, ensure_image, ensure_network, ensure_volume, start_container, stop_container, }; use crate::kubeconfig::{rewrite_kubeconfig, rewrite_kubeconfig_remote, store_kubeconfig}; use crate::metadata::{ - create_cluster_metadata, create_cluster_metadata_with_host, extract_host_from_ssh_destination, + create_gateway_metadata, create_gateway_metadata_with_host, extract_host_from_ssh_destination, local_gateway_host, resolve_ssh_hostname, }; use crate::mtls::store_pki_bundle; use crate::pki::generate_pki; use crate::runtime::{ clean_stale_nodes, exec_capture_with_exit, fetch_recent_logs, navigator_workload_exists, - restart_navigator_deployment, wait_for_cluster_ready, wait_for_kubeconfig, + restart_navigator_deployment, wait_for_gateway_ready, wait_for_kubeconfig, }; -pub use crate::docker::ExistingClusterInfo; +pub use crate::docker::ExistingGatewayInfo; pub use crate::kubeconfig::{ default_local_kubeconfig_path, print_kubeconfig, stored_kubeconfig_path, update_local_kubeconfig, }; pub use crate::metadata::{ - ClusterMetadata, clear_active_cluster, get_cluster_metadata, list_clusters, - load_active_cluster, load_cluster_metadata, load_last_sandbox, remove_cluster_metadata, - save_active_cluster, save_last_sandbox, store_cluster_metadata, + GatewayMetadata, clear_active_gateway, get_gateway_metadata, list_gateways, + load_active_gateway, load_gateway_metadata, load_last_sandbox, remove_gateway_metadata, + save_active_gateway, save_last_sandbox, store_gateway_metadata, }; /// Options for remote SSH deployment. @@ -191,20 +191,20 @@ impl DeployOptions { } #[derive(Debug, Clone)] -pub struct ClusterHandle { +pub struct GatewayHandle { name: String, kubeconfig_path: PathBuf, - metadata: ClusterMetadata, + metadata: GatewayMetadata, docker: Docker, } -impl ClusterHandle { +impl GatewayHandle { pub fn kubeconfig_path(&self) -> &Path { &self.kubeconfig_path } - /// Get the cluster metadata. - pub fn metadata(&self) -> &ClusterMetadata { + /// Get the gateway metadata. + pub fn metadata(&self) -> &GatewayMetadata { &self.metadata } @@ -218,35 +218,35 @@ impl ClusterHandle { } pub async fn destroy(&self) -> Result<()> { - destroy_cluster_resources(&self.docker, &self.name, &self.kubeconfig_path).await + destroy_gateway_resources(&self.docker, &self.name, &self.kubeconfig_path).await } } -/// Check whether a cluster with the given name already has resources deployed. +/// Check whether a gateway with the given name already has resources deployed. /// -/// Returns `None` if no existing cluster resources are found, or -/// `Some(ExistingClusterInfo)` with details about what exists. +/// Returns `None` if no existing gateway resources are found, or +/// `Some(ExistingGatewayInfo)` with details about what exists. pub async fn check_existing_deployment( name: &str, remote: Option<&RemoteOptions>, -) -> Result> { +) -> Result> { let docker = match remote { Some(remote_opts) => create_ssh_docker_client(remote_opts).await?, None => Docker::connect_with_local_defaults().into_diagnostic()?, }; - check_existing_cluster(&docker, name).await + check_existing_gateway(&docker, name).await } -pub async fn deploy_cluster(options: DeployOptions) -> Result { - deploy_cluster_with_logs(options, |_| {}).await +pub async fn deploy_gateway(options: DeployOptions) -> Result { + deploy_gateway_with_logs(options, |_| {}).await } -pub async fn deploy_cluster_with_logs(options: DeployOptions, on_log: F) -> Result +pub async fn deploy_gateway_with_logs(options: DeployOptions, on_log: F) -> Result where F: FnMut(String) + Send + 'static, { let name = options.name; - let image_ref = options.image_ref.unwrap_or_else(default_cluster_image_ref); + let image_ref = options.image_ref.unwrap_or_else(default_gateway_image_ref); let port = options.port; let gateway_host = options.gateway_host; let kube_port = options.kube_port; @@ -450,17 +450,17 @@ where // Create a short-lived closure that locks on each call rather than holding // the MutexGuard across await points. let on_log_ref = Arc::clone(&on_log); - let mut cluster_log = move |msg: String| { + let mut gateway_log = move |msg: String| { if let Ok(mut f) = on_log_ref.lock() { f(msg); } }; - wait_for_cluster_ready(&target_docker, &name, &mut cluster_log).await?; + wait_for_gateway_ready(&target_docker, &name, &mut gateway_log).await?; } - // Create and store cluster metadata. + // Create and store gateway metadata. log("[progress] Persisting gateway metadata".to_string()); - let metadata = create_cluster_metadata_with_host( + let metadata = create_gateway_metadata_with_host( &name, remote_opts.as_ref(), port, @@ -468,9 +468,9 @@ where ssh_gateway_host.as_deref(), disable_tls, ); - store_cluster_metadata(&name, &metadata)?; + store_gateway_metadata(&name, &metadata)?; - Ok(ClusterHandle { + Ok(GatewayHandle { name, kubeconfig_path, metadata, @@ -478,11 +478,11 @@ where }) } -/// Get a handle to an existing cluster. +/// Get a handle to an existing gateway. /// -/// For local clusters, pass `None` for remote options. -/// For remote clusters, pass the same `RemoteOptions` used during deployment. -pub async fn cluster_handle(name: &str, remote: Option<&RemoteOptions>) -> Result { +/// For local gateways, pass `None` for remote options. +/// For remote gateways, pass the same `RemoteOptions` used during deployment. +pub async fn gateway_handle(name: &str, remote: Option<&RemoteOptions>) -> Result { let docker = match remote { Some(remote_opts) => create_ssh_docker_client(remote_opts).await?, None => Docker::connect_with_local_defaults().into_diagnostic()?, @@ -490,9 +490,9 @@ pub async fn cluster_handle(name: &str, remote: Option<&RemoteOptions>) -> Resul let kubeconfig_path = stored_kubeconfig_path(name)?; // Try to load existing metadata, fall back to creating new metadata // with the default ports (the actual ports are only known at deploy time). - let metadata = load_cluster_metadata(name) - .unwrap_or_else(|_| create_cluster_metadata(name, remote, DEFAULT_GATEWAY_PORT, None)); - Ok(ClusterHandle { + let metadata = load_gateway_metadata(name) + .unwrap_or_else(|_| create_gateway_metadata(name, remote, DEFAULT_GATEWAY_PORT, None)); + Ok(GatewayHandle { name: name.to_string(), kubeconfig_path, metadata, @@ -500,28 +500,28 @@ pub async fn cluster_handle(name: &str, remote: Option<&RemoteOptions>) -> Resul }) } -pub async fn ensure_cluster_image(version: &str, registry_token: Option<&str>) -> Result { +pub async fn ensure_gateway_image(version: &str, registry_token: Option<&str>) -> Result { let docker = Docker::connect_with_local_defaults().into_diagnostic()?; - let image_ref = format!("{}:{version}", image::DEFAULT_CLUSTER_IMAGE); + let image_ref = format!("{}:{version}", image::DEFAULT_GATEWAY_IMAGE); ensure_image(&docker, &image_ref, registry_token).await?; Ok(image_ref) } -fn default_cluster_image_ref() -> String { +fn default_gateway_image_ref() -> String { if let Ok(image) = std::env::var("OPENSHELL_CLUSTER_IMAGE") && !image.trim().is_empty() { return image; } - image::DEFAULT_CLUSTER_IMAGE.to_string() + image::DEFAULT_GATEWAY_IMAGE.to_string() } /// Create the three TLS K8s secrets required by the `OpenShell` server and sandbox pods. /// /// Secrets are created via `kubectl` exec'd inside the cluster container: -/// - `navigator-server-tls` (kubernetes.io/tls): server cert + key -/// - `navigator-server-client-ca` (Opaque): CA cert for verifying client certs -/// - `navigator-client-tls` (Opaque): client cert + key + CA cert (shared by CLI & sandboxes) +/// - `openshell-server-tls` (kubernetes.io/tls): server cert + key +/// - `openshell-server-client-ca` (Opaque): CA cert for verifying client certs +/// - `openshell-client-tls` (Opaque): client cert + key + CA cert (shared by CLI & sandboxes) async fn create_k8s_tls_secrets( docker: &Docker, name: &str, @@ -560,13 +560,13 @@ async fn create_k8s_tls_secrets( } }; - // 1. navigator-server-tls (kubernetes.io/tls) + // 1. openshell-server-tls (kubernetes.io/tls) let server_tls_manifest = serde_json::json!({ "apiVersion": "v1", "kind": "Secret", "metadata": { "name": SERVER_TLS_SECRET_NAME, - "namespace": "navigator" + "namespace": "openshell" }, "type": "kubernetes.io/tls", "data": { @@ -576,15 +576,15 @@ async fn create_k8s_tls_secrets( }); apply_secret(server_tls_manifest.to_string()) .await - .wrap_err("failed to create navigator-server-tls secret")?; + .wrap_err("failed to create openshell-server-tls secret")?; - // 2. navigator-server-client-ca (Opaque) + // 2. openshell-server-client-ca (Opaque) let client_ca_manifest = serde_json::json!({ "apiVersion": "v1", "kind": "Secret", "metadata": { "name": SERVER_CLIENT_CA_SECRET_NAME, - "namespace": "navigator" + "namespace": "openshell" }, "type": "Opaque", "data": { @@ -593,15 +593,15 @@ async fn create_k8s_tls_secrets( }); apply_secret(client_ca_manifest.to_string()) .await - .wrap_err("failed to create navigator-server-client-ca secret")?; + .wrap_err("failed to create openshell-server-client-ca secret")?; - // 3. navigator-client-tls (Opaque) — shared by CLI and sandbox pods + // 3. openshell-client-tls (Opaque) — shared by CLI and sandbox pods let client_tls_manifest = serde_json::json!({ "apiVersion": "v1", "kind": "Secret", "metadata": { "name": CLIENT_TLS_SECRET_NAME, - "namespace": "navigator" + "namespace": "openshell" }, "type": "Opaque", "data": { @@ -612,15 +612,15 @@ async fn create_k8s_tls_secrets( }); apply_secret(client_tls_manifest.to_string()) .await - .wrap_err("failed to create navigator-client-tls secret")?; + .wrap_err("failed to create openshell-client-tls secret")?; Ok(()) } -/// Reconcile cluster TLS secrets: reuse existing PKI if valid, generate new if needed. +/// Reconcile gateway TLS secrets: reuse existing PKI if valid, generate new if needed. /// /// Returns `(bundle, rotated)` where `rotated` is true if new PKI was generated -/// and applied to the cluster (meaning the server needs a restart to pick it up). +/// and applied to the gateway (meaning the server needs a restart to pick it up). async fn reconcile_pki( docker: &Docker, name: &str, @@ -651,8 +651,8 @@ where // Generate fresh PKI and apply to cluster. // Namespace may still be creating on first bootstrap, so wait here only // when rotation is actually needed. - log("[progress] Waiting for navigator namespace".to_string()); - wait_for_namespace(docker, &cname, kubeconfig, "navigator").await?; + log("[progress] Waiting for openshell namespace".to_string()); + wait_for_namespace(docker, &cname, kubeconfig, "openshell").await?; log("[progress] Generating TLS certificates".to_string()); let bundle = generate_pki(extra_sans)?; log("[progress] Applying TLS secrets to gateway".to_string()); @@ -776,7 +776,7 @@ async fn wait_for_namespace( { let logs = fetch_recent_logs(docker, container_name, 40).await; return Err(miette::miette!( - "cluster container is not running while waiting for namespace '{namespace}': {status_err}\n{logs}" + "gateway container is not running while waiting for namespace '{namespace}': {status_err}\n{logs}" )) .wrap_err("K8s namespace not ready"); } diff --git a/crates/navigator-bootstrap/src/metadata.rs b/crates/navigator-bootstrap/src/metadata.rs index daf96ea08..62d9da0ca 100644 --- a/crates/navigator-bootstrap/src/metadata.rs +++ b/crates/navigator-bootstrap/src/metadata.rs @@ -2,19 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 use crate::RemoteOptions; -use crate::paths::{active_cluster_path, clusters_dir, last_sandbox_path, xdg_config_dir}; +use crate::paths::{active_gateway_path, gateways_dir, last_sandbox_path}; use miette::{IntoDiagnostic, Result, WrapErr}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -/// Cluster metadata stored alongside the kubeconfig. +/// Gateway metadata stored alongside the kubeconfig. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ClusterMetadata { - /// The cluster name. +pub struct GatewayMetadata { + /// The gateway name. pub name: String, /// Gateway endpoint URL (e.g., `https://127.0.0.1:8080`). pub gateway_endpoint: String, - /// Whether this is a remote cluster. + /// Whether this is a remote gateway. pub is_remote: bool, /// Host port mapped to the gateway `NodePort`. pub gateway_port: u16, @@ -23,10 +23,10 @@ pub struct ClusterMetadata { /// Old metadata files without this field are deserialized as `None`. #[serde(default, skip_serializing_if = "Option::is_none")] pub kube_port: Option, - /// For remote clusters, the SSH destination (e.g., `user@hostname`). + /// For remote gateways, the SSH destination (e.g., `user@hostname`). #[serde(skip_serializing_if = "Option::is_none")] pub remote_host: Option, - /// For remote clusters, the resolved hostname/IP from SSH config. + /// For remote gateways, the resolved hostname/IP from SSH config. #[serde(skip_serializing_if = "Option::is_none", default)] pub resolved_host: Option, @@ -51,32 +51,32 @@ pub struct ClusterMetadata { pub edge_auth_url: Option, } -pub fn create_cluster_metadata( +pub fn create_gateway_metadata( name: &str, remote: Option<&RemoteOptions>, port: u16, kube_port: Option, -) -> ClusterMetadata { - create_cluster_metadata_with_host(name, remote, port, kube_port, None, false) +) -> GatewayMetadata { + create_gateway_metadata_with_host(name, remote, port, kube_port, None, false) } -/// Create cluster metadata, optionally overriding the gateway host. +/// Create gateway metadata, optionally overriding the gateway host. /// /// When `gateway_host` is `Some`, that value is used as the host portion of -/// `gateway_endpoint` instead of the default (`127.0.0.1` for local clusters, -/// or the resolved SSH host for remote clusters). +/// `gateway_endpoint` instead of the default (`127.0.0.1` for local gateways, +/// or the resolved SSH host for remote gateways). /// /// When `disable_tls` is `true`, the gateway endpoint uses the `http://` /// scheme instead of `https://`. This must match the server configuration /// so that the CLI connects with the correct protocol. -pub fn create_cluster_metadata_with_host( +pub fn create_gateway_metadata_with_host( name: &str, remote: Option<&RemoteOptions>, port: u16, kube_port: Option, gateway_host: Option<&str>, disable_tls: bool, -) -> ClusterMetadata { +) -> GatewayMetadata { let scheme = if disable_tls { "http" } else { "https" }; let (gateway_endpoint, is_remote, remote_host, resolved_host) = remote.map_or_else( @@ -103,7 +103,7 @@ pub fn create_cluster_metadata_with_host( }, ); - ClusterMetadata { + GatewayMetadata { name: name.to_string(), gateway_endpoint, is_remote, @@ -146,11 +146,7 @@ pub fn local_gateway_host_from_docker_host(docker_host: &str) -> Option } fn stored_metadata_path(name: &str) -> Result { - let base = xdg_config_dir()?; - Ok(base - .join("openshell") - .join("clusters") - .join(format!("{name}_metadata.json"))) + Ok(gateways_dir()?.join(name).join("metadata.json")) } /// Extract the hostname from an SSH destination string. @@ -215,7 +211,7 @@ pub fn resolve_ssh_hostname(host: &str) -> String { } } -pub fn store_cluster_metadata(name: &str, metadata: &ClusterMetadata) -> Result<()> { +pub fn store_gateway_metadata(name: &str, metadata: &GatewayMetadata) -> Result<()> { let path = stored_metadata_path(name)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) @@ -224,31 +220,31 @@ pub fn store_cluster_metadata(name: &str, metadata: &ClusterMetadata) -> Result< } let contents = serde_json::to_string_pretty(metadata) .into_diagnostic() - .wrap_err("failed to serialize cluster metadata")?; + .wrap_err("failed to serialize gateway metadata")?; std::fs::write(&path, contents) .into_diagnostic() .wrap_err_with(|| format!("failed to write metadata to {}", path.display()))?; Ok(()) } -pub fn load_cluster_metadata(name: &str) -> Result { +pub fn load_gateway_metadata(name: &str) -> Result { let path = stored_metadata_path(name)?; let contents = std::fs::read_to_string(&path) .into_diagnostic() .wrap_err_with(|| format!("failed to read metadata from {}", path.display()))?; serde_json::from_str(&contents) .into_diagnostic() - .wrap_err("failed to parse cluster metadata") + .wrap_err("failed to parse gateway metadata") } -/// Load cluster metadata if available. -pub fn get_cluster_metadata(name: &str) -> Option { - load_cluster_metadata(name).ok() +/// Load gateway metadata if available. +pub fn get_gateway_metadata(name: &str) -> Option { + load_gateway_metadata(name).ok() } -/// Save the active cluster name to persistent storage. -pub fn save_active_cluster(name: &str) -> Result<()> { - let path = active_cluster_path()?; +/// Save the active gateway name to persistent storage. +pub fn save_active_gateway(name: &str) -> Result<()> { + let path = active_gateway_path()?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .into_diagnostic() @@ -256,23 +252,23 @@ pub fn save_active_cluster(name: &str) -> Result<()> { } std::fs::write(&path, name) .into_diagnostic() - .wrap_err_with(|| format!("failed to write active cluster to {}", path.display()))?; + .wrap_err_with(|| format!("failed to write active gateway to {}", path.display()))?; Ok(()) } -/// Load the active cluster name from persistent storage. +/// Load the active gateway name from persistent storage. /// -/// Returns `None` if no active cluster has been set. -pub fn load_active_cluster() -> Option { - let path = active_cluster_path().ok()?; +/// Returns `None` if no active gateway has been set. +pub fn load_active_gateway() -> Option { + let path = active_gateway_path().ok()?; let contents = std::fs::read_to_string(&path).ok()?; let name = contents.trim().to_string(); if name.is_empty() { None } else { Some(name) } } -/// Save the last-used sandbox name for a cluster to persistent storage. -pub fn save_last_sandbox(cluster: &str, sandbox: &str) -> Result<()> { - let path = last_sandbox_path(cluster)?; +/// Save the last-used sandbox name for a gateway to persistent storage. +pub fn save_last_sandbox(gateway: &str, sandbox: &str) -> Result<()> { + let path = last_sandbox_path(gateway)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .into_diagnostic() @@ -284,50 +280,51 @@ pub fn save_last_sandbox(cluster: &str, sandbox: &str) -> Result<()> { Ok(()) } -/// Load the last-used sandbox name for a cluster from persistent storage. +/// Load the last-used sandbox name for a gateway from persistent storage. /// /// Returns `None` if no last sandbox has been set. -pub fn load_last_sandbox(cluster: &str) -> Option { - let path = last_sandbox_path(cluster).ok()?; +pub fn load_last_sandbox(gateway: &str) -> Option { + let path = last_sandbox_path(gateway).ok()?; let contents = std::fs::read_to_string(&path).ok()?; let name = contents.trim().to_string(); if name.is_empty() { None } else { Some(name) } } -/// List all clusters that have stored metadata. +/// List all gateways that have stored metadata. /// -/// Scans `$XDG_CONFIG_HOME/navigator/clusters/` for `*_metadata.json` files -/// and returns the parsed metadata for each. -pub fn list_clusters() -> Result> { - let dir = clusters_dir()?; +/// Scans `$XDG_CONFIG_HOME/openshell/gateways/` for subdirectories containing +/// `metadata.json` and returns the parsed metadata for each. +pub fn list_gateways() -> Result> { + let dir = gateways_dir()?; if !dir.exists() { return Ok(Vec::new()); } - let mut clusters = Vec::new(); + let mut gateways = Vec::new(); let entries = std::fs::read_dir(&dir) .into_diagnostic() .wrap_err_with(|| format!("failed to read directory {}", dir.display()))?; for entry in entries { let entry = entry.into_diagnostic()?; - let file_name = entry.file_name(); - let name_str = file_name.to_string_lossy(); - if let Some(cluster_name) = name_str.strip_suffix("_metadata.json") - && let Ok(metadata) = load_cluster_metadata(cluster_name) - { - clusters.push(metadata); + let path = entry.path(); + // Only consider directories that contain a metadata.json file + if path.is_dir() { + let gateway_name = entry.file_name().to_string_lossy().to_string(); + if let Ok(metadata) = load_gateway_metadata(&gateway_name) { + gateways.push(metadata); + } } } // Sort by name for stable output - clusters.sort_by(|a, b| a.name.cmp(&b.name)); - Ok(clusters) + gateways.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(gateways) } -/// Remove the active cluster file (used when destroying the active cluster). -pub fn clear_active_cluster() -> Result<()> { - let path = active_cluster_path()?; +/// Remove the active gateway file (used when destroying the active gateway). +pub fn clear_active_gateway() -> Result<()> { + let path = active_gateway_path()?; if path.exists() { std::fs::remove_file(&path) .into_diagnostic() @@ -336,8 +333,8 @@ pub fn clear_active_cluster() -> Result<()> { Ok(()) } -/// Remove cluster metadata file. -pub fn remove_cluster_metadata(name: &str) -> Result<()> { +/// Remove gateway metadata file. +pub fn remove_gateway_metadata(name: &str) -> Result<()> { let path = stored_metadata_path(name)?; if path.exists() { std::fs::remove_file(&path) @@ -381,8 +378,8 @@ mod tests { } #[test] - fn local_cluster_metadata() { - let meta = create_cluster_metadata("test", None, 8080, None); + fn local_gateway_metadata() { + let meta = create_gateway_metadata("test", None, 8080, None); assert_eq!(meta.name, "test"); assert_eq!(meta.gateway_endpoint, "https://127.0.0.1:8080"); assert_eq!(meta.gateway_port, 8080); @@ -393,21 +390,21 @@ mod tests { } #[test] - fn local_cluster_metadata_custom_port() { - let meta = create_cluster_metadata("test", None, 9090, None); + fn local_gateway_metadata_custom_port() { + let meta = create_gateway_metadata("test", None, 9090, None); assert_eq!(meta.gateway_endpoint, "https://127.0.0.1:9090"); assert_eq!(meta.gateway_port, 9090); } #[test] - fn local_cluster_metadata_with_kube_port() { - let meta = create_cluster_metadata("test", None, 8080, Some(7443)); + fn local_gateway_metadata_with_kube_port() { + let meta = create_gateway_metadata("test", None, 8080, Some(7443)); assert_eq!(meta.kube_port, Some(7443)); } #[test] - fn local_cluster_metadata_without_kube_port() { - let meta = create_cluster_metadata("test", None, 8080, None); + fn local_gateway_metadata_without_kube_port() { + let meta = create_gateway_metadata("test", None, 8080, None); assert!(meta.kube_port.is_none()); } @@ -430,9 +427,9 @@ mod tests { } #[test] - fn remote_cluster_metadata_has_resolved_host() { + fn remote_gateway_metadata_has_resolved_host() { let opts = RemoteOptions::new("user@10.0.0.5"); - let meta = create_cluster_metadata("test", Some(&opts), 8080, Some(6443)); + let meta = create_gateway_metadata("test", Some(&opts), 8080, Some(6443)); assert!(meta.is_remote); assert_eq!(meta.remote_host.as_deref(), Some("user@10.0.0.5")); // When the host is a plain IP, ssh -G should resolve it to itself @@ -447,7 +444,7 @@ mod tests { #[test] fn metadata_roundtrip_with_kube_port() { - let meta = ClusterMetadata { + let meta = GatewayMetadata { name: "test".to_string(), gateway_endpoint: "https://10.0.0.5:8080".to_string(), is_remote: true, @@ -460,7 +457,7 @@ mod tests { edge_auth_url: None, }; let json = serde_json::to_string(&meta).unwrap(); - let parsed: ClusterMetadata = serde_json::from_str(&json).unwrap(); + let parsed: GatewayMetadata = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.resolved_host.as_deref(), Some("10.0.0.5")); assert_eq!(parsed.gateway_endpoint, "https://10.0.0.5:8080"); assert_eq!(parsed.gateway_port, 8080); @@ -469,7 +466,7 @@ mod tests { #[test] fn metadata_roundtrip_without_kube_port() { - let meta = ClusterMetadata { + let meta = GatewayMetadata { name: "test".to_string(), gateway_endpoint: "https://10.0.0.5:8080".to_string(), is_remote: true, @@ -486,7 +483,7 @@ mod tests { !json.contains("kube_port"), "None should be omitted from JSON" ); - let parsed: ClusterMetadata = serde_json::from_str(&json).unwrap(); + let parsed: GatewayMetadata = serde_json::from_str(&json).unwrap(); assert!(parsed.kube_port.is_none()); } @@ -501,15 +498,15 @@ mod tests { "gateway_port": 8080, "remote_host": "user@myserver" }"#; - let parsed: ClusterMetadata = serde_json::from_str(json).unwrap(); + let parsed: GatewayMetadata = serde_json::from_str(json).unwrap(); assert!(parsed.resolved_host.is_none()); // kube_port should default to None when not present in JSON assert!(parsed.kube_port.is_none()); } #[test] - fn local_cluster_metadata_with_gateway_host_override() { - let meta = create_cluster_metadata_with_host( + fn local_gateway_metadata_with_gateway_host_override() { + let meta = create_gateway_metadata_with_host( "test", None, 8080, @@ -526,21 +523,21 @@ mod tests { } #[test] - fn local_cluster_metadata_with_no_gateway_host_override() { - // When gateway_host is None, behaviour matches create_cluster_metadata. - let meta = create_cluster_metadata_with_host("test", None, 8080, None, None, false); + fn local_gateway_metadata_with_no_gateway_host_override() { + // When gateway_host is None, behaviour matches create_gateway_metadata. + let meta = create_gateway_metadata_with_host("test", None, 8080, None, None, false); assert_eq!(meta.gateway_endpoint, "https://127.0.0.1:8080"); } #[test] - fn local_cluster_metadata_with_tls_disabled() { - let meta = create_cluster_metadata_with_host("test", None, 8080, None, None, true); + fn local_gateway_metadata_with_tls_disabled() { + let meta = create_gateway_metadata_with_host("test", None, 8080, None, None, true); assert_eq!(meta.gateway_endpoint, "http://127.0.0.1:8080"); } #[test] - fn local_cluster_metadata_with_tls_disabled_and_gateway_host() { - let meta = create_cluster_metadata_with_host( + fn local_gateway_metadata_with_tls_disabled_and_gateway_host() { + let meta = create_gateway_metadata_with_host( "test", None, 8080, @@ -552,10 +549,10 @@ mod tests { } #[test] - fn remote_cluster_metadata_with_tls_disabled() { + fn remote_gateway_metadata_with_tls_disabled() { let opts = RemoteOptions::new("user@10.0.0.5"); let meta = - create_cluster_metadata_with_host("test", Some(&opts), 8080, Some(6443), None, true); + create_gateway_metadata_with_host("test", Some(&opts), 8080, Some(6443), None, true); assert!(meta.is_remote); assert!(meta.gateway_endpoint.starts_with("http://")); assert!(!meta.gateway_endpoint.starts_with("https://")); @@ -587,8 +584,8 @@ mod tests { fn save_and_load_last_sandbox_roundtrip() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - save_last_sandbox("mycluster", "dev-box").unwrap(); - assert_eq!(load_last_sandbox("mycluster"), Some("dev-box".to_string())); + save_last_sandbox("mygateway", "dev-box").unwrap(); + assert_eq!(load_last_sandbox("mygateway"), Some("dev-box".to_string())); }); } @@ -596,7 +593,7 @@ mod tests { fn load_last_sandbox_returns_none_when_not_set() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - assert_eq!(load_last_sandbox("no-such-cluster"), None); + assert_eq!(load_last_sandbox("no-such-gateway"), None); }); } @@ -604,9 +601,9 @@ mod tests { fn save_last_sandbox_overwrites_previous() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - save_last_sandbox("c1", "first").unwrap(); - save_last_sandbox("c1", "second").unwrap(); - assert_eq!(load_last_sandbox("c1"), Some("second".to_string())); + save_last_sandbox("g1", "first").unwrap(); + save_last_sandbox("g1", "second").unwrap(); + assert_eq!(load_last_sandbox("g1"), Some("second".to_string())); }); } @@ -614,10 +611,10 @@ mod tests { fn save_last_sandbox_creates_parent_dirs() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - // The cluster subdir doesn't exist yet — save should create it. - save_last_sandbox("brand-new-cluster", "sb1").unwrap(); + // The gateway subdir doesn't exist yet — save should create it. + save_last_sandbox("brand-new-gateway", "sb1").unwrap(); assert_eq!( - load_last_sandbox("brand-new-cluster"), + load_last_sandbox("brand-new-gateway"), Some("sb1".to_string()) ); }); @@ -628,10 +625,10 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { // Write the file manually with surrounding whitespace. - let path = last_sandbox_path("ws-cluster").unwrap(); + let path = last_sandbox_path("ws-gateway").unwrap(); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, " my-sb \n").unwrap(); - assert_eq!(load_last_sandbox("ws-cluster"), Some("my-sb".to_string())); + assert_eq!(load_last_sandbox("ws-gateway"), Some("my-sb".to_string())); }); } @@ -639,25 +636,25 @@ mod tests { fn load_last_sandbox_returns_none_for_empty_file() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - let path = last_sandbox_path("empty-cluster").unwrap(); + let path = last_sandbox_path("empty-gateway").unwrap(); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, " \n").unwrap(); - assert_eq!(load_last_sandbox("empty-cluster"), None); + assert_eq!(load_last_sandbox("empty-gateway"), None); }); } #[test] - fn last_sandbox_is_per_cluster() { + fn last_sandbox_is_per_gateway() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - save_last_sandbox("cluster-a", "sandbox-a").unwrap(); - save_last_sandbox("cluster-b", "sandbox-b").unwrap(); + save_last_sandbox("gateway-a", "sandbox-a").unwrap(); + save_last_sandbox("gateway-b", "sandbox-b").unwrap(); assert_eq!( - load_last_sandbox("cluster-a"), + load_last_sandbox("gateway-a"), Some("sandbox-a".to_string()) ); assert_eq!( - load_last_sandbox("cluster-b"), + load_last_sandbox("gateway-b"), Some("sandbox-b".to_string()) ); }); diff --git a/crates/navigator-bootstrap/src/mtls.rs b/crates/navigator-bootstrap/src/mtls.rs index 84b801d57..3767a639a 100644 --- a/crates/navigator-bootstrap/src/mtls.rs +++ b/crates/navigator-bootstrap/src/mtls.rs @@ -72,7 +72,7 @@ pub fn store_pki_bundle(name: &str, bundle: &PkiBundle) -> Result<()> { fn cli_mtls_dir(name: &str) -> Result { Ok(xdg_config_dir()? .join("openshell") - .join("clusters") + .join("gateways") .join(name) .join("mtls")) } diff --git a/crates/navigator-bootstrap/src/paths.rs b/crates/navigator-bootstrap/src/paths.rs index 22ef36190..84bd06e30 100644 --- a/crates/navigator-bootstrap/src/paths.rs +++ b/crates/navigator-bootstrap/src/paths.rs @@ -14,25 +14,25 @@ pub fn xdg_config_dir() -> Result { Ok(PathBuf::from(home).join(".config")) } -/// Path to the file that stores the active cluster name. +/// Path to the file that stores the active gateway name. /// -/// Location: `$XDG_CONFIG_HOME/openshell/active_cluster` -pub fn active_cluster_path() -> Result { - Ok(xdg_config_dir()?.join("openshell").join("active_cluster")) +/// Location: `$XDG_CONFIG_HOME/openshell/active_gateway` +pub fn active_gateway_path() -> Result { + Ok(xdg_config_dir()?.join("openshell").join("active_gateway")) } -/// Base directory for all cluster metadata files. +/// Base directory for all gateway metadata files. /// -/// Location: `$XDG_CONFIG_HOME/openshell/clusters/` -pub fn clusters_dir() -> Result { - Ok(xdg_config_dir()?.join("openshell").join("clusters")) +/// Location: `$XDG_CONFIG_HOME/openshell/gateways/` +pub fn gateways_dir() -> Result { + Ok(xdg_config_dir()?.join("openshell").join("gateways")) } -/// Path to the file that stores the last-used sandbox name for a cluster. +/// Path to the file that stores the last-used sandbox name for a gateway. /// -/// Location: `$XDG_CONFIG_HOME/openshell/clusters//last_sandbox` -pub fn last_sandbox_path(cluster: &str) -> Result { - Ok(clusters_dir()?.join(cluster).join("last_sandbox")) +/// Location: `$XDG_CONFIG_HOME/openshell/gateways//last_sandbox` +pub fn last_sandbox_path(gateway: &str) -> Result { + Ok(gateways_dir()?.join(gateway).join("last_sandbox")) } #[cfg(test)] @@ -50,9 +50,9 @@ mod tests { unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()); } - let path = last_sandbox_path("my-cluster").unwrap(); + let path = last_sandbox_path("my-gateway").unwrap(); assert!( - path.ends_with("openshell/clusters/my-cluster/last_sandbox"), + path.ends_with("openshell/gateways/my-gateway/last_sandbox"), "unexpected path: {path:?}" ); unsafe { diff --git a/crates/navigator-bootstrap/src/pki.rs b/crates/navigator-bootstrap/src/pki.rs index 3abd23e2b..537030af4 100644 --- a/crates/navigator-bootstrap/src/pki.rs +++ b/crates/navigator-bootstrap/src/pki.rs @@ -19,9 +19,9 @@ pub struct PkiBundle { /// Default SANs always included on the server certificate. const DEFAULT_SERVER_SANS: &[&str] = &[ - "navigator", - "navigator.navigator.svc", - "navigator.navigator.svc.cluster.local", + "openshell", + "openshell.openshell.svc", + "openshell.openshell.svc.cluster.local", "localhost", "host.docker.internal", "127.0.0.1", @@ -46,10 +46,10 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); ca_params .distinguished_name - .push(DnType::OrganizationName, "navigator"); + .push(DnType::OrganizationName, "openshell"); ca_params .distinguished_name - .push(DnType::CommonName, "navigator-ca"); + .push(DnType::CommonName, "openshell-ca"); let ca_cert = ca_params .self_signed(&ca_key) @@ -67,7 +67,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { server_params.subject_alt_names = server_sans; server_params .distinguished_name - .push(DnType::CommonName, "navigator-server"); + .push(DnType::CommonName, "openshell-server"); let server_cert = server_params .signed_by(&server_key, &ca_cert, &ca_key) diff --git a/crates/navigator-bootstrap/src/push.rs b/crates/navigator-bootstrap/src/push.rs index 0e3f39ba3..8fafc33fe 100644 --- a/crates/navigator-bootstrap/src/push.rs +++ b/crates/navigator-bootstrap/src/push.rs @@ -1,11 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Push locally-built images into a k3s cluster's containerd runtime. +//! Push locally-built images into a k3s gateway's containerd runtime. //! //! This module implements the "push" path for local development: images are //! exported from the local Docker daemon (equivalent to `docker save`), -//! uploaded into the cluster container as a tar file via the Docker +//! uploaded into the gateway container as a tar file via the Docker //! `put_archive` API, and then imported into containerd via `ctr images import`. //! //! The standalone `ctr` binary is used (not `k3s ctr` which may not work in @@ -26,14 +26,14 @@ const CONTAINERD_SOCK: &str = "/run/k3s/containerd/containerd.sock"; /// Path inside the container where the image tar is staged. const IMPORT_TAR_PATH: &str = "/tmp/navigator-images.tar"; -/// Push a list of images from the local Docker daemon into a k3s cluster's +/// Push a list of images from the local Docker daemon into a k3s gateway's /// containerd runtime. /// /// All images are exported as a single tar (shared layers are deduplicated), /// uploaded to the container filesystem, and imported into containerd. pub async fn push_local_images( local_docker: &Docker, - cluster_docker: &Docker, + gateway_docker: &Docker, container_name: &str, images: &[&str], on_log: &mut impl FnMut(String), @@ -43,7 +43,7 @@ pub async fn push_local_images( } on_log(format!( - "[status] Importing {} component image(s) into cluster", + "[status] Importing {} component image(s) into gateway", images.len() )); for img in images { @@ -60,14 +60,14 @@ pub async fn push_local_images( // 2. Wrap the image tar as a file inside an outer tar archive and upload // it into the container filesystem via the Docker put_archive API. - on_log("[status] Uploading images into cluster container".to_string()); + on_log("[status] Uploading images into gateway container".to_string()); let outer_tar = wrap_in_tar(IMPORT_TAR_PATH, &image_tar)?; - upload_archive(cluster_docker, container_name, &outer_tar).await?; + upload_archive(gateway_docker, container_name, &outer_tar).await?; // 3. Import the tar into containerd via ctr. on_log("[status] Running ctr images import".to_string()); let (output, exit_code) = exec_capture_with_exit( - cluster_docker, + gateway_docker, container_name, vec![ "ctr".to_string(), @@ -90,7 +90,7 @@ pub async fn push_local_images( // 4. Clean up the staged tar file. let _ = exec_capture_with_exit( - cluster_docker, + gateway_docker, container_name, vec![ "rm".to_string(), @@ -100,7 +100,7 @@ pub async fn push_local_images( ) .await; - on_log("[status] All component images imported into cluster".to_string()); + on_log("[status] All component images imported into gateway".to_string()); Ok(()) } diff --git a/crates/navigator-bootstrap/src/runtime.rs b/crates/navigator-bootstrap/src/runtime.rs index b80c0866c..fdb010cfe 100644 --- a/crates/navigator-bootstrap/src/runtime.rs +++ b/crates/navigator-bootstrap/src/runtime.rs @@ -23,7 +23,7 @@ pub async fn wait_for_kubeconfig(docker: &Docker, name: &str) -> Result { let logs = fetch_recent_logs(docker, &container_name, 20).await; return Err(miette::miette!( - "cluster container is not running while waiting for kubeconfig: {status_err}\n{logs}" + "gateway container is not running while waiting for kubeconfig: {status_err}\n{logs}" )); } @@ -51,7 +51,7 @@ pub async fn wait_for_kubeconfig(docker: &Docker, name: &str) -> Result Err(miette::miette!("timed out waiting for kubeconfig\n{logs}")) } -pub async fn wait_for_cluster_ready(docker: &Docker, name: &str, mut on_log: F) -> Result<()> +pub async fn wait_for_gateway_ready(docker: &Docker, name: &str, mut on_log: F) -> Result<()> where F: FnMut(String) + Send, { @@ -94,7 +94,7 @@ where .and_then(|s| s.error.as_deref()) .unwrap_or(""); let mut detail = - format!("cluster container exited unexpectedly (exit_code={exit_code})"); + format!("gateway container exited unexpectedly (exit_code={exit_code})"); if !error_msg.is_empty() { use std::fmt::Write; let _ = write!(detail, ", error={error_msg}"); @@ -118,14 +118,14 @@ where } Some(HealthStatusEnum::UNHEALTHY) if attempt + 1 == attempts => { result = Some(Err(miette::miette!( - "cluster health check reported unhealthy\n{}", + "gateway health check reported unhealthy\n{}", format_recent_logs(&recent_logs) ))); break; } Some(HealthStatusEnum::NONE | HealthStatusEnum::EMPTY) | None if attempt == 0 => { result = Some(Err(miette::miette!( - "cluster container does not expose a health check\n{}", + "gateway container does not expose a health check\n{}", format_recent_logs(&recent_logs) ))); break; @@ -139,14 +139,14 @@ where if result.is_none() { drain_logs(&mut log_rx, &mut recent_logs, &mut on_log); result = Some(Err(miette::miette!( - "timed out waiting for cluster health check\n{}", + "timed out waiting for gateway health check\n{}", format_recent_logs(&recent_logs) ))); } log_task.abort(); - result.unwrap_or_else(|| Err(miette::miette!("cluster health status unavailable"))) + result.unwrap_or_else(|| Err(miette::miette!("gateway health status unavailable"))) } async fn stream_container_logs( diff --git a/crates/navigator-cli/src/bootstrap.rs b/crates/navigator-cli/src/bootstrap.rs index 7bf84c160..8667ce2d5 100644 --- a/crates/navigator-cli/src/bootstrap.rs +++ b/crates/navigator-cli/src/bootstrap.rs @@ -3,8 +3,8 @@ //! Auto-bootstrap helpers for sandbox creation. //! -//! When `sandbox create` cannot reach a cluster, these helpers determine whether -//! to offer cluster bootstrap, prompt the user for confirmation, and execute the +//! When `sandbox create` cannot reach a gateway, these helpers determine whether +//! to offer gateway bootstrap, prompt the user for confirmation, and execute the //! local or remote bootstrap flow. use crate::tls::TlsOptions; @@ -13,28 +13,28 @@ use miette::Result; use owo_colors::OwoColorize; use std::io::IsTerminal; -use crate::run::{deploy_cluster_with_panel, print_deploy_summary}; +use crate::run::{deploy_gateway_with_panel, print_deploy_summary}; -/// Default cluster name used during auto-bootstrap. -const DEFAULT_CLUSTER_NAME: &str = "openshell"; +/// Default gateway name used during auto-bootstrap. +const DEFAULT_GATEWAY_NAME: &str = "openshell"; -/// Determines if a gRPC connection error indicates the cluster is unreachable +/// Determines if a gRPC connection error indicates the gateway is unreachable /// and bootstrap should be offered. /// /// Returns `true` for connectivity errors (connection refused, timeout, DNS failure) -/// and for missing default TLS materials (which implies no cluster has been deployed). +/// and for missing default TLS materials (which implies no gateway has been deployed). /// /// Returns `false` for explicit TLS configuration errors, auth failures, and other /// non-connectivity issues. pub fn should_attempt_bootstrap(error: &miette::Report, tls: &TlsOptions) -> bool { // If TLS paths were explicitly provided (e.g. in tests) and they failed, - // that's a configuration error, not a missing-cluster situation. + // that's a configuration error, not a missing-gateway situation. if tls.has_any() { return is_connectivity_error(error); } // With no explicit TLS options, missing default cert files strongly implies - // no cluster has been bootstrapped yet. + // no gateway has been bootstrapped yet. let msg = format!("{error:?}"); if is_missing_tls_material(&msg) { return true; @@ -95,7 +95,7 @@ fn is_connectivity_error(error: &miette::Report) -> bool { connectivity_patterns.iter().any(|p| lower.contains(p)) } -/// Prompt the user to confirm cluster bootstrap. +/// Prompt the user to confirm gateway bootstrap. /// /// When `override_value` is `Some(true)` or `Some(false)`, the decision is /// made immediately (from `--bootstrap` / `--no-bootstrap`). Otherwise, @@ -126,7 +126,7 @@ pub fn confirm_bootstrap(override_value: Option) -> Result { Ok(confirmed) } -/// Bootstrap a local cluster and return refreshed TLS options that pick up the +/// Bootstrap a local gateway and return refreshed TLS options that pick up the /// newly-written mTLS certificates. pub async fn run_bootstrap( remote: Option<&str>, @@ -134,7 +134,7 @@ pub async fn run_bootstrap( ) -> Result<(TlsOptions, String)> { let location = if remote.is_some() { "remote" } else { "local" }; - let mut options = navigator_bootstrap::DeployOptions::new(DEFAULT_CLUSTER_NAME); + let mut options = navigator_bootstrap::DeployOptions::new(DEFAULT_GATEWAY_NAME); if let Some(dest) = remote { let mut remote_opts = navigator_bootstrap::RemoteOptions::new(dest); if let Some(key) = ssh_key { @@ -143,7 +143,7 @@ pub async fn run_bootstrap( options = options.with_remote(remote_opts); } // Read registry token from environment for the auto-bootstrap path. - // The explicit `--registry-token` flag is only on `cluster admin deploy`; + // The explicit `--registry-token` flag is only on `gateway start`; // when bootstrapping via `sandbox create`, the env var is the mechanism. if let Ok(token) = std::env::var("NEMOCLAW_REGISTRY_TOKEN") && !token.trim().is_empty() @@ -151,20 +151,20 @@ pub async fn run_bootstrap( options = options.with_registry_token(token); } - let handle = deploy_cluster_with_panel(options, DEFAULT_CLUSTER_NAME, location).await?; + let handle = deploy_gateway_with_panel(options, DEFAULT_GATEWAY_NAME, location).await?; let server = handle.gateway_endpoint().to_string(); - print_deploy_summary(DEFAULT_CLUSTER_NAME, &handle); + print_deploy_summary(DEFAULT_GATEWAY_NAME, &handle); - // Auto-activate the bootstrapped cluster. - if let Err(err) = navigator_bootstrap::save_active_cluster(DEFAULT_CLUSTER_NAME) { - tracing::debug!("failed to set active cluster after bootstrap: {err}"); + // Auto-activate the bootstrapped gateway. + if let Err(err) = navigator_bootstrap::save_active_gateway(DEFAULT_GATEWAY_NAME) { + tracing::debug!("failed to set active gateway after bootstrap: {err}"); } // Build fresh TLS options that resolve the newly-written mTLS certs from - // the default XDG path for this cluster, using the cluster name directly. + // the default XDG path for this gateway, using the gateway name directly. let tls = TlsOptions::default() - .with_cluster_name(DEFAULT_CLUSTER_NAME) + .with_gateway_name(DEFAULT_GATEWAY_NAME) .with_default_paths(&server); Ok((tls, server)) diff --git a/crates/navigator-cli/src/completers.rs b/crates/navigator-cli/src/completers.rs index fbdd8ad2b..6fa4e1d20 100644 --- a/crates/navigator-cli/src/completers.rs +++ b/crates/navigator-cli/src/completers.rs @@ -6,29 +6,29 @@ use std::future::Future; use std::time::Duration; use clap_complete::engine::CompletionCandidate; -use navigator_bootstrap::{list_clusters, load_active_cluster, load_cluster_metadata}; +use navigator_bootstrap::{list_gateways, load_active_gateway, load_gateway_metadata}; use navigator_core::proto::navigator_client::NavigatorClient; use navigator_core::proto::{ListProvidersRequest, ListSandboxesRequest}; use tonic::transport::{Channel, Endpoint}; use crate::tls::{TlsOptions, build_tonic_tls_config, require_tls_materials}; -/// Complete cluster names from local metadata files (no network call). -pub fn complete_cluster_names(_prefix: &OsStr) -> Vec { - let Ok(clusters) = list_clusters() else { +/// Complete gateway names from local metadata files (no network call). +pub fn complete_gateway_names(_prefix: &OsStr) -> Vec { + let Ok(gateways) = list_gateways() else { return Vec::new(); }; - clusters + gateways .into_iter() - .map(|c| CompletionCandidate::new(c.name)) + .map(|g| CompletionCandidate::new(g.name)) .collect() } -/// Complete sandbox names by querying the active cluster's gateway. +/// Complete sandbox names by querying the active gateway. pub fn complete_sandbox_names(_prefix: &OsStr) -> Vec { blocking_complete(async { - let (endpoint, cluster_name) = resolve_active_cluster()?; - let mut client = completion_grpc_client(&endpoint, &cluster_name).await?; + let (endpoint, gateway_name) = resolve_active_gateway()?; + let mut client = completion_grpc_client(&endpoint, &gateway_name).await?; let response = client .list_sandboxes(ListSandboxesRequest { limit: 200, @@ -47,11 +47,11 @@ pub fn complete_sandbox_names(_prefix: &OsStr) -> Vec { }) } -/// Complete provider names by querying the active cluster's gateway. +/// Complete provider names by querying the active gateway. pub fn complete_provider_names(_prefix: &OsStr) -> Vec { blocking_complete(async { - let (endpoint, cluster_name) = resolve_active_cluster()?; - let mut client = completion_grpc_client(&endpoint, &cluster_name).await?; + let (endpoint, gateway_name) = resolve_active_gateway()?; + let mut client = completion_grpc_client(&endpoint, &gateway_name).await?; let response = client .list_providers(ListProvidersRequest { limit: 200, @@ -70,20 +70,20 @@ pub fn complete_provider_names(_prefix: &OsStr) -> Vec { }) } -fn resolve_active_cluster() -> Option<(String, String)> { - let name = std::env::var("OPENSHELL_CLUSTER") +fn resolve_active_gateway() -> Option<(String, String)> { + let name = std::env::var("OPENSHELL_GATEWAY") .ok() .filter(|v| !v.trim().is_empty()) - .or_else(load_active_cluster)?; - let metadata = load_cluster_metadata(&name).ok()?; + .or_else(load_active_gateway)?; + let metadata = load_gateway_metadata(&name).ok()?; Some((metadata.gateway_endpoint, name)) } async fn completion_grpc_client( server: &str, - cluster_name: &str, + gateway_name: &str, ) -> Option> { - let tls_opts = TlsOptions::default().with_cluster_name(cluster_name); + let tls_opts = TlsOptions::default().with_gateway_name(gateway_name); let materials = require_tls_materials(server, &tls_opts).ok()?; let tls_config = build_tonic_tls_config(&materials); let endpoint = Endpoint::from_shared(server.to_string()) @@ -131,23 +131,23 @@ mod tests { with_vars( [ ("XDG_CONFIG_HOME", Some(tmp.as_str())), - ("OPENSHELL_CLUSTER", None::<&str>), + ("OPENSHELL_GATEWAY", None::<&str>), ], f, ); } #[test] - fn cluster_completer_returns_empty_when_no_config() { + fn gateway_completer_returns_empty_when_no_config() { let temp = tempfile::tempdir().unwrap(); with_isolated_cli_env(temp.path(), || { - let result = complete_cluster_names(OsStr::new("")); + let result = complete_gateway_names(OsStr::new("")); assert!(result.is_empty()); }); } #[test] - fn sandbox_completer_returns_empty_when_no_active_cluster() { + fn sandbox_completer_returns_empty_when_no_active_gateway() { let temp = tempfile::tempdir().unwrap(); with_isolated_cli_env(temp.path(), || { let result = complete_sandbox_names(OsStr::new("")); @@ -156,7 +156,7 @@ mod tests { } #[test] - fn provider_completer_returns_empty_when_no_active_cluster() { + fn provider_completer_returns_empty_when_no_active_gateway() { let temp = tempfile::tempdir().unwrap(); with_isolated_cli_env(temp.path(), || { let result = complete_provider_names(OsStr::new("")); diff --git a/crates/navigator-cli/src/main.rs b/crates/navigator-cli/src/main.rs index 823b4c57c..b1f1ae88c 100644 --- a/crates/navigator-cli/src/main.rs +++ b/crates/navigator-cli/src/main.rs @@ -11,31 +11,31 @@ use owo_colors::OwoColorize; use std::io::Write; use navigator_bootstrap::{ - edge_token::load_edge_token, get_cluster_metadata, list_clusters, load_active_cluster, - load_cluster_metadata, load_last_sandbox, save_last_sandbox, + edge_token::load_edge_token, get_gateway_metadata, list_gateways, load_active_gateway, + load_gateway_metadata, load_last_sandbox, save_last_sandbox, }; use navigator_cli::completers; use navigator_cli::run; use navigator_cli::tls::TlsOptions; -/// Resolved cluster context: name + gateway endpoint. +/// Resolved gateway context: name + gateway endpoint. struct GatewayContext { - /// The cluster name (used for TLS cert directory, metadata lookup, etc.). + /// The gateway name (used for TLS cert directory, metadata lookup, etc.). name: String, /// The gateway endpoint URL (e.g., `https://127.0.0.1` or `https://10.0.0.5`). endpoint: String, } -/// Resolve the cluster name to a [`GatewayContext`] with the gateway endpoint. +/// Resolve the gateway name to a [`GatewayContext`] with the gateway endpoint. /// /// Resolution priority: /// 1. `--gateway-endpoint` flag (direct URL, preserving metadata when available) -/// 2. `--cluster` flag (explicit name) -/// 3. `OPENSHELL_CLUSTER` environment variable -/// 4. Active cluster from `~/.config/openshell/active_cluster` +/// 2. `--gateway` flag (explicit name) +/// 3. `OPENSHELL_GATEWAY` environment variable +/// 4. Active gateway from `~/.config/openshell/active_gateway` /// /// When `--gateway-endpoint` is provided, it is used directly as the endpoint. -/// If stored metadata can still identify the gateway, the stored cluster name +/// If stored metadata can still identify the gateway, the stored gateway name /// is preserved so auth and TLS materials continue to resolve correctly. fn normalize_gateway_endpoint(endpoint: &str) -> &str { endpoint.trim_end_matches('/') @@ -44,27 +44,27 @@ fn normalize_gateway_endpoint(endpoint: &str) -> &str { fn find_gateway_by_endpoint(endpoint: &str) -> Option { let endpoint = normalize_gateway_endpoint(endpoint); - if let Some(active_name) = load_active_cluster() - && let Ok(metadata) = load_cluster_metadata(&active_name) + if let Some(active_name) = load_active_gateway() + && let Ok(metadata) = load_gateway_metadata(&active_name) && normalize_gateway_endpoint(&metadata.gateway_endpoint) == endpoint { return Some(metadata.name); } - list_clusters().ok()?.into_iter().find_map(|metadata| { + list_gateways().ok()?.into_iter().find_map(|metadata| { (normalize_gateway_endpoint(&metadata.gateway_endpoint) == endpoint) .then_some(metadata.name) }) } fn resolve_gateway( - cluster_flag: &Option, + gateway_flag: &Option, gateway_endpoint: &Option, ) -> Result { if let Some(endpoint) = gateway_endpoint { - let name = cluster_flag + let name = gateway_flag .clone() - .filter(|name| get_cluster_metadata(name).is_some()) + .filter(|name| get_gateway_metadata(name).is_some()) .or_else(|| find_gateway_by_endpoint(endpoint)) .unwrap_or_else(|| endpoint.clone()); return Ok(GatewayContext { @@ -73,14 +73,14 @@ fn resolve_gateway( }); } - let name = cluster_flag + let name = gateway_flag .clone() .or_else(|| { - std::env::var("OPENSHELL_CLUSTER") + std::env::var("OPENSHELL_GATEWAY") .ok() .filter(|v| !v.trim().is_empty()) }) - .or_else(load_active_cluster) + .or_else(load_active_gateway) .ok_or_else(|| { miette::miette!( "No active gateway.\n\ @@ -89,7 +89,7 @@ fn resolve_gateway( ) })?; - let metadata = load_cluster_metadata(&name).map_err(|_| { + let metadata = load_gateway_metadata(&name).map_err(|_| { miette::miette!( "Unknown gateway '{name}'.\n\ Deploy it first: openshell gateway start --name {name}\n\ @@ -103,46 +103,46 @@ fn resolve_gateway( }) } -/// Resolve only the cluster name (without requiring metadata to exist). +/// Resolve only the gateway name (without requiring metadata to exist). /// -/// Used by gateway commands that operate on a cluster by name but may not need -/// the gateway endpoint (e.g., `gateway start` creates the cluster). -fn resolve_gateway_name(cluster_flag: &Option) -> Option { - cluster_flag +/// Used by gateway commands that operate on a gateway by name but may not need +/// the gateway endpoint (e.g., `gateway start` creates the gateway). +fn resolve_gateway_name(gateway_flag: &Option) -> Option { + gateway_flag .clone() .or_else(|| { - std::env::var("OPENSHELL_CLUSTER") + std::env::var("OPENSHELL_GATEWAY") .ok() .filter(|v| !v.trim().is_empty()) }) - .or_else(load_active_cluster) + .or_else(load_active_gateway) } -/// Apply edge authentication token from local storage when the cluster uses edge auth. +/// Apply edge authentication token from local storage when the gateway uses edge auth. /// -/// When the resolved cluster has `auth_mode == "cloudflare_jwt"`, loads the +/// When the resolved gateway has `auth_mode == "cloudflare_jwt"`, loads the /// stored edge token from disk and sets it on the `TlsOptions`. The token is -/// always read from cluster metadata rather than supplied via a CLI flag. -fn apply_edge_auth(tls: &mut TlsOptions, cluster_name: &str) { - if let Some(meta) = get_cluster_metadata(cluster_name) { +/// always read from gateway metadata rather than supplied via a CLI flag. +fn apply_edge_auth(tls: &mut TlsOptions, gateway_name: &str) { + if let Some(meta) = get_gateway_metadata(gateway_name) { if meta.auth_mode.as_deref() == Some("cloudflare_jwt") { - if let Some(token) = load_edge_token(cluster_name) { + if let Some(token) = load_edge_token(gateway_name) { tls.edge_token = Some(token); } } } } -/// Resolve a sandbox name, falling back to the last-used sandbox for the cluster. +/// Resolve a sandbox name, falling back to the last-used sandbox for the gateway. /// /// When `name` is `None`, looks up the last sandbox recorded for the active -/// cluster. Prints a hint when falling back so the user knows which sandbox +/// gateway. Prints a hint when falling back so the user knows which sandbox /// was chosen. -fn resolve_sandbox_name(name: Option, cluster: &str) -> Result { +fn resolve_sandbox_name(name: Option, gateway: &str) -> Result { if let Some(n) = name { return Ok(n); } - let last = load_last_sandbox(cluster).ok_or_else(|| { + let last = load_last_sandbox(gateway).ok_or_else(|| { miette::miette!( "No sandbox name provided and no last-used sandbox.\n\ Specify a sandbox name or connect to one first: nav sandbox connect " @@ -278,12 +278,12 @@ struct Cli { #[arg(short, long, action = clap::ArgAction::Count, global = true)] verbose: u8, - /// Cluster name to operate on (resolved from stored metadata). - #[arg(long, short, global = true, env = "OPENSHELL_CLUSTER")] - cluster: Option, + /// Gateway name to operate on (resolved from stored metadata). + #[arg(long, short = 'g', global = true, env = "OPENSHELL_GATEWAY")] + gateway: Option, /// Gateway endpoint URL (e.g. https://gateway.example.com). - /// Connects directly without looking up cluster metadata. + /// Connects directly without looking up gateway metadata. #[arg(long, global = true, env = "OPENSHELL_GATEWAY_ENDPOINT")] gateway_endpoint: Option, @@ -370,7 +370,7 @@ enum Commands { #[command(hide = true, after_help = INFERENCE_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)] Inference { #[command(subcommand)] - command: Option, + command: Option, }, // =================================================================== @@ -395,12 +395,12 @@ enum Commands { /// `openshell ssh-proxy --gateway --sandbox-id --token ` /// /// **Name mode** (for use in `~/.ssh/config`): - /// `openshell ssh-proxy --cluster --name ` + /// `openshell ssh-proxy --gateway --name ` #[command(hide = true)] SshProxy { /// Gateway URL (e.g., ). - /// Required in token mode. - #[arg(long)] + /// Required in token mode. In name mode, can be a gateway name. + #[arg(long, short = 'g')] gateway: Option, /// Sandbox id. Required in token mode. @@ -411,25 +411,18 @@ enum Commands { #[arg(long)] token: Option, - /// Cluster endpoint URL. Used in name mode. Deprecated: prefer --cluster. + /// Gateway endpoint URL. Used in name mode. Deprecated: prefer --gateway. #[arg(long)] server: Option, - /// Cluster name (resolves endpoint from stored metadata). Used in name mode. - #[arg(long, short)] - cluster: Option, + /// Gateway name. Used with --name to resolve gateway from metadata. + #[arg(long)] + gateway_name: Option, /// Sandbox name. Used in name mode. #[arg(long)] name: Option, }, - - /// Manage cluster (deprecated: use `gateway`). - #[command(hide = true)] - Cluster { - #[command(subcommand)] - command: ClusterCommands, - }, } #[derive(Clone, Debug, ValueEnum)] @@ -619,7 +612,7 @@ enum GatewayCommands { /// Deploy/start the gateway. Start { /// Gateway name. - #[arg(long, default_value = "openshell", env = "OPENSHELL_CLUSTER")] + #[arg(long, default_value = "openshell", env = "OPENSHELL_GATEWAY")] name: String, /// Write stored kubeconfig into local kubeconfig. @@ -694,14 +687,14 @@ enum GatewayCommands { /// Stop the gateway (preserves state). Stop { /// Gateway name (defaults to active gateway). - #[arg(long, env = "OPENSHELL_CLUSTER")] + #[arg(long, env = "OPENSHELL_GATEWAY")] name: Option, - /// Override SSH destination (auto-resolved from cluster metadata). + /// Override SSH destination (auto-resolved from gateway metadata). #[arg(long)] remote: Option, - /// Path to SSH private key for remote cluster. + /// Path to SSH private key for remote gateway. #[arg(long, value_hint = ValueHint::FilePath)] ssh_key: Option, }, @@ -709,14 +702,14 @@ enum GatewayCommands { /// Destroy the gateway and its state. Destroy { /// Gateway name (defaults to active gateway). - #[arg(long, env = "OPENSHELL_CLUSTER")] + #[arg(long, env = "OPENSHELL_GATEWAY")] name: Option, - /// Override SSH destination (auto-resolved from cluster metadata). + /// Override SSH destination (auto-resolved from gateway metadata). #[arg(long)] remote: Option, - /// Path to SSH private key for remote cluster. + /// Path to SSH private key for remote gateway. #[arg(long, value_hint = ValueHint::FilePath)] ssh_key: Option, }, @@ -747,7 +740,7 @@ enum GatewayCommands { /// or to authenticate a gateway added with `--no-auth`. Login { /// Gateway name (defaults to the active gateway). - #[arg(add = ArgValueCompleter::new(completers::complete_cluster_names))] + #[arg(add = ArgValueCompleter::new(completers::complete_gateway_names))] name: Option, }, @@ -756,24 +749,24 @@ enum GatewayCommands { /// When called without a name, lists available gateways to choose from. Select { /// Gateway name (omit to list available gateways). - #[arg(add = ArgValueCompleter::new(completers::complete_cluster_names))] + #[arg(add = ArgValueCompleter::new(completers::complete_gateway_names))] name: Option, }, /// Show gateway deployment details. Info { /// Gateway name (defaults to active gateway). - #[arg(long, env = "OPENSHELL_CLUSTER")] + #[arg(long, env = "OPENSHELL_GATEWAY")] name: Option, }, /// Print or start an SSH tunnel for kubectl access to a remote gateway. Tunnel { /// Gateway name (defaults to active gateway). - #[arg(long, env = "OPENSHELL_CLUSTER")] + #[arg(long, env = "OPENSHELL_GATEWAY")] name: Option, - /// Override SSH destination (auto-resolved from cluster metadata). + /// Override SSH destination (auto-resolved from gateway metadata). #[arg(long)] remote: Option, @@ -788,29 +781,12 @@ enum GatewayCommands { } // ----------------------------------------------------------------------- -// Hidden backwards-compat: `cluster admin deploy` → `gateway start` +// Inference commands // ----------------------------------------------------------------------- #[derive(Subcommand, Debug)] -enum ClusterCommands { - /// Deprecated: use `gateway start`. - #[command(hide = true)] - Admin { - #[command(subcommand)] - command: ClusterAdminCommands, - }, - - /// Manage cluster-level inference configuration. - #[command(hide = true)] - Inference { - #[command(subcommand)] - command: ClusterInferenceCommands, - }, -} - -#[derive(Subcommand, Debug)] -enum ClusterInferenceCommands { - /// Set cluster-level inference provider and model. +enum InferenceCommands { + /// Set gateway-level inference provider and model. Set { /// Provider name. #[arg(long, add = ArgValueCompleter::new(completers::complete_provider_names))] @@ -821,7 +797,7 @@ enum ClusterInferenceCommands { model: String, }, - /// Update cluster-level inference configuration (partial update). + /// Update gateway-level inference configuration (partial update). Update { /// Provider name (unchanged if omitted). #[arg(long, add = ArgValueCompleter::new(completers::complete_provider_names))] @@ -832,56 +808,10 @@ enum ClusterInferenceCommands { model: Option, }, - /// Get cluster-level inference provider and model. + /// Get gateway-level inference provider and model. Get, } -#[derive(Subcommand, Debug)] -enum ClusterAdminCommands { - /// Deprecated: use `gateway start`. - Deploy { - /// Cluster name. - #[arg(long, default_value = "openshell", env = "OPENSHELL_CLUSTER")] - name: String, - - /// Write stored kubeconfig into local kubeconfig. - #[arg(long)] - update_kube_config: bool, - - /// Print stored kubeconfig to stdout. - #[arg(long)] - get_kubeconfig: bool, - - /// SSH destination for remote deployment (e.g., user@hostname). - #[arg(long)] - remote: Option, - - /// Path to SSH private key for remote deployment. - #[arg(long, value_hint = ValueHint::FilePath)] - ssh_key: Option, - - /// Host port to map to the gateway (default: 8080). - #[arg(long, default_value_t = navigator_bootstrap::DEFAULT_GATEWAY_PORT)] - port: u16, - - /// Override the gateway host written into cluster metadata. - #[arg(long)] - gateway_host: Option, - - /// Expose the Kubernetes control plane on a host port for kubectl access. - #[arg(long, num_args = 0..=1, default_missing_value = "0")] - kube_port: Option, - - /// Destroy and recreate from scratch if a cluster already exists. - #[arg(long)] - recreate: bool, - - /// Authentication token for pulling container images from ghcr.io. - #[arg(long, env = "OPENSHELL_REGISTRY_TOKEN")] - registry_token: Option, - }, -} - #[derive(Subcommand, Debug)] enum SandboxCommands { /// Create a sandbox. @@ -1190,7 +1120,7 @@ async fn main() -> Result<()> { disable_gateway_auth, registry_token, } => { - run::cluster_admin_deploy( + run::gateway_admin_deploy( &name, update_kube_config, get_kubeconfig, @@ -1212,9 +1142,9 @@ async fn main() -> Result<()> { ssh_key, } => { let name = name - .or_else(|| resolve_gateway_name(&cli.cluster)) + .or_else(|| resolve_gateway_name(&cli.gateway)) .unwrap_or_else(|| "openshell".to_string()); - run::cluster_admin_stop(&name, remote.as_deref(), ssh_key.as_deref()).await?; + run::gateway_admin_stop(&name, remote.as_deref(), ssh_key.as_deref()).await?; } GatewayCommands::Destroy { name, @@ -1222,9 +1152,9 @@ async fn main() -> Result<()> { ssh_key, } => { let name = name - .or_else(|| resolve_gateway_name(&cli.cluster)) + .or_else(|| resolve_gateway_name(&cli.gateway)) .unwrap_or_else(|| "openshell".to_string()); - run::cluster_admin_destroy(&name, remote.as_deref(), ssh_key.as_deref()).await?; + run::gateway_admin_destroy(&name, remote.as_deref(), ssh_key.as_deref()).await?; } GatewayCommands::Add { endpoint, @@ -1235,7 +1165,7 @@ async fn main() -> Result<()> { } GatewayCommands::Login { name } => { let name = name - .or_else(|| resolve_gateway_name(&cli.cluster)) + .or_else(|| resolve_gateway_name(&cli.gateway)) .ok_or_else(|| { miette::miette!( "No active gateway.\n\ @@ -1247,10 +1177,10 @@ async fn main() -> Result<()> { } GatewayCommands::Select { name } => { if let Some(name) = name { - run::cluster_use(&name)?; + run::gateway_use(&name)?; } else { // No name provided — show available gateways. - run::cluster_list(&cli.cluster)?; + run::gateway_list(&cli.gateway)?; eprintln!(); eprintln!( "Select a gateway with: {}", @@ -1260,9 +1190,9 @@ async fn main() -> Result<()> { } GatewayCommands::Info { name } => { let name = name - .or_else(|| resolve_gateway_name(&cli.cluster)) + .or_else(|| resolve_gateway_name(&cli.gateway)) .unwrap_or_else(|| "openshell".to_string()); - run::cluster_admin_info(&name)?; + run::gateway_admin_info(&name)?; } GatewayCommands::Tunnel { name, @@ -1271,9 +1201,9 @@ async fn main() -> Result<()> { print_command, } => { let name = name - .or_else(|| resolve_gateway_name(&cli.cluster)) + .or_else(|| resolve_gateway_name(&cli.gateway)) .unwrap_or_else(|| "openshell".to_string()); - run::cluster_admin_tunnel( + run::gateway_admin_tunnel( &name, remote.as_deref(), ssh_key.as_deref(), @@ -1283,13 +1213,13 @@ async fn main() -> Result<()> { }, // ----------------------------------------------------------- - // Top-level status (was `cluster status`) + // Top-level status // ----------------------------------------------------------- Some(Commands::Status) => { - if let Ok(ctx) = resolve_gateway(&cli.cluster, &cli.gateway_endpoint) { - let mut tls = tls.with_cluster_name(&ctx.name); + if let Ok(ctx) = resolve_gateway(&cli.gateway, &cli.gateway_endpoint) { + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); - run::cluster_status(&ctx.name, &ctx.endpoint, &tls).await?; + run::gateway_status(&ctx.name, &ctx.endpoint, &tls).await?; } else { println!("{}", "Gateway Status".cyan().bold()); println!(); @@ -1309,8 +1239,8 @@ async fn main() -> Result<()> { command: Some(fwd_cmd), }) => match fwd_cmd { ForwardCommands::Stop { port, name } => { - let cluster_name = resolve_gateway_name(&cli.cluster).unwrap_or_default(); - let name = resolve_sandbox_name(name, &cluster_name)?; + let gateway_name = resolve_gateway_name(&cli.gateway).unwrap_or_default(); + let name = resolve_sandbox_name(name, &gateway_name)?; if run::stop_forward(&name, port)? { eprintln!( "{} Stopped forward of port {port} for sandbox {name}", @@ -1363,8 +1293,8 @@ async fn main() -> Result<()> { name, background, } => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; - let mut tls = tls.with_cluster_name(&ctx.name); + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); let name = resolve_sandbox_name(name, &ctx.name)?; run::sandbox_forward(&ctx.endpoint, &name, port, background, &tls).await?; @@ -1390,8 +1320,8 @@ async fn main() -> Result<()> { source, level, }) => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; - let mut tls = tls.with_cluster_name(&ctx.name); + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); let name = resolve_sandbox_name(name, &ctx.name)?; run::sandbox_logs( @@ -1413,8 +1343,8 @@ async fn main() -> Result<()> { Some(Commands::Policy { command: Some(policy_cmd), }) => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; - let mut tls = tls.with_cluster_name(&ctx.name); + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); match policy_cmd { PolicyCommands::Set { @@ -1444,16 +1374,16 @@ async fn main() -> Result<()> { Some(Commands::Inference { command: Some(command), }) => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; let endpoint = &ctx.endpoint; - let mut tls = tls.with_cluster_name(&ctx.name); + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); match command { - ClusterInferenceCommands::Set { provider, model } => { - run::cluster_inference_set(endpoint, &provider, &model, &tls).await?; + InferenceCommands::Set { provider, model } => { + run::gateway_inference_set(endpoint, &provider, &model, &tls).await?; } - ClusterInferenceCommands::Update { provider, model } => { - run::cluster_inference_update( + InferenceCommands::Update { provider, model } => { + run::gateway_inference_update( endpoint, provider.as_deref(), model.as_deref(), @@ -1461,8 +1391,8 @@ async fn main() -> Result<()> { ) .await?; } - ClusterInferenceCommands::Get => { - run::cluster_inference_get(endpoint, &tls).await?; + InferenceCommands::Get => { + run::gateway_inference_get(endpoint, &tls).await?; } } } @@ -1528,7 +1458,7 @@ async fn main() -> Result<()> { // For `sandbox create`, a missing cluster is not fatal — the // bootstrap flow inside `sandbox_create` can deploy one. - match resolve_gateway(&cli.cluster, &cli.gateway_endpoint) { + match resolve_gateway(&cli.gateway, &cli.gateway_endpoint) { Ok(ctx) => { if remote.is_some() { eprintln!( @@ -1540,7 +1470,7 @@ async fn main() -> Result<()> { return Ok(()); } let endpoint = &ctx.endpoint; - let mut tls = tls.with_cluster_name(&ctx.name); + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); Box::pin(run::sandbox_create( endpoint, @@ -1563,7 +1493,7 @@ async fn main() -> Result<()> { .await?; } Err(_) => { - // No cluster configured — go straight to bootstrap. + // No gateway configured — go straight to bootstrap. Box::pin(run::sandbox_create_with_bootstrap( name.as_deref(), from.as_deref(), @@ -1589,8 +1519,8 @@ async fn main() -> Result<()> { dest, no_git_ignore, } => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; - let mut tls = tls.with_cluster_name(&ctx.name); + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); let sandbox_dest = dest.as_deref().unwrap_or("/sandbox"); let local = std::path::Path::new(&local_path); @@ -1623,8 +1553,8 @@ async fn main() -> Result<()> { sandbox_path, dest, } => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; - let mut tls = tls.with_cluster_name(&ctx.name); + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); let local_dest = std::path::Path::new(dest.as_deref().unwrap_or(".")); eprintln!( @@ -1637,9 +1567,9 @@ async fn main() -> Result<()> { eprintln!("{} Download complete", "✓".green().bold()); } other => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; let endpoint = &ctx.endpoint; - let mut tls = tls.with_cluster_name(&ctx.name); + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); match other { SandboxCommands::Create { .. } @@ -1678,9 +1608,9 @@ async fn main() -> Result<()> { Some(Commands::Provider { command: Some(command), }) => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; let endpoint = &ctx.endpoint; - let mut tls = tls.with_cluster_name(&ctx.name); + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); match command { @@ -1734,8 +1664,8 @@ async fn main() -> Result<()> { } } Some(Commands::Term) => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; - let mut tls = tls.with_cluster_name(&ctx.name); + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; + let mut tls = tls.with_gateway_name(&ctx.name); apply_edge_auth(&mut tls, &ctx.name); let channel = navigator_cli::tls::build_channel(&ctx.endpoint, &tls).await?; navigator_tui::run(channel, &ctx.name, &ctx.endpoint).await?; @@ -1756,115 +1686,51 @@ async fn main() -> Result<()> { sandbox_id, token, server, - cluster, + gateway_name, name, }) => { - match (gateway, sandbox_id, token, server, cluster, name) { + match (gateway, sandbox_id, token, server, gateway_name, name) { // Token mode (existing behavior): pre-created session credentials. - (Some(gw), Some(sid), Some(tok), _, cluster_opt, _) => { - let mut effective_tls = match cluster_opt { - Some(ref c) => tls.with_cluster_name(c), + (Some(gw), Some(sid), Some(tok), _, gateway_name_opt, _) => { + let mut effective_tls = match gateway_name_opt { + Some(ref g) => tls.with_gateway_name(g), None => tls, }; - if let Some(ref c) = cluster_opt { - apply_edge_auth(&mut effective_tls, c); + if let Some(ref g) = gateway_name_opt { + apply_edge_auth(&mut effective_tls, g); } run::sandbox_ssh_proxy(&gw, &sid, &tok, &effective_tls).await?; } - // Name mode with --cluster: resolve endpoint from metadata. - (_, _, _, server_override, Some(c), Some(n)) => { + // Name mode with --gateway-name: resolve endpoint from metadata. + (_, _, _, server_override, Some(g), Some(n)) => { let endpoint = if let Some(srv) = server_override { srv } else { - let meta = load_cluster_metadata(&c).map_err(|_| { + let meta = load_gateway_metadata(&g).map_err(|_| { miette::miette!( - "Unknown gateway '{c}'.\n\ - Deploy it first: openshell gateway start --name {c}\n\ + "Unknown gateway '{g}'.\n\ + Deploy it first: openshell gateway start --name {g}\n\ Or list available gateways: openshell gateway select" ) })?; meta.gateway_endpoint }; - let mut tls = tls.with_cluster_name(&c); - apply_edge_auth(&mut tls, &c); + let mut tls = tls.with_gateway_name(&g); + apply_edge_auth(&mut tls, &g); run::sandbox_ssh_proxy_by_name(&endpoint, &n, &tls).await?; } - // Legacy name mode with --server only (no --cluster). + // Legacy name mode with --server only (no --gateway-name). (_, _, _, Some(srv), None, Some(n)) => { run::sandbox_ssh_proxy_by_name(&srv, &n, &tls).await?; } _ => { return Err(miette::miette!( - "provide either --gateway/--sandbox-id/--token or --cluster/--name (or --server/--name)" + "provide either --gateway/--sandbox-id/--token or --gateway-name/--name (or --server/--name)" )); } } } - // ----------------------------------------------------------- - // Hidden backwards-compat: `cluster admin deploy` - // ----------------------------------------------------------- - Some(Commands::Cluster { command }) => match command { - ClusterCommands::Admin { command } => match command { - ClusterAdminCommands::Deploy { - name, - update_kube_config, - get_kubeconfig, - remote, - ssh_key, - port, - gateway_host, - kube_port, - recreate, - registry_token, - } => { - eprintln!( - "{} `openshell cluster admin deploy` is deprecated. \ - Use `openshell gateway start` instead.", - "warning:".yellow().bold(), - ); - run::cluster_admin_deploy( - &name, - update_kube_config, - get_kubeconfig, - remote.as_deref(), - ssh_key.as_deref(), - port, - gateway_host.as_deref(), - kube_port, - recreate, - false, // disable_tls - false, // disable_gateway_auth - registry_token.as_deref(), - ) - .await?; - } - }, - ClusterCommands::Inference { command } => { - let ctx = resolve_gateway(&cli.cluster, &cli.gateway_endpoint)?; - let endpoint = &ctx.endpoint; - let mut tls = tls.with_cluster_name(&ctx.name); - apply_edge_auth(&mut tls, &ctx.name); - match command { - ClusterInferenceCommands::Set { provider, model } => { - run::cluster_inference_set(endpoint, &provider, &model, &tls).await?; - } - ClusterInferenceCommands::Update { provider, model } => { - run::cluster_inference_update( - endpoint, - provider.as_deref(), - model.as_deref(), - &tls, - ) - .await?; - } - ClusterInferenceCommands::Get => { - run::cluster_inference_get(endpoint, &tls).await?; - } - } - } - }, - // No subcommand provided - print help for the command Some(Commands::Sandbox { command: None }) => { Cli::command() @@ -1937,7 +1803,7 @@ fn parse_upload_spec(spec: &str) -> (String, Option) { mod tests { use super::*; use navigator_bootstrap::{ - ClusterMetadata, edge_token::store_edge_token, store_cluster_metadata, + GatewayMetadata, edge_token::store_edge_token, store_gateway_metadata, }; use std::ffi::OsString; use std::fs; @@ -1967,8 +1833,8 @@ mod tests { } } - fn edge_metadata(name: &str, endpoint: &str) -> ClusterMetadata { - ClusterMetadata { + fn edge_metadata(name: &str, endpoint: &str) -> GatewayMetadata { + GatewayMetadata { name: name.to_string(), gateway_endpoint: endpoint.to_string(), is_remote: true, @@ -2156,7 +2022,7 @@ mod tests { fn resolve_sandbox_name_returns_explicit_name() { // When a name is provided, it should be returned regardless of any // stored last-sandbox state. - let result = resolve_sandbox_name(Some("explicit".to_string()), "any-cluster"); + let result = resolve_sandbox_name(Some("explicit".to_string()), "any-gateway"); assert_eq!(result.unwrap(), "explicit"); } @@ -2164,8 +2030,8 @@ mod tests { fn resolve_sandbox_name_falls_back_to_last_used() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - save_last_sandbox("test-cluster", "remembered-sb").unwrap(); - let result = resolve_sandbox_name(None, "test-cluster"); + save_last_sandbox("test-gateway", "remembered-sb").unwrap(); + let result = resolve_sandbox_name(None, "test-gateway"); assert_eq!(result.unwrap(), "remembered-sb"); }); } @@ -2174,7 +2040,7 @@ mod tests { fn resolve_sandbox_name_errors_without_fallback() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - let err = resolve_sandbox_name(None, "unknown-cluster").unwrap_err(); + let err = resolve_sandbox_name(None, "unknown-gateway").unwrap_err(); let msg = err.to_string(); assert!( msg.contains("nav sandbox connect"), @@ -2187,7 +2053,7 @@ mod tests { fn resolve_gateway_uses_stored_name_for_matching_endpoint() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - store_cluster_metadata( + store_gateway_metadata( "edge-gateway", &edge_metadata("edge-gateway", "https://gw.example.com"), ) @@ -2200,10 +2066,10 @@ mod tests { } #[test] - fn resolve_gateway_prefers_explicit_cluster_for_direct_endpoint() { + fn resolve_gateway_prefers_explicit_gateway_for_direct_endpoint() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - store_cluster_metadata( + store_gateway_metadata( "named-gateway", &edge_metadata("named-gateway", "https://stored.example.com"), ) @@ -2224,7 +2090,7 @@ mod tests { fn apply_edge_auth_uses_stored_token() { let tmp = tempfile::tempdir().unwrap(); with_tmp_xdg(tmp.path(), || { - store_cluster_metadata( + store_gateway_metadata( "edge-gateway", &edge_metadata("edge-gateway", "https://gw.example.com"), ) diff --git a/crates/navigator-cli/src/run.rs b/crates/navigator-cli/src/run.rs index 969df8eab..ddce9560e 100644 --- a/crates/navigator-cli/src/run.rs +++ b/crates/navigator-cli/src/run.rs @@ -16,10 +16,10 @@ use hyper_util::{client::legacy::Client, rt::TokioExecutor}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{IntoDiagnostic, Result, WrapErr}; use navigator_bootstrap::{ - ClusterMetadata, DeployOptions, RemoteOptions, clear_active_cluster, - default_local_kubeconfig_path, get_cluster_metadata, list_clusters, load_active_cluster, - print_kubeconfig, remove_cluster_metadata, save_active_cluster, save_last_sandbox, - store_cluster_metadata, update_local_kubeconfig, + DeployOptions, GatewayMetadata, RemoteOptions, clear_active_gateway, + default_local_kubeconfig_path, get_gateway_metadata, list_gateways, load_active_gateway, + print_kubeconfig, remove_gateway_metadata, save_active_gateway, save_last_sandbox, + store_gateway_metadata, update_local_kubeconfig, }; use navigator_core::proto::{ CreateProviderRequest, CreateSandboxRequest, DeleteProviderRequest, DeleteSandboxRequest, @@ -378,7 +378,7 @@ fn truncate_to_width(s: &str, max_width: usize) -> String { format!("{}…", &s[..end]) } -struct ClusterDeployLogPanel { +struct GatewayDeployLogPanel { mp: MultiProgress, status: String, progress: Option, @@ -391,7 +391,7 @@ struct ClusterDeployLogPanel { buffer: VecDeque, } -impl ClusterDeployLogPanel { +impl GatewayDeployLogPanel { fn new(_name: &str, _location: &str) -> Self { let mp = MultiProgress::new(); @@ -584,12 +584,12 @@ fn is_progress_status(status: &str) -> bool { || status.starts_with("Extracting:") } -/// Show cluster status. +/// Show gateway status. #[allow(clippy::branches_sharing_code)] -pub async fn cluster_status(cluster_name: &str, server: &str, tls: &TlsOptions) -> Result<()> { +pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions) -> Result<()> { println!("{}", "Server Status".cyan().bold()); println!(); - println!(" {} {}", "Cluster:".dimmed(), cluster_name); + println!(" {} {}", "Gateway:".dimmed(), gateway_name); println!(" {} {}", "Server:".dimmed(), server); if tls.is_bearer_auth() { println!(" {} {}", "Auth:".dimmed(), "Edge (bearer token)"); @@ -641,10 +641,10 @@ pub async fn cluster_status(cluster_name: &str, server: &str, tls: &TlsOptions) Ok(()) } -/// Set the active cluster. -pub fn cluster_use(name: &str) -> Result<()> { - // Verify the cluster exists - get_cluster_metadata(name).ok_or_else(|| { +/// Set the active gateway. +pub fn gateway_use(name: &str) -> Result<()> { + // Verify the gateway exists + get_gateway_metadata(name).ok_or_else(|| { miette::miette!( "No gateway metadata found for '{name}'.\n\ Deploy a gateway first with: openshell gateway start --name {name}\n\ @@ -652,7 +652,7 @@ pub fn cluster_use(name: &str) -> Result<()> { ) })?; - save_active_cluster(name)?; + save_active_gateway(name)?; eprintln!("{} Active gateway set to '{name}'", "✓".green().bold()); Ok(()) } @@ -685,7 +685,7 @@ pub async fn gateway_add(endpoint: &str, name: Option<&str>, no_auth: bool) -> R }; // Build metadata for an edge-authenticated remote gateway. - let metadata = ClusterMetadata { + let metadata = GatewayMetadata { name: name.to_string(), gateway_endpoint: endpoint.clone(), is_remote: true, @@ -698,8 +698,8 @@ pub async fn gateway_add(endpoint: &str, name: Option<&str>, no_auth: bool) -> R edge_auth_url: None, }; - store_cluster_metadata(name, &metadata)?; - save_active_cluster(name)?; + store_gateway_metadata(name, &metadata)?; + save_active_gateway(name)?; if no_auth { eprintln!( @@ -743,7 +743,7 @@ pub async fn gateway_add(endpoint: &str, name: Option<&str>, no_auth: bool) -> R /// /// Opens a browser for edge proxy login and stores the updated token. pub async fn gateway_login(name: &str) -> Result<()> { - let metadata = navigator_bootstrap::load_cluster_metadata(name).map_err(|_| { + let metadata = navigator_bootstrap::load_gateway_metadata(name).map_err(|_| { miette::miette!( "Unknown gateway '{name}'.\n\ List available gateways: openshell gateway select" @@ -765,12 +765,12 @@ pub async fn gateway_login(name: &str) -> Result<()> { Ok(()) } -/// List all provisioned clusters. -pub fn cluster_list(gateway_flag: &Option) -> Result<()> { - let clusters = list_clusters()?; - let active = gateway_flag.clone().or_else(load_active_cluster); +/// List all provisioned gateways. +pub fn gateway_list(gateway_flag: &Option) -> Result<()> { + let gateways = list_gateways()?; + let active = gateway_flag.clone().or_else(load_active_gateway); - if clusters.is_empty() { + if gateways.is_empty() { println!("No gateways found."); println!(); println!( @@ -781,15 +781,15 @@ pub fn cluster_list(gateway_flag: &Option) -> Result<()> { } // Calculate column widths - let name_width = clusters + let name_width = gateways .iter() - .map(|c| c.name.len()) + .map(|g| g.name.len()) .max() .unwrap_or(4) .max(4); - let endpoint_width = clusters + let endpoint_width = gateways .iter() - .map(|c| c.gateway_endpoint.len()) + .map(|g| g.gateway_endpoint.len()) .max() .unwrap_or(8) .max(8); @@ -803,17 +803,17 @@ pub fn cluster_list(gateway_flag: &Option) -> Result<()> { ); // Print rows - for cluster in &clusters { - let is_active = active.as_deref() == Some(&cluster.name); + for gateway in &gateways { + let is_active = active.as_deref() == Some(&gateway.name); let marker = if is_active { "*" } else { " " }; - let cluster_type = match cluster.auth_mode.as_deref() { + let gateway_type = match gateway.auth_mode.as_deref() { Some("cloudflare_jwt") => "edge", - _ if cluster.is_remote => "remote", + _ if gateway.is_remote => "remote", _ => "local", }; let line = format!( - "{marker} {: Result Result { let status = if info.container_running { "running" @@ -896,24 +896,24 @@ fn prompt_existing_cluster( Ok(choice == "y" || choice == "yes") } -/// Deploy a cluster with the rich progress panel (interactive) or simple -/// logging (non-interactive). Returns the [`ClusterHandle`] on success. +/// Deploy a gateway with the rich progress panel (interactive) or simple +/// logging (non-interactive). Returns the [`GatewayHandle`] on success. /// /// This is the shared deploy UX used by both `gateway start` and /// the auto-bootstrap path in `sandbox create`. -pub(crate) async fn deploy_cluster_with_panel( +pub(crate) async fn deploy_gateway_with_panel( options: DeployOptions, name: &str, location: &str, -) -> Result { +) -> Result { let interactive = std::io::stderr().is_terminal(); if interactive { - let panel = std::sync::Arc::new(std::sync::Mutex::new(ClusterDeployLogPanel::new( + let panel = std::sync::Arc::new(std::sync::Mutex::new(GatewayDeployLogPanel::new( name, location, ))); let panel_clone = std::sync::Arc::clone(&panel); - let result = navigator_bootstrap::deploy_cluster_with_logs(options, move |line| { + let result = navigator_bootstrap::deploy_gateway_with_logs(options, move |line| { if let Ok(mut p) = panel_clone.lock() { p.push_log(line); } @@ -942,7 +942,7 @@ pub(crate) async fn deploy_cluster_with_panel( } } else { eprintln!("Deploying {location} gateway {name}..."); - let handle = navigator_bootstrap::deploy_cluster_with_logs(options, |line| { + let handle = navigator_bootstrap::deploy_gateway_with_logs(options, |line| { if let Some(status) = line.strip_prefix("[status] ") { eprintln!(" {status}"); } else if line.strip_prefix("[progress] ").is_some() { @@ -957,8 +957,8 @@ pub(crate) async fn deploy_cluster_with_panel( } } -/// Print post-deploy summary showing the cluster name and gateway endpoint. -pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::ClusterHandle) { +/// Print post-deploy summary showing the gateway name and endpoint. +pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::GatewayHandle) { eprintln!(); eprintln!("{} {} {name}", "✓".green().bold(), "Gateway ready:".green(),); eprintln!( @@ -969,8 +969,8 @@ pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::Clu eprintln!(); } -/// Provision or start a cluster (local or remote). -pub async fn cluster_admin_deploy( +/// Provision or start a gateway (local or remote). +pub async fn gateway_admin_deploy( name: &str, update_kube_config: bool, get_kubeconfig: bool, @@ -1014,7 +1014,7 @@ pub async fn cluster_admin_deploy( let interactive = std::io::stderr().is_terminal(); - // Check for existing cluster and prompt user if found. + // Check for existing gateway and prompt user if found. // --recreate skips the prompt and always destroys. { let remote_opts = remote.map(|dest| { @@ -1030,7 +1030,7 @@ pub async fn cluster_admin_deploy( let should_recreate = if recreate { true } else if interactive { - prompt_existing_cluster(name, &info)? + prompt_existing_gateway(name, &info)? } else { false // non-interactive without --recreate: silently reuse }; @@ -1038,7 +1038,7 @@ pub async fn cluster_admin_deploy( if should_recreate { eprintln!("• Destroying existing gateway..."); let handle = - navigator_bootstrap::cluster_handle(name, remote_opts.as_ref()).await?; + navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?; handle.destroy().await?; eprintln!("{} Gateway destroyed, starting fresh.", "✓".green().bold()); eprintln!(); @@ -1047,7 +1047,7 @@ pub async fn cluster_admin_deploy( } } - let handle = deploy_cluster_with_panel(options, name, location).await?; + let handle = deploy_gateway_with_panel(options, name, location).await?; if update_kube_config { let target_path = default_local_kubeconfig_path()?; @@ -1065,63 +1065,63 @@ pub async fn cluster_admin_deploy( print_deploy_summary(name, &handle); - // Auto-activate: set this cluster as the active cluster. - save_active_cluster(name)?; + // Auto-activate: set this gateway as the active gateway. + save_active_gateway(name)?; eprintln!("{} Active gateway set to '{name}'", "✓".green().bold()); Ok(()) } -/// Resolve the remote SSH destination for a cluster. +/// Resolve the remote SSH destination for a gateway. /// /// If `remote_override` is provided, use it. Otherwise, look up the remote -/// host from stored cluster metadata. -enum ClusterControlTarget { +/// host from stored gateway metadata. +enum GatewayControlTarget { Local, Remote(String), ExternalRegistration, } -fn resolve_cluster_control_target( +fn resolve_gateway_control_target( name: &str, remote_override: Option<&str>, -) -> ClusterControlTarget { - resolve_cluster_control_target_from(get_cluster_metadata(name), remote_override) +) -> GatewayControlTarget { + resolve_gateway_control_target_from(get_gateway_metadata(name), remote_override) } -fn resolve_cluster_control_target_from( - metadata: Option, +fn resolve_gateway_control_target_from( + metadata: Option, remote_override: Option<&str>, -) -> ClusterControlTarget { +) -> GatewayControlTarget { if let Some(r) = remote_override { - return ClusterControlTarget::Remote(r.to_string()); + return GatewayControlTarget::Remote(r.to_string()); } match metadata { Some(metadata) if metadata.is_remote => metadata .remote_host - .map(ClusterControlTarget::Remote) - .unwrap_or(ClusterControlTarget::ExternalRegistration), - _ => ClusterControlTarget::Local, + .map(GatewayControlTarget::Remote) + .unwrap_or(GatewayControlTarget::ExternalRegistration), + _ => GatewayControlTarget::Local, } } -fn cluster_control_target_options( +fn gateway_control_target_options( name: &str, remote_override: Option<&str>, ssh_key: Option<&str>, ) -> Result> { - match resolve_cluster_control_target(name, remote_override) { - ClusterControlTarget::Local => Ok(None), - ClusterControlTarget::Remote(dest) => { + match resolve_gateway_control_target(name, remote_override) { + GatewayControlTarget::Local => Ok(None), + GatewayControlTarget::Remote(dest) => { let mut opts = RemoteOptions::new(&dest); if let Some(key) = ssh_key { opts = opts.with_ssh_key(key); } Ok(Some(opts)) } - ClusterControlTarget::ExternalRegistration => Err(miette::miette!( - "Gateway '{name}' is an external registration, not a managed Docker cluster.\n\ + GatewayControlTarget::ExternalRegistration => Err(miette::miette!( + "Gateway '{name}' is an external registration, not a managed Docker gateway.\n\ `openshell gateway stop` is only supported for local or SSH-managed gateways." )), } @@ -1131,60 +1131,60 @@ fn remove_gateway_registration(name: &str) { if let Err(err) = navigator_bootstrap::edge_token::remove_edge_token(name) { tracing::debug!("failed to remove edge token: {err}"); } - if let Err(err) = remove_cluster_metadata(name) { - tracing::debug!("failed to remove cluster metadata: {err}"); + if let Err(err) = remove_gateway_metadata(name) { + tracing::debug!("failed to remove gateway metadata: {err}"); } - if load_active_cluster().as_deref() == Some(name) - && let Err(err) = clear_active_cluster() + if load_active_gateway().as_deref() == Some(name) + && let Err(err) = clear_active_gateway() { - tracing::debug!("failed to clear active cluster: {err}"); + tracing::debug!("failed to clear active gateway: {err}"); } } -fn cleanup_cluster_metadata(name: &str) { +fn cleanup_gateway_metadata(name: &str) { if let Err(err) = navigator_bootstrap::edge_token::remove_edge_token(name) { tracing::debug!("failed to remove edge token: {err}"); } - if let Err(err) = remove_cluster_metadata(name) { - tracing::debug!("failed to remove cluster metadata: {err}"); + if let Err(err) = remove_gateway_metadata(name) { + tracing::debug!("failed to remove gateway metadata: {err}"); } - if load_active_cluster().as_deref() == Some(name) - && let Err(err) = clear_active_cluster() + if load_active_gateway().as_deref() == Some(name) + && let Err(err) = clear_active_gateway() { - tracing::debug!("failed to clear active cluster: {err}"); + tracing::debug!("failed to clear active gateway: {err}"); } } fn resolve_remote(name: &str, remote_override: Option<&str>) -> Option { - match resolve_cluster_control_target(name, remote_override) { - ClusterControlTarget::Remote(dest) => Some(dest), - ClusterControlTarget::Local | ClusterControlTarget::ExternalRegistration => None, + match resolve_gateway_control_target(name, remote_override) { + GatewayControlTarget::Remote(dest) => Some(dest), + GatewayControlTarget::Local | GatewayControlTarget::ExternalRegistration => None, } } -/// Stop a cluster. -pub async fn cluster_admin_stop( +/// Stop a gateway. +pub async fn gateway_admin_stop( name: &str, remote: Option<&str>, ssh_key: Option<&str>, ) -> Result<()> { - let remote_opts = cluster_control_target_options(name, remote, ssh_key)?; + let remote_opts = gateway_control_target_options(name, remote, ssh_key)?; - eprintln!("• Stopping cluster {name}..."); - let handle = navigator_bootstrap::cluster_handle(name, remote_opts.as_ref()).await?; + eprintln!("• Stopping gateway {name}..."); + let handle = navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?; handle.stop().await?; - eprintln!("{} Cluster {name} stopped.", "✓".green().bold()); + eprintln!("{} Gateway {name} stopped.", "✓".green().bold()); Ok(()) } -/// Destroy a cluster and its state. -pub async fn cluster_admin_destroy( +/// Destroy a gateway and its state. +pub async fn gateway_admin_destroy( name: &str, remote: Option<&str>, ssh_key: Option<&str>, ) -> Result<()> { - match resolve_cluster_control_target(name, remote) { - ClusterControlTarget::ExternalRegistration => { + match resolve_gateway_control_target(name, remote) { + GatewayControlTarget::ExternalRegistration => { eprintln!("• Removing gateway registration {name}..."); remove_gateway_registration(name); eprintln!( @@ -1193,24 +1193,24 @@ pub async fn cluster_admin_destroy( ); Ok(()) } - ClusterControlTarget::Local | ClusterControlTarget::Remote(_) => { - let remote_opts = cluster_control_target_options(name, remote, ssh_key)?; + GatewayControlTarget::Local | GatewayControlTarget::Remote(_) => { + let remote_opts = gateway_control_target_options(name, remote, ssh_key)?; - eprintln!("• Destroying cluster {name}..."); - let handle = navigator_bootstrap::cluster_handle(name, remote_opts.as_ref()).await?; + eprintln!("• Destroying gateway {name}..."); + let handle = navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?; handle.destroy().await?; - cleanup_cluster_metadata(name); + cleanup_gateway_metadata(name); - eprintln!("{} Cluster {name} destroyed.", "✓".green().bold()); + eprintln!("{} Gateway {name} destroyed.", "✓".green().bold()); Ok(()) } } } -/// Show cluster deployment details. -pub fn cluster_admin_info(name: &str) -> Result<()> { - let metadata = get_cluster_metadata(name).ok_or_else(|| { +/// Show gateway deployment details. +pub fn gateway_admin_info(name: &str) -> Result<()> { + let metadata = get_gateway_metadata(name).ok_or_else(|| { miette::miette!( "No gateway metadata found for '{name}'.\n\ Deploy a gateway first with: openshell gateway start --name {name}" @@ -1219,9 +1219,9 @@ pub fn cluster_admin_info(name: &str) -> Result<()> { let kubeconfig_path = navigator_bootstrap::stored_kubeconfig_path(name)?; - println!("{}", "Cluster Info".cyan().bold()); + println!("{}", "Gateway Info".cyan().bold()); println!(); - println!(" {} {}", "Cluster:".dimmed(), metadata.name); + println!(" {} {}", "Gateway:".dimmed(), metadata.name); println!( " {} {}", "Gateway endpoint:".dimmed(), @@ -1259,8 +1259,8 @@ pub fn cluster_admin_info(name: &str) -> Result<()> { Ok(()) } -/// Print or start an SSH tunnel for kubectl access to a remote cluster. -pub fn cluster_admin_tunnel( +/// Print or start an SSH tunnel for kubectl access to a remote gateway. +pub fn gateway_admin_tunnel( name: &str, remote_override: Option<&str>, ssh_key: Option<&str>, @@ -1268,16 +1268,16 @@ pub fn cluster_admin_tunnel( ) -> Result<()> { let remote = resolve_remote(name, remote_override).ok_or_else(|| { miette::miette!( - "Cluster '{name}' is not a remote cluster (no SSH destination found).\n\ - SSH tunnels are only needed for remote clusters." + "Gateway '{name}' is not a remote gateway (no SSH destination found).\n\ + SSH tunnels are only needed for remote gateways." ) })?; - let kube_port = get_cluster_metadata(name) + let kube_port = get_gateway_metadata(name) .and_then(|m| m.kube_port) .ok_or_else(|| { miette::miette!( - "Cluster '{name}' was deployed without --kube-port.\n\ + "Gateway '{name}' was deployed without --kube-port.\n\ Redeploy with --kube-port to enable kubectl access via SSH tunnel." ) })?; @@ -1310,9 +1310,9 @@ pub fn cluster_admin_tunnel( Ok(()) } -/// Create a sandbox when no cluster is configured. +/// Create a sandbox when no gateway is configured. /// -/// Offers to bootstrap a new cluster first, then delegates to [`sandbox_create`]. +/// Offers to bootstrap a new gateway first, then delegates to [`sandbox_create`]. #[allow(clippy::too_many_arguments)] pub async fn sandbox_create_with_bootstrap( name: Option<&str>, @@ -1337,13 +1337,13 @@ pub async fn sandbox_create_with_bootstrap( )); } let (tls, server) = crate::bootstrap::run_bootstrap(remote, ssh_key).await?; - // The bootstrap flow always creates a cluster named "openshell". - let cluster_name = "openshell"; + // The bootstrap flow always creates a gateway named "openshell". + let gateway_name = "openshell"; sandbox_create( &server, name, from, - cluster_name, + gateway_name, upload, keep, remote, @@ -1366,7 +1366,7 @@ pub async fn sandbox_create( server: &str, name: Option<&str>, from: Option<&str>, - cluster_name: &str, + gateway_name: &str, upload: Option<&(String, Option, bool)>, keep: bool, remote: Option<&str>, @@ -1380,7 +1380,7 @@ pub async fn sandbox_create( auto_providers_override: Option, tls: &TlsOptions, ) -> Result<()> { - // Try connecting to the cluster. If it fails due to an unreachable cluster, + // Try connecting to the gateway. If it fails due to an unreachable gateway, // offer to bootstrap a local one and retry. let (mut client, effective_server, effective_tls) = match grpc_client(server, tls).await { Ok(c) => (c, server.to_string(), tls.clone()), @@ -1394,7 +1394,7 @@ pub async fn sandbox_create( let (new_tls, new_server) = crate::bootstrap::run_bootstrap(remote, ssh_key).await?; let c = grpc_client(&new_server, &new_tls) .await - .wrap_err("bootstrap succeeded but failed to connect to cluster")?; + .wrap_err("bootstrap succeeded but failed to connect to gateway")?; (c, new_server, new_tls) } }; @@ -1410,7 +1410,7 @@ pub async fn sandbox_create( dockerfile, context, } => { - let tag = build_from_dockerfile(&dockerfile, &context, cluster_name).await?; + let tag = build_from_dockerfile(&dockerfile, &context, gateway_name).await?; Some(tag) } } @@ -1464,9 +1464,9 @@ pub async fn sandbox_create( let interactive = std::io::stdout().is_terminal(); let sandbox_name = sandbox.name.clone(); - // Record this sandbox as the last-used for the active cluster. - if let Some(cluster) = effective_tls.cluster_name() { - let _ = save_last_sandbox(cluster, &sandbox_name); + // Record this sandbox as the last-used for the active gateway. + if let Some(gateway) = effective_tls.gateway_name() { + let _ = save_last_sandbox(gateway, &sandbox_name); } // Set up display — interactive terminals get a step-based checklist with @@ -1883,20 +1883,20 @@ fn resolve_from(value: &str) -> Result { Ok(ResolvedSource::Image(format!("{prefix}/{value}:latest"))) } -/// Build a Dockerfile and push the resulting image into the cluster. +/// Build a Dockerfile and push the resulting image into the gateway. /// /// Returns the image tag that was built so the caller can use it for sandbox /// creation. async fn build_from_dockerfile( dockerfile: &Path, context: &Path, - cluster_name: &str, + gateway_name: &str, ) -> Result { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); - let tag = format!("navigator/sandbox-from:{timestamp}"); + let tag = format!("openshell/sandbox-from:{timestamp}"); eprintln!( "Building image {} from {}", @@ -1904,7 +1904,7 @@ async fn build_from_dockerfile( dockerfile.display() ); eprintln!(" {} {}", "Context:".dimmed(), context.display()); - eprintln!(" {} {}", "Cluster:".dimmed(), cluster_name); + eprintln!(" {} {}", "Gateway:".dimmed(), gateway_name); eprintln!(); let mut on_log = |msg: String| { @@ -1915,7 +1915,7 @@ async fn build_from_dockerfile( dockerfile, &tag, context, - cluster_name, + gateway_name, &HashMap::new(), &mut on_log, ) @@ -1923,7 +1923,7 @@ async fn build_from_dockerfile( eprintln!(); eprintln!( - "{} Image {} is available in the cluster.", + "{} Image {} is available in the gateway.", "✓".green().bold(), tag.cyan(), ); @@ -2785,7 +2785,7 @@ pub async fn provider_delete(server: &str, names: &[String], tls: &TlsOptions) - Ok(()) } -pub async fn cluster_inference_set( +pub async fn gateway_inference_set( server: &str, provider_name: &str, model_id: &str, @@ -2801,7 +2801,7 @@ pub async fn cluster_inference_set( .into_diagnostic()?; let configured = response.into_inner(); - println!("{}", "Cluster inference configured:".cyan().bold()); + println!("{}", "Gateway inference configured:".cyan().bold()); println!(); println!(" {} {}", "Provider:".dimmed(), configured.provider_name); println!(" {} {}", "Model:".dimmed(), configured.model_id); @@ -2809,7 +2809,7 @@ pub async fn cluster_inference_set( Ok(()) } -pub async fn cluster_inference_update( +pub async fn gateway_inference_update( server: &str, provider_name: Option<&str>, model_id: Option<&str>, @@ -2842,7 +2842,7 @@ pub async fn cluster_inference_update( .into_diagnostic()?; let configured = response.into_inner(); - println!("{}", "Cluster inference updated:".cyan().bold()); + println!("{}", "Gateway inference updated:".cyan().bold()); println!(); println!(" {} {}", "Provider:".dimmed(), configured.provider_name); println!(" {} {}", "Model:".dimmed(), configured.model_id); @@ -2850,7 +2850,7 @@ pub async fn cluster_inference_update( Ok(()) } -pub async fn cluster_inference_get(server: &str, tls: &TlsOptions) -> Result<()> { +pub async fn gateway_inference_get(server: &str, tls: &TlsOptions) -> Result<()> { let mut client = grpc_inference_client(server, tls).await?; let response = client .get_cluster_inference(GetClusterInferenceRequest {}) @@ -2858,7 +2858,7 @@ pub async fn cluster_inference_get(server: &str, tls: &TlsOptions) -> Result<()> .into_diagnostic()?; let configured = response.into_inner(); - println!("{}", "Cluster inference:".cyan().bold()); + println!("{}", "Gateway inference:".cyan().bold()); println!(); println!(" {} {}", "Provider:".dimmed(), configured.provider_name); println!(" {} {}", "Model:".dimmed(), configured.model_id); @@ -3369,8 +3369,8 @@ fn print_log_line(log: &navigator_core::proto::SandboxLogLine) { #[cfg(test)] mod tests { use super::{ - ClusterControlTarget, TlsOptions, git_sync_files, http_health_check, - inferred_provider_type, parse_credential_pairs, resolve_cluster_control_target_from, + GatewayControlTarget, TlsOptions, git_sync_files, http_health_check, + inferred_provider_type, parse_credential_pairs, resolve_gateway_control_target_from, }; use hyper::StatusCode; use std::fs; @@ -3380,7 +3380,7 @@ mod tests { use std::process::Command; use std::thread; - use navigator_bootstrap::ClusterMetadata; + use navigator_bootstrap::GatewayMetadata; struct EnvVarGuard { key: &'static str, @@ -3421,8 +3421,8 @@ mod tests { } } - fn edge_registration(name: &str, endpoint: &str) -> ClusterMetadata { - ClusterMetadata { + fn edge_registration(name: &str, endpoint: &str) -> GatewayMetadata { + GatewayMetadata { name: name.to_string(), gateway_endpoint: endpoint.to_string(), is_remote: true, @@ -3562,18 +3562,18 @@ mod tests { } #[test] - fn resolve_cluster_control_target_marks_edge_registration_unmanaged() { + fn resolve_gateway_control_target_marks_edge_registration_unmanaged() { let metadata = edge_registration("edge-gateway", "https://gw.example.com"); - let target = resolve_cluster_control_target_from(Some(metadata), None); - assert!(matches!(target, ClusterControlTarget::ExternalRegistration)); + let target = resolve_gateway_control_target_from(Some(metadata), None); + assert!(matches!(target, GatewayControlTarget::ExternalRegistration)); } #[test] - fn resolve_cluster_control_target_prefers_explicit_remote_override() { - let target = resolve_cluster_control_target_from(None, Some("user@host")); + fn resolve_gateway_control_target_prefers_explicit_remote_override() { + let target = resolve_gateway_control_target_from(None, Some("user@host")); match target { - ClusterControlTarget::Remote(dest) => assert_eq!(dest, "user@host"), - ClusterControlTarget::Local | ClusterControlTarget::ExternalRegistration => { + GatewayControlTarget::Remote(dest) => assert_eq!(dest, "user@host"), + GatewayControlTarget::Local | GatewayControlTarget::ExternalRegistration => { panic!("expected remote target") } } diff --git a/crates/navigator-cli/src/ssh.rs b/crates/navigator-cli/src/ssh.rs index 06f4f5547..8ccf64adc 100644 --- a/crates/navigator-cli/src/ssh.rs +++ b/crates/navigator-cli/src/ssh.rs @@ -78,15 +78,15 @@ async fn ssh_session_config( session.gateway_scheme, gateway_host, gateway_port, session.connect_path ) }; - let cluster_name = tls - .cluster_name() - .ok_or_else(|| miette::miette!("cluster name is required to build SSH proxy command"))?; + let gateway_name = tls + .gateway_name() + .ok_or_else(|| miette::miette!("gateway name is required to build SSH proxy command"))?; let proxy_command = format!( - "{exe_command} ssh-proxy --gateway {} --sandbox-id {} --token {} --cluster {}", + "{exe_command} ssh-proxy --gateway {} --sandbox-id {} --token {} --gateway-name {}", gateway_url, session.sandbox_id, session.token, - shell_escape(cluster_name), + shell_escape(gateway_name), ); Ok(SshSessionConfig { @@ -580,14 +580,14 @@ pub async fn sandbox_ssh_proxy_by_name(server: &str, name: &str, tls: &TlsOption /// The output is suitable for appending to `~/.ssh/config` so that tools like /// `VSCode` Remote-SSH can connect to the sandbox by host alias. /// -/// The `ProxyCommand` uses `--cluster` so that `ssh-proxy` resolves the +/// The `ProxyCommand` uses `--gateway-name` so that `ssh-proxy` resolves the /// gateway endpoint and TLS certificates from the gateway metadata directory -/// (`~/.config/openshell/clusters//mtls/`). -pub fn print_ssh_config(cluster: &str, name: &str) { +/// (`~/.config/openshell/gateways//mtls/`). +pub fn print_ssh_config(gateway: &str, name: &str) { let exe = std::env::current_exe().expect("failed to resolve OpenShell executable"); let exe = shell_escape(&exe.to_string_lossy()); - let proxy_cmd = format!("{exe} ssh-proxy --cluster {cluster} --name {name}"); + let proxy_cmd = format!("{exe} ssh-proxy --gateway-name {gateway} --name {name}"); println!("Host openshell-{name}"); println!(" User sandbox"); diff --git a/crates/navigator-cli/src/tls.rs b/crates/navigator-cli/src/tls.rs index a885161b6..e977b0cc7 100644 --- a/crates/navigator-cli/src/tls.rs +++ b/crates/navigator-cli/src/tls.rs @@ -29,8 +29,8 @@ pub struct TlsOptions { ca: Option, cert: Option, key: Option, - /// Cluster name for resolving default cert directory. - cluster_name: Option, + /// Gateway name for resolving default cert directory. + gateway_name: Option, /// Edge auth bearer token — when set, disables mTLS client certs and /// injects authentication headers on every gRPC request instead. pub edge_token: Option, @@ -42,7 +42,7 @@ impl TlsOptions { ca, cert, key, - cluster_name: None, + gateway_name: None, edge_token: None, } } @@ -51,16 +51,16 @@ impl TlsOptions { self.ca.is_some() || self.cert.is_some() || self.key.is_some() } - /// Return the cluster name, if set. - pub fn cluster_name(&self) -> Option<&str> { - self.cluster_name.as_deref() + /// Return the gateway name, if set. + pub fn gateway_name(&self) -> Option<&str> { + self.gateway_name.as_deref() } - /// Set the cluster name for cert directory resolution. + /// Set the gateway name for cert directory resolution. #[must_use] - pub fn with_cluster_name(&self, name: &str) -> Self { + pub fn with_gateway_name(&self, name: &str) -> Self { Self { - cluster_name: Some(name.to_string()), + gateway_name: Some(name.to_string()), ..self.clone() } } @@ -68,9 +68,9 @@ impl TlsOptions { #[must_use] pub fn with_default_paths(&self, server: &str) -> Self { let base = self - .cluster_name + .gateway_name .as_deref() - .and_then(tls_dir_for_cluster) + .and_then(tls_dir_for_gateway) .or_else(|| default_tls_dir(server)); Self { ca: self @@ -85,7 +85,7 @@ impl TlsOptions { .key .clone() .or_else(|| base.as_ref().map(|dir| dir.join("tls.key"))), - cluster_name: self.cluster_name.clone(), + gateway_name: self.gateway_name.clone(), ..self.clone() } } @@ -102,18 +102,18 @@ pub struct TlsMaterials { key: Vec, } -/// Resolve the TLS cert directory for a known cluster name. -fn tls_dir_for_cluster(name: &str) -> Option { +/// Resolve the TLS cert directory for a known gateway name. +fn tls_dir_for_gateway(name: &str) -> Option { let safe_name = sanitize_name(name); - let base = xdg_config_dir().ok()?.join("openshell").join("clusters"); + let base = xdg_config_dir().ok()?.join("openshell").join("gateways"); Some(base.join(safe_name).join("mtls")) } /// Fallback TLS directory resolution from a server URL. /// -/// Used when no cluster name is set (e.g., `SshProxy` which receives a raw URL). +/// Used when no gateway name is set (e.g., `SshProxy` which receives a raw URL). fn default_tls_dir(server: &str) -> Option { - let mut name = std::env::var("OPENSHELL_CLUSTER_NAME") + let mut name = std::env::var("OPENSHELL_GATEWAY_NAME") .ok() .filter(|value| !value.trim().is_empty()); @@ -132,7 +132,7 @@ fn default_tls_dir(server: &str) -> Option { let name = name.unwrap_or_else(|| "openshell".to_string()); let safe_name = sanitize_name(&name); - let base = xdg_config_dir().ok()?.join("openshell").join("clusters"); + let base = xdg_config_dir().ok()?.join("openshell").join("gateways"); Some(base.join(safe_name).join("mtls")) } diff --git a/crates/navigator-server/src/sandbox/mod.rs b/crates/navigator-server/src/sandbox/mod.rs index d4a2dd424..db9dee306 100644 --- a/crates/navigator-server/src/sandbox/mod.rs +++ b/crates/navigator-server/src/sandbox/mod.rs @@ -28,9 +28,9 @@ const KUBE_API_TIMEOUT: Duration = Duration::from_secs(30); const SANDBOX_GROUP: &str = "agents.x-k8s.io"; const SANDBOX_VERSION: &str = "v1alpha1"; pub const SANDBOX_KIND: &str = "Sandbox"; -const SANDBOX_ID_LABEL: &str = "navigator.ai/sandbox-id"; -const SANDBOX_MANAGED_LABEL: &str = "navigator.ai/managed-by"; -const SANDBOX_MANAGED_VALUE: &str = "navigator"; +const SANDBOX_ID_LABEL: &str = "openshell.ai/sandbox-id"; +const SANDBOX_MANAGED_LABEL: &str = "openshell.ai/managed-by"; +const SANDBOX_MANAGED_VALUE: &str = "openshell"; #[derive(Clone)] pub struct SandboxClient { @@ -898,8 +898,8 @@ fn sandbox_template_to_k8s( container.insert( "volumeMounts".to_string(), serde_json::json!([{ - "name": "navigator-client-tls", - "mountPath": "/etc/navigator-tls/client", + "name": "openshell-client-tls", + "mountPath": "/etc/openshell-tls/client", "readOnly": true }]), ); @@ -918,7 +918,7 @@ fn sandbox_template_to_k8s( spec.insert( "volumes".to_string(), serde_json::json!([{ - "name": "navigator-client-tls", + "name": "openshell-client-tls", "secret": { "secretName": client_tls_secret_name } }]), ); @@ -968,7 +968,7 @@ fn inject_pod_template( .or_insert_with(|| serde_json::Value::Array(Vec::new())); if let Some(volumes_arr) = volumes.as_array_mut() { volumes_arr.push(serde_json::json!({ - "name": "navigator-client-tls", + "name": "openshell-client-tls", "secret": { "secretName": client_tls_secret_name } })); } @@ -1015,8 +1015,8 @@ fn inject_pod_template( .or_insert_with(|| serde_json::Value::Array(Vec::new())); if let Some(mounts_arr) = mounts.as_array_mut() { mounts_arr.push(serde_json::json!({ - "name": "navigator-client-tls", - "mountPath": "/etc/navigator-tls/client", + "name": "openshell-client-tls", + "mountPath": "/etc/openshell-tls/client", "readOnly": true })); } diff --git a/crates/navigator-tui/src/app.rs b/crates/navigator-tui/src/app.rs index 044060ca2..aacf37ce6 100644 --- a/crates/navigator-tui/src/app.rs +++ b/crates/navigator-tui/src/app.rs @@ -33,7 +33,7 @@ pub enum InputMode { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Focus { // Dashboard screen - Clusters, + Gateways, Providers, Sandboxes, // Sandbox screen — metadata pane is always visible (non-interactive); @@ -84,10 +84,10 @@ impl LogSourceFilter { } // --------------------------------------------------------------------------- -// Cluster entry +// Gateway entry // --------------------------------------------------------------------------- -pub struct ClusterEntry { +pub struct GatewayEntry { pub name: String, pub endpoint: String, pub is_remote: bool, @@ -276,16 +276,16 @@ pub struct App { /// When the splash screen was shown (for auto-dismiss timing). pub splash_start: Option, - // Active cluster connection - pub cluster_name: String, + // Active gateway connection + pub gateway_name: String, pub endpoint: String, pub client: NavigatorClient, pub status_text: String, - // Cluster list - pub clusters: Vec, - pub cluster_selected: usize, - pub pending_cluster_switch: Option, + // Gateway list + pub gateways: Vec, + pub gateway_selected: usize, + pub pending_gateway_switch: Option, // Provider list pub provider_names: Vec, @@ -356,21 +356,21 @@ pub struct App { } impl App { - pub fn new(client: NavigatorClient, cluster_name: String, endpoint: String) -> Self { + pub fn new(client: NavigatorClient, gateway_name: String, endpoint: String) -> Self { Self { running: true, screen: Screen::Splash, input_mode: InputMode::Normal, - focus: Focus::Clusters, + focus: Focus::Gateways, command_input: String::new(), splash_start: Some(Instant::now()), - cluster_name, + gateway_name, endpoint, client, status_text: String::from("connecting..."), - clusters: Vec::new(), - cluster_selected: 0, - pending_cluster_switch: None, + gateways: Vec::new(), + gateway_selected: 0, + pending_gateway_switch: None, provider_names: Vec::new(), provider_types: Vec::new(), provider_cred_keys: Vec::new(), @@ -485,7 +485,7 @@ impl App { fn handle_normal_key(&mut self, key: KeyEvent) { match self.focus { - Focus::Clusters => self.handle_clusters_key(key), + Focus::Gateways => self.handle_gateways_key(key), Focus::Providers => self.handle_providers_key(key), Focus::Sandboxes => self.handle_sandboxes_key(key), Focus::SandboxPolicy => self.handle_policy_key(key), @@ -493,7 +493,7 @@ impl App { } } - fn handle_clusters_key(&mut self, key: KeyEvent) { + fn handle_gateways_key(&mut self, key: KeyEvent) { match key.code { KeyCode::Char('q') => self.running = false, KeyCode::Tab => self.focus = Focus::Providers, @@ -503,18 +503,18 @@ impl App { self.command_input.clear(); } KeyCode::Char('j') | KeyCode::Down => { - if !self.clusters.is_empty() { - self.cluster_selected = - (self.cluster_selected + 1).min(self.clusters.len() - 1); + if !self.gateways.is_empty() { + self.gateway_selected = + (self.gateway_selected + 1).min(self.gateways.len() - 1); } } KeyCode::Char('k') | KeyCode::Up => { - self.cluster_selected = self.cluster_selected.saturating_sub(1); + self.gateway_selected = self.gateway_selected.saturating_sub(1); } KeyCode::Enter => { - if let Some(entry) = self.clusters.get(self.cluster_selected) { - if entry.name != self.cluster_name { - self.pending_cluster_switch = Some(entry.name.clone()); + if let Some(entry) = self.gateways.get(self.gateway_selected) { + if entry.name != self.gateway_name { + self.pending_gateway_switch = Some(entry.name.clone()); } self.focus = Focus::Providers; } @@ -541,7 +541,7 @@ impl App { match key.code { KeyCode::Char('q') => self.running = false, KeyCode::Tab => self.focus = Focus::Sandboxes, - KeyCode::BackTab => self.focus = Focus::Clusters, + KeyCode::BackTab => self.focus = Focus::Gateways, KeyCode::Char(':') => { self.input_mode = InputMode::Command; self.command_input.clear(); @@ -582,7 +582,7 @@ impl App { fn handle_sandboxes_key(&mut self, key: KeyEvent) { match key.code { KeyCode::Char('q') => self.running = false, - KeyCode::Tab => self.focus = Focus::Clusters, + KeyCode::Tab => self.focus = Focus::Gateways, KeyCode::BackTab => self.focus = Focus::Providers, KeyCode::Char(':') => { self.input_mode = InputMode::Command; @@ -1308,7 +1308,7 @@ impl App { } } - /// Reset sandbox and provider state after switching clusters. + /// Reset sandbox and provider state after switching gateways. pub fn reset_sandbox_state(&mut self) { self.stop_anim(); self.cancel_log_stream(); diff --git a/crates/navigator-tui/src/lib.rs b/crates/navigator-tui/src/lib.rs index 89a4dccc1..5181cbdb7 100644 --- a/crates/navigator-tui/src/lib.rs +++ b/crates/navigator-tui/src/lib.rs @@ -22,7 +22,7 @@ use ratatui::backend::CrosstermBackend; use tokio::sync::mpsc; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity}; -use app::{App, ClusterEntry, Focus, LogLine, Screen}; +use app::{App, Focus, GatewayEntry, LogLine, Screen}; use event::{Event, EventHandler}; /// Duration to show the splash screen before auto-dismissing. @@ -31,9 +31,9 @@ const SPLASH_DURATION: Duration = Duration::from_secs(3); /// Launch the OpenShell TUI. /// /// `channel` must be a connected gRPC channel to the OpenShell gateway. -pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result<()> { +pub async fn run(channel: Channel, gateway_name: &str, endpoint: &str) -> Result<()> { let client = NavigatorClient::new(channel); - let mut app = App::new(client, cluster_name.to_string(), endpoint.to_string()); + let mut app = App::new(client, gateway_name.to_string(), endpoint.to_string()); enable_raw_mode().into_diagnostic()?; let mut stdout = io::stdout(); @@ -45,7 +45,7 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result let mut events = EventHandler::new(Duration::from_secs(2)); - refresh_cluster_list(&mut app); + refresh_gateway_list(&mut app); refresh_data(&mut app).await; while app.running { @@ -57,8 +57,8 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result Some(Event::Key(key)) => { app.handle_key(key); // Handle async actions triggered by key presses. - if app.pending_cluster_switch.is_some() { - handle_cluster_switch(&mut app).await; + if app.pending_gateway_switch.is_some() { + handle_gateway_switch(&mut app).await; } if app.pending_log_fetch { app.pending_log_fetch = false; @@ -199,7 +199,7 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result } } - refresh_cluster_list(&mut app); + refresh_gateway_list(&mut app); refresh_data(&mut app).await; // Auto-refresh the policy view when a new version is detected. @@ -327,15 +327,15 @@ pub async fn run(channel: Channel, cluster_name: &str, endpoint: &str) -> Result } // --------------------------------------------------------------------------- -// Cluster discovery and switching +// Gateway discovery and switching // --------------------------------------------------------------------------- -/// Refresh the list of known clusters from disk. -fn refresh_cluster_list(app: &mut App) { - if let Ok(clusters) = navigator_bootstrap::list_clusters() { - app.clusters = clusters +/// Refresh the list of known gateways from disk. +fn refresh_gateway_list(app: &mut App) { + if let Ok(gateways) = navigator_bootstrap::list_gateways() { + app.gateways = gateways .into_iter() - .map(|m| ClusterEntry { + .map(|m| GatewayEntry { name: m.name, endpoint: m.gateway_endpoint, is_remote: m.is_remote, @@ -343,39 +343,39 @@ fn refresh_cluster_list(app: &mut App) { .collect(); // Keep selection in bounds. - if app.cluster_selected >= app.clusters.len() && !app.clusters.is_empty() { - app.cluster_selected = app.clusters.len() - 1; + if app.gateway_selected >= app.gateways.len() && !app.gateways.is_empty() { + app.gateway_selected = app.gateways.len() - 1; } - // If the active cluster appears in the list, move cursor to it on first load. - if let Some(idx) = app.clusters.iter().position(|c| c.name == app.cluster_name) { + // If the active gateway appears in the list, move cursor to it on first load. + if let Some(idx) = app.gateways.iter().position(|g| g.name == app.gateway_name) { // Only snap the cursor when it's still at 0 (initial state). - if app.cluster_selected == 0 { - app.cluster_selected = idx; + if app.gateway_selected == 0 { + app.gateway_selected = idx; } } } } -/// Handle a pending cluster switch requested by the user. -async fn handle_cluster_switch(app: &mut App) { - let Some(name) = app.pending_cluster_switch.take() else { +/// Handle a pending gateway switch requested by the user. +async fn handle_gateway_switch(app: &mut App) { + let Some(name) = app.pending_gateway_switch.take() else { return; }; - // Look up the endpoint from the cluster list. - let endpoint = match app.clusters.iter().find(|c| c.name == name) { - Some(c) => c.endpoint.clone(), + // Look up the endpoint from the gateway list. + let endpoint = match app.gateways.iter().find(|g| g.name == name) { + Some(g) => g.endpoint.clone(), None => return, }; - match connect_to_cluster(&name, &endpoint).await { + match connect_to_gateway(&name, &endpoint).await { Ok(channel) => { app.client = NavigatorClient::new(channel); - app.cluster_name = name; + app.gateway_name = name; app.endpoint = endpoint; app.reset_sandbox_state(); - // Immediately refresh data for the new cluster. + // Immediately refresh data for the new gateway. refresh_data(app).await; } Err(e) => { @@ -384,20 +384,20 @@ async fn handle_cluster_switch(app: &mut App) { } } -/// Build a gRPC channel to a cluster using its mTLS certs on disk. -async fn connect_to_cluster(name: &str, endpoint: &str) -> Result { - let mtls_dir = cluster_mtls_dir(name) - .ok_or_else(|| miette::miette!("cannot determine config directory for cluster {name}"))?; +/// Build a gRPC channel to a gateway using its mTLS certs on disk. +async fn connect_to_gateway(name: &str, endpoint: &str) -> Result { + let mtls_dir = gateway_mtls_dir(name) + .ok_or_else(|| miette::miette!("cannot determine config directory for gateway {name}"))?; let ca = std::fs::read(mtls_dir.join("ca.crt")) .into_diagnostic() - .map_err(|_| miette::miette!("missing CA cert for cluster {name}"))?; + .map_err(|_| miette::miette!("missing CA cert for gateway {name}"))?; let cert = std::fs::read(mtls_dir.join("tls.crt")) .into_diagnostic() - .map_err(|_| miette::miette!("missing client cert for cluster {name}"))?; + .map_err(|_| miette::miette!("missing client cert for gateway {name}"))?; let key = std::fs::read(mtls_dir.join("tls.key")) .into_diagnostic() - .map_err(|_| miette::miette!("missing client key for cluster {name}"))?; + .map_err(|_| miette::miette!("missing client key for gateway {name}"))?; let tls_config = ClientTlsConfig::new() .ca_certificate(Certificate::from_pem(ca)) @@ -417,8 +417,8 @@ async fn connect_to_cluster(name: &str, endpoint: &str) -> Result { Ok(channel) } -/// Resolve the mTLS cert directory for a cluster. -fn cluster_mtls_dir(name: &str) -> Option { +/// Resolve the mTLS cert directory for a gateway. +fn gateway_mtls_dir(name: &str) -> Option { let config_dir = std::env::var("XDG_CONFIG_HOME") .map(PathBuf::from) .or_else(|_| std::env::var("HOME").map(|h| PathBuf::from(h).join(".config"))) @@ -426,7 +426,7 @@ fn cluster_mtls_dir(name: &str) -> Option { Some( config_dir .join("openshell") - .join("clusters") + .join("gateways") .join(name) .join("mtls"), ) @@ -733,9 +733,9 @@ async fn handle_shell_connect( } }; let exe_str = shell_escape(&exe.to_string_lossy()); - let cluster = shell_escape(&app.cluster_name); + let gateway = shell_escape(&app.gateway_name); let proxy_command = format!( - "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway {cluster}", + "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway-name {gateway}", session.sandbox_id, session.token, ); // Step 5: Build the SSH command. @@ -875,9 +875,9 @@ async fn handle_exec_command( } }; let exe_str = shell_escape(&exe.to_string_lossy()); - let cluster = shell_escape(&app.cluster_name); + let gateway = shell_escape(&app.gateway_name); let proxy_command = format!( - "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway {cluster}", + "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway-name {gateway}", session.sandbox_id, session.token, ); @@ -1149,7 +1149,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { app.pending_forward_ports = ports.clone(); let endpoint = app.endpoint.clone(); - let cluster_name = app.cluster_name.clone(); + let gateway_name = app.gateway_name.clone(); let need_ready = !ports.is_empty() || !app.pending_exec_command.is_empty(); tokio::spawn(async move { @@ -1239,7 +1239,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { start_port_forwards( &mut client, &endpoint, - &cluster_name, + &gateway_name, &sandbox_name, &sandbox_id, &ports, @@ -1259,7 +1259,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { async fn start_port_forwards( client: &mut NavigatorClient, endpoint: &str, - cluster_name: &str, + gateway_name: &str, sandbox_name: &str, sandbox_id: &str, ports: &[u16], @@ -1301,9 +1301,9 @@ async fn start_port_forwards( } }; let exe_str = shell_escape(&exe.to_string_lossy()); - let cluster = shell_escape(cluster_name); + let gateway = shell_escape(gateway_name); let proxy_command = format!( - "{exe_str} ssh-proxy --gateway {gateway_url} --sandbox-id {} --token {} --cluster {cluster}", + "{exe_str} ssh-proxy --gateway-endpoint {gateway_url} --sandbox-id {} --token {} --gateway-name {gateway}", session.sandbox_id, session.token, ); diff --git a/crates/navigator-tui/src/ui/dashboard.rs b/crates/navigator-tui/src/ui/dashboard.rs index d310ef533..0426c2f36 100644 --- a/crates/navigator-tui/src/ui/dashboard.rs +++ b/crates/navigator-tui/src/ui/dashboard.rs @@ -19,13 +19,13 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect) { ]) .split(area); - draw_cluster_list(frame, app, chunks[0]); + draw_gateway_list(frame, app, chunks[0]); super::providers::draw(frame, app, chunks[1], app.focus == Focus::Providers); super::sandboxes::draw(frame, app, chunks[2], app.focus == Focus::Sandboxes); } -fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) { - let focused = app.focus == Focus::Clusters; +fn draw_gateway_list(frame: &mut Frame<'_>, app: &App, area: Rect) { + let focused = app.focus == Focus::Gateways; let header = Row::new(vec![ Cell::from(Span::styled(" NAME", styles::MUTED)), @@ -35,12 +35,12 @@ fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) { .bottom_margin(1); let rows: Vec> = app - .clusters + .gateways .iter() .enumerate() .map(|(i, entry)| { - let is_active = entry.name == app.cluster_name; - let is_cursor = focused && i == app.cluster_selected; + let is_active = entry.name == app.gateway_name; + let is_cursor = focused && i == app.gateway_selected; let cursor = if is_cursor { ">" } else { " " }; let dot = if is_active { "* " } else { " " }; @@ -92,7 +92,7 @@ fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) { }; let block = Block::default() - .title(Span::styled(" Clusters ", styles::HEADING)) + .title(Span::styled(" Gateways ", styles::HEADING)) .borders(Borders::ALL) .border_style(border_style) .padding(Padding::horizontal(1)); @@ -107,14 +107,14 @@ fn draw_cluster_list(frame: &mut Frame<'_>, app: &App, area: Rect) { frame.render_widget(table, area); - if app.clusters.is_empty() { + if app.gateways.is_empty() { let inner = Rect { x: area.x + 2, y: area.y + 2, width: area.width.saturating_sub(4), height: area.height.saturating_sub(3), }; - let msg = Paragraph::new(Span::styled(" No clusters found.", styles::MUTED)); + let msg = Paragraph::new(Span::styled(" No gateways found.", styles::MUTED)); frame.render_widget(msg, inner); } } diff --git a/crates/navigator-tui/src/ui/mod.rs b/crates/navigator-tui/src/ui/mod.rs index 8bbb63b12..640490ba0 100644 --- a/crates/navigator-tui/src/ui/mod.rs +++ b/crates/navigator-tui/src/ui/mod.rs @@ -115,8 +115,8 @@ fn draw_title_bar(frame: &mut Frame<'_>, app: &App, area: Rect) { .add_modifier(ratatui::style::Modifier::BOLD), ), Span::styled(" | ", styles::MUTED), - Span::styled("Current Cluster: ", styles::TEXT), - Span::styled(&app.cluster_name, styles::HEADING), + Span::styled("Current Gateway: ", styles::TEXT), + Span::styled(&app.gateway_name, styles::HEADING), Span::styled(" (", styles::MUTED), status_span, Span::styled(")", styles::MUTED), diff --git a/crates/navigator-tui/src/ui/providers.rs b/crates/navigator-tui/src/ui/providers.rs index a453da788..5e77579d2 100644 --- a/crates/navigator-tui/src/ui/providers.rs +++ b/crates/navigator-tui/src/ui/providers.rs @@ -71,7 +71,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect, focused: bool) { Line::from(vec![ Span::styled(" Providers ", styles::HEADING), Span::styled("- ", styles::BORDER), - Span::styled(&app.cluster_name, styles::MUTED), + Span::styled(&app.gateway_name, styles::MUTED), Span::styled(" ", styles::MUTED), ]) }; diff --git a/crates/navigator-tui/src/ui/sandboxes.rs b/crates/navigator-tui/src/ui/sandboxes.rs index e38f47552..661b95723 100644 --- a/crates/navigator-tui/src/ui/sandboxes.rs +++ b/crates/navigator-tui/src/ui/sandboxes.rs @@ -78,7 +78,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect, focused: bool) { let title = Line::from(vec![ Span::styled(" Sandboxes ", styles::HEADING), Span::styled("─ ", styles::BORDER), - Span::styled(&app.cluster_name, styles::MUTED), + Span::styled(&app.gateway_name, styles::MUTED), Span::styled(" ", styles::MUTED), ]); diff --git a/deploy/docker/Dockerfile.cluster b/deploy/docker/Dockerfile.cluster index 3cb288a9c..2fd5cfb93 100644 --- a/deploy/docker/Dockerfile.cluster +++ b/deploy/docker/Dockerfile.cluster @@ -8,7 +8,7 @@ # - HelmChart CR for auto-deploying OpenShell # - Custom entrypoint for DNS configuration in Docker environments # -# Component images (navigator/server, navigator/sandbox) are +# Component images (openshell/server, openshell/sandbox) are # pulled at runtime from the distribution registry rather than bundled as tarballs. # Registry credentials are generated by the entrypoint script at container start. # @@ -30,8 +30,8 @@ FROM rancher/k3s:${K3S_VERSION} RUN mkdir -p /var/lib/rancher/k3s/server/manifests \ /var/lib/rancher/k3s/server/static/charts \ /etc/rancher/k3s \ - /opt/navigator/manifests \ - /opt/navigator/charts + /opt/openshell/manifests \ + /opt/openshell/charts # Copy entrypoint script that configures DNS for Docker environments # This script detects the host gateway IP and configures CoreDNS to use it @@ -48,11 +48,11 @@ RUN chmod +x /usr/local/bin/cluster-healthcheck.sh # Copy packaged helm charts to a staging directory that won't be # overwritten by the /var/lib/rancher/k3s volume mount. The entrypoint # script copies them into the k3s static charts directory at container start. -COPY deploy/docker/.build/charts/*.tgz /opt/navigator/charts/ +COPY deploy/docker/.build/charts/*.tgz /opt/openshell/charts/ # Copy Kubernetes manifests to a persistent location that won't be overwritten by the volume mount. # The bootstrap code will copy these to /var/lib/rancher/k3s/server/manifests/ after cluster start. -COPY deploy/kube/manifests/*.yaml /opt/navigator/manifests/ +COPY deploy/kube/manifests/*.yaml /opt/openshell/manifests/ # Use custom entrypoint that configures DNS before starting k3s ENTRYPOINT ["/usr/local/bin/cluster-entrypoint.sh"] diff --git a/deploy/docker/cluster-entrypoint.sh b/deploy/docker/cluster-entrypoint.sh index 012456189..33f6d348f 100644 --- a/deploy/docker/cluster-entrypoint.sh +++ b/deploy/docker/cluster-entrypoint.sh @@ -27,6 +27,36 @@ set -e RESOLV_CONF="/etc/rancher/k3s/resolv.conf" +has_default_route() { + ip -4 route show default 2>/dev/null | grep -q '^default ' \ + || ip -6 route show default 2>/dev/null | grep -q '^default ' +} + +wait_for_default_route() { + attempts=${1:-30} + delay_s=${2:-1} + i=1 + + while [ "$i" -le "$attempts" ]; do + if has_default_route; then + return 0 + fi + sleep "$delay_s" + i=$((i + 1)) + done + + echo "Error: no default route present before starting k3s" + echo "IPv4 routes:" + ip -4 route show 2>/dev/null || true + echo "IPv6 routes:" + ip -6 route show 2>/dev/null || true + echo "/proc/net/route:" + cat /proc/net/route 2>/dev/null || true + echo "/proc/net/ipv6_route:" + cat /proc/net/ipv6_route 2>/dev/null || true + return 1 +} + # --------------------------------------------------------------------------- # Configure DNS proxy via iptables # --------------------------------------------------------------------------- @@ -165,12 +195,12 @@ else fi # Copy bundled Helm chart tarballs to the k3s static charts directory. -# These are stored in /opt/navigator/charts/ because the volume mount +# These are stored in /opt/openshell/charts/ because the volume mount # on /var/lib/rancher/k3s overwrites any files baked into that path. # Without this, a persistent volume from a previous deploy would keep # serving stale chart tarballs. K3S_CHARTS="/var/lib/rancher/k3s/server/static/charts" -BUNDLED_CHARTS="/opt/navigator/charts" +BUNDLED_CHARTS="/opt/openshell/charts" CHART_CHECKSUM="" if [ -d "$BUNDLED_CHARTS" ]; then @@ -179,30 +209,30 @@ if [ -d "$BUNDLED_CHARTS" ]; then [ ! -f "$chart" ] && continue cp "$chart" "$K3S_CHARTS/" done - # Compute a checksum of the navigator chart so we can inject it into the + # Compute a checksum of the openshell chart so we can inject it into the # HelmChart manifest below. When the chart content changes between image # versions the checksum changes, which modifies the HelmChart CR spec and # forces the k3s Helm controller to re-install. - NAV_CHART="$BUNDLED_CHARTS/navigator-0.1.0.tgz" - if [ -f "$NAV_CHART" ]; then + OPENSHELL_CHART="$BUNDLED_CHARTS/openshell-0.1.0.tgz" + if [ -f "$OPENSHELL_CHART" ]; then if command -v sha256sum >/dev/null 2>&1; then - CHART_CHECKSUM=$(sha256sum "$NAV_CHART" | cut -d ' ' -f 1) + CHART_CHECKSUM=$(sha256sum "$OPENSHELL_CHART" | cut -d ' ' -f 1) elif command -v shasum >/dev/null 2>&1; then - CHART_CHECKSUM=$(shasum -a 256 "$NAV_CHART" | cut -d ' ' -f 1) + CHART_CHECKSUM=$(shasum -a 256 "$OPENSHELL_CHART" | cut -d ' ' -f 1) fi fi fi # Copy bundled manifests to k3s manifests directory. -# These are stored in /opt/navigator/manifests/ because the volume mount +# These are stored in /opt/openshell/manifests/ because the volume mount # on /var/lib/rancher/k3s overwrites any files baked into that path. # # When reusing a persistent volume from a previous deploy, stale manifests # (e.g. envoy-gateway-helmchart.yaml from an older image) may linger. -# We remove any navigator-managed manifests that no longer exist in the +# We remove any openshell-managed manifests that no longer exist in the # bundled set so k3s does not keep installing removed components. K3S_MANIFESTS="/var/lib/rancher/k3s/server/manifests" -BUNDLED_MANIFESTS="/opt/navigator/manifests" +BUNDLED_MANIFESTS="/opt/openshell/manifests" if [ -d "$BUNDLED_MANIFESTS" ]; then echo "Copying bundled manifests to k3s..." @@ -211,10 +241,11 @@ if [ -d "$BUNDLED_MANIFESTS" ]; then cp "$manifest" "$K3S_MANIFESTS/" done - # Remove navigator-managed manifests that are no longer bundled. - # Only clean up files that look like navigator manifests (navigator-* or + # Remove openshell-managed manifests that are no longer bundled. + # Only clean up files that look like openshell manifests (openshell-* or # envoy-gateway-* or agent-*) to avoid removing built-in k3s manifests. - for existing in "$K3S_MANIFESTS"/navigator-*.yaml \ + for existing in "$K3S_MANIFESTS"/openshell-*.yaml \ + "$K3S_MANIFESTS"/navigator-*.yaml \ "$K3S_MANIFESTS"/envoy-gateway-*.yaml \ "$K3S_MANIFESTS"/agent-*.yaml; do [ ! -f "$existing" ] && continue @@ -233,7 +264,7 @@ fi # images in the HelmChart manifest so k3s deploys the locally-pushed versions. # When IMAGE_PULL_POLICY is set, override the default "Always" so k3s uses # images already present in containerd instead of pulling from the registry. -HELMCHART="/var/lib/rancher/k3s/server/manifests/navigator-helmchart.yaml" +HELMCHART="/var/lib/rancher/k3s/server/manifests/openshell-helmchart.yaml" if [ -n "${IMAGE_REPO_BASE:-}" ] && [ -f "$HELMCHART" ]; then target_tag="${IMAGE_TAG:-latest}" @@ -343,5 +374,10 @@ else sed -i '/__CHART_CHECKSUM__/d' "$HELMCHART" fi +# Docker Desktop can briefly start the container before its bridge default route +# is fully installed. k3s exits immediately in that state, so wait briefly for +# routing to settle first. +wait_for_default_route + # Execute k3s with explicit resolv-conf. exec /bin/k3s "$@" --resolv-conf="$RESOLV_CONF" diff --git a/deploy/docker/cluster-healthcheck.sh b/deploy/docker/cluster-healthcheck.sh index e253626b8..829f2ccb1 100644 --- a/deploy/docker/cluster-healthcheck.sh +++ b/deploy/docker/cluster-healthcheck.sh @@ -9,12 +9,12 @@ export KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get --raw='/readyz' >/dev/null 2>&1 || exit 1 -kubectl -n navigator get statefulset/navigator >/dev/null 2>&1 || exit 1 -kubectl -n navigator wait --for=jsonpath='{.status.readyReplicas}'=1 statefulset/navigator --timeout=1s >/dev/null 2>&1 || exit 1 +kubectl -n openshell get statefulset/openshell >/dev/null 2>&1 || exit 1 +kubectl -n openshell wait --for=jsonpath='{.status.readyReplicas}'=1 statefulset/openshell --timeout=1s >/dev/null 2>&1 || exit 1 -# Verify TLS secrets exist (created by navigator-bootstrap before the StatefulSet starts) +# Verify TLS secrets exist (created by openshell-bootstrap before the StatefulSet starts) # Skip when TLS is disabled — secrets are not required. if [ "${DISABLE_TLS:-}" != "true" ]; then - kubectl -n navigator get secret navigator-server-tls >/dev/null 2>&1 || exit 1 - kubectl -n navigator get secret navigator-client-tls >/dev/null 2>&1 || exit 1 + kubectl -n openshell get secret openshell-server-tls >/dev/null 2>&1 || exit 1 + kubectl -n openshell get secret openshell-client-tls >/dev/null 2>&1 || exit 1 fi diff --git a/deploy/docker/sandbox/Dockerfile.nvidia b/deploy/docker/sandbox/Dockerfile.nvidia index 417e7c548..7d8a42be8 100644 --- a/deploy/docker/sandbox/Dockerfile.nvidia +++ b/deploy/docker/sandbox/Dockerfile.nvidia @@ -10,7 +10,7 @@ # # TODO: Add NVIDIA-specific layers (CUDA toolkit, cuDNN, GPU drivers, etc.) -ARG BASE_IMAGE=navigator/sandbox:dev +ARG BASE_IMAGE=openshell/sandbox:dev FROM ${BASE_IMAGE} # Placeholder: add NVIDIA GPU packages and configuration here. diff --git a/deploy/helm/navigator/templates/rolebinding.yaml b/deploy/helm/navigator/templates/rolebinding.yaml deleted file mode 100644 index 201549cbe..000000000 --- a/deploy/helm/navigator/templates/rolebinding.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "navigator.fullname" . }}-sandbox - labels: - {{- include "navigator.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ include "navigator.fullname" . }}-sandbox -subjects: - - kind: ServiceAccount - name: {{ include "navigator.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} diff --git a/deploy/helm/navigator/templates/serviceaccount.yaml b/deploy/helm/navigator/templates/serviceaccount.yaml deleted file mode 100644 index 9731c6d3b..000000000 --- a/deploy/helm/navigator/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "navigator.serviceAccountName" . }} - labels: - {{- include "navigator.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/deploy/helm/navigator/.helmignore b/deploy/helm/openshell/.helmignore similarity index 100% rename from deploy/helm/navigator/.helmignore rename to deploy/helm/openshell/.helmignore diff --git a/deploy/helm/navigator/Chart.yaml b/deploy/helm/openshell/Chart.yaml similarity index 94% rename from deploy/helm/navigator/Chart.yaml rename to deploy/helm/openshell/Chart.yaml index b3e4e8ce2..f97e316cf 100644 --- a/deploy/helm/navigator/Chart.yaml +++ b/deploy/helm/openshell/Chart.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 apiVersion: v2 -name: navigator +name: openshell description: runtime environment for autonomous agents type: application version: 0.1.0 diff --git a/deploy/helm/navigator/templates/_helpers.tpl b/deploy/helm/openshell/templates/_helpers.tpl similarity index 73% rename from deploy/helm/navigator/templates/_helpers.tpl rename to deploy/helm/openshell/templates/_helpers.tpl index a2b3eaa42..10fd3de0d 100644 --- a/deploy/helm/navigator/templates/_helpers.tpl +++ b/deploy/helm/openshell/templates/_helpers.tpl @@ -1,14 +1,14 @@ {{/* Expand the name of the chart. */}} -{{- define "navigator.name" -}} +{{- define "openshell.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. */}} -{{- define "navigator.fullname" -}} +{{- define "openshell.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -24,16 +24,16 @@ Create a default fully qualified app name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "navigator.chart" -}} +{{- define "openshell.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "navigator.labels" -}} -helm.sh/chart: {{ include "navigator.chart" . }} -{{ include "navigator.selectorLabels" . }} +{{- define "openshell.labels" -}} +helm.sh/chart: {{ include "openshell.chart" . }} +{{ include "openshell.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -43,17 +43,17 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "navigator.selectorLabels" -}} -app.kubernetes.io/name: {{ include "navigator.name" . }} +{{- define "openshell.selectorLabels" -}} +app.kubernetes.io/name: {{ include "openshell.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "navigator.serviceAccountName" -}} +{{- define "openshell.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "navigator.fullname" .) .Values.serviceAccount.name }} +{{- default (include "openshell.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/helm/navigator/templates/networkpolicy.yaml b/deploy/helm/openshell/templates/networkpolicy.yaml similarity index 61% rename from deploy/helm/navigator/templates/networkpolicy.yaml rename to deploy/helm/openshell/templates/networkpolicy.yaml index aea332dae..3e0b6f504 100644 --- a/deploy/helm/navigator/templates/networkpolicy.yaml +++ b/deploy/helm/openshell/templates/networkpolicy.yaml @@ -1,20 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + {{- if .Values.networkPolicy.enabled }} # NetworkPolicy restricting SSH ingress on sandbox pods to the gateway pod. # Sandbox pods are dynamically created by the server and labelled with -# navigator.ai/managed-by=navigator. This policy ensures only the gateway -# (navigator server) pod can reach the sandbox SSH port (2222), blocking +# openshell.ai/managed-by=openshell. This policy ensures only the gateway +# (openshell server) pod can reach the sandbox SSH port (2222), blocking # lateral movement from other in-cluster workloads. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: {{ include "navigator.fullname" . }}-sandbox-ssh + name: {{ include "openshell.fullname" . }}-sandbox-ssh namespace: {{ .Values.server.sandboxNamespace }} labels: - {{- include "navigator.labels" . | nindent 4 }} + {{- include "openshell.labels" . | nindent 4 }} spec: podSelector: matchLabels: - navigator.ai/managed-by: navigator + openshell.ai/managed-by: openshell policyTypes: - Ingress ingress: @@ -24,7 +27,7 @@ spec: kubernetes.io/metadata.name: {{ .Release.Namespace }} podSelector: matchLabels: - app.kubernetes.io/name: {{ include "navigator.name" . }} + app.kubernetes.io/name: {{ include "openshell.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} ports: - protocol: TCP diff --git a/deploy/helm/navigator/templates/role.yaml b/deploy/helm/openshell/templates/role.yaml similarity index 61% rename from deploy/helm/navigator/templates/role.yaml rename to deploy/helm/openshell/templates/role.yaml index 489afbf48..1d756117c 100644 --- a/deploy/helm/navigator/templates/role.yaml +++ b/deploy/helm/openshell/templates/role.yaml @@ -1,9 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - name: {{ include "navigator.fullname" . }}-sandbox + name: {{ include "openshell.fullname" . }}-sandbox labels: - {{- include "navigator.labels" . | nindent 4 }} + {{- include "openshell.labels" . | nindent 4 }} rules: - apiGroups: - agents.x-k8s.io diff --git a/deploy/helm/openshell/templates/rolebinding.yaml b/deploy/helm/openshell/templates/rolebinding.yaml new file mode 100644 index 000000000..2bb3c7d08 --- /dev/null +++ b/deploy/helm/openshell/templates/rolebinding.yaml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "openshell.fullname" . }}-sandbox + labels: + {{- include "openshell.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "openshell.fullname" . }}-sandbox +subjects: + - kind: ServiceAccount + name: {{ include "openshell.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/helm/navigator/templates/service.yaml b/deploy/helm/openshell/templates/service.yaml similarity index 55% rename from deploy/helm/navigator/templates/service.yaml rename to deploy/helm/openshell/templates/service.yaml index 2086d56a9..0e4aa4b76 100644 --- a/deploy/helm/navigator/templates/service.yaml +++ b/deploy/helm/openshell/templates/service.yaml @@ -1,9 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + apiVersion: v1 kind: Service metadata: - name: {{ include "navigator.fullname" . }} + name: {{ include "openshell.fullname" . }} labels: - {{- include "navigator.labels" . | nindent 4 }} + {{- include "openshell.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: @@ -16,4 +19,4 @@ spec: nodePort: {{ .Values.service.nodePort }} {{- end }} selector: - {{- include "navigator.selectorLabels" . | nindent 4 }} + {{- include "openshell.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/openshell/templates/serviceaccount.yaml b/deploy/helm/openshell/templates/serviceaccount.yaml new file mode 100644 index 000000000..1f03f8e94 --- /dev/null +++ b/deploy/helm/openshell/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "openshell.serviceAccountName" . }} + labels: + {{- include "openshell.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/navigator/templates/statefulset.yaml b/deploy/helm/openshell/templates/statefulset.yaml similarity index 84% rename from deploy/helm/navigator/templates/statefulset.yaml rename to deploy/helm/openshell/templates/statefulset.yaml index d086315e9..ce79e3c82 100644 --- a/deploy/helm/navigator/templates/statefulset.yaml +++ b/deploy/helm/openshell/templates/statefulset.yaml @@ -1,15 +1,18 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + apiVersion: apps/v1 kind: StatefulSet metadata: - name: {{ include "navigator.fullname" . }} + name: {{ include "openshell.fullname" . }} labels: - {{- include "navigator.labels" . | nindent 4 }} + {{- include "openshell.labels" . | nindent 4 }} spec: - serviceName: {{ include "navigator.fullname" . }} + serviceName: {{ include "openshell.fullname" . }} replicas: {{ .Values.replicaCount }} selector: matchLabels: - {{- include "navigator.selectorLabels" . | nindent 6 }} + {{- include "openshell.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} @@ -17,7 +20,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} labels: - {{- include "navigator.labels" . | nindent 8 }} + {{- include "openshell.labels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} @@ -27,7 +30,7 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} - serviceAccountName: {{ include "navigator.serviceAccountName" . }} + serviceAccountName: {{ include "openshell.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: @@ -65,11 +68,11 @@ spec: value: "true" {{- else }} - name: OPENSHELL_TLS_CERT - value: /etc/navigator-tls/server/tls.crt + value: /etc/openshell-tls/server/tls.crt - name: OPENSHELL_TLS_KEY - value: /etc/navigator-tls/server/tls.key + value: /etc/openshell-tls/server/tls.key - name: OPENSHELL_TLS_CLIENT_CA - value: /etc/navigator-tls/client-ca/ca.crt + value: /etc/openshell-tls/client-ca/ca.crt - name: OPENSHELL_CLIENT_TLS_SECRET_NAME value: {{ .Values.server.tls.clientTlsSecretName | quote }} {{- if .Values.server.disableGatewayAuth }} @@ -78,14 +81,14 @@ spec: {{- end }} {{- end }} volumeMounts: - - name: navigator-data - mountPath: /var/navigator + - name: openshell-data + mountPath: /var/openshell {{- if not .Values.server.disableTls }} - name: tls-cert - mountPath: /etc/navigator-tls/server + mountPath: /etc/openshell-tls/server readOnly: true - name: tls-client-ca - mountPath: /etc/navigator-tls/client-ca + mountPath: /etc/openshell-tls/client-ca readOnly: true {{- end }} ports: @@ -131,7 +134,7 @@ spec: {{- end }} volumeClaimTemplates: - metadata: - name: navigator-data + name: openshell-data spec: accessModes: ["ReadWriteOnce"] resources: diff --git a/deploy/helm/navigator/values.yaml b/deploy/helm/openshell/values.yaml similarity index 86% rename from deploy/helm/navigator/values.yaml rename to deploy/helm/openshell/values.yaml index 3553385b1..d2a58b677 100644 --- a/deploy/helm/navigator/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: - repository: ghcr.io/nvidia/nemoclaw/server + repository: ghcr.io/nvidia/openshell/server pullPolicy: Always tag: "latest" @@ -65,11 +65,11 @@ affinity: {} # Server configuration server: logLevel: info - sandboxNamespace: navigator - dbUrl: "sqlite:/var/navigator/navigator.db" - sandboxImage: "d1i0nduu2f6qxk.cloudfront.net/navigator/sandbox:latest" + sandboxNamespace: openshell + dbUrl: "sqlite:/var/openshell/openshell.db" + sandboxImage: "ghcr.io/nvidia/openshell/sandbox:latest" # gRPC endpoint for sandboxes to callback to OpenShell (must be reachable from pods) - grpcEndpoint: "https://navigator.navigator.svc.cluster.local:8080" + grpcEndpoint: "https://openshell.openshell.svc.cluster.local:8080" # Public host/port returned to CLI clients for SSH proxy CONNECT requests. # For local clusters the default 127.0.0.1:8080 is correct; for remote # clusters these should be set to the externally reachable host and port. @@ -90,11 +90,11 @@ server: disableTls: false tls: # K8s secret (type kubernetes.io/tls) with tls.crt and tls.key for the server - certSecretName: navigator-server-tls + certSecretName: openshell-server-tls # K8s secret with ca.crt for client certificate verification - clientCaSecretName: navigator-server-client-ca + clientCaSecretName: openshell-server-client-ca # K8s secret mounted into sandbox pods for mTLS to the server - clientTlsSecretName: navigator-client-tls + clientTlsSecretName: openshell-client-tls # NetworkPolicy restricting SSH ingress on sandbox pods to the gateway only. networkPolicy: diff --git a/deploy/kube/manifests/navigator-helmchart.yaml b/deploy/kube/manifests/openshell-helmchart.yaml similarity index 69% rename from deploy/kube/manifests/navigator-helmchart.yaml rename to deploy/kube/manifests/openshell-helmchart.yaml index 3c97ffde6..b55cd91a8 100644 --- a/deploy/kube/manifests/navigator-helmchart.yaml +++ b/deploy/kube/manifests/openshell-helmchart.yaml @@ -6,7 +6,7 @@ # automatically installed when the k3s cluster starts. # # The chart tarball is served from the k3s static file server at: -# https:///static/charts/navigator-0.1.0.tgz +# https:///static/charts/openshell-0.1.0.tgz # # Component images are pulled at runtime from ghcr.io. # Authentication is configured via /etc/rancher/k3s/registries.yaml @@ -16,27 +16,27 @@ apiVersion: helm.cattle.io/v1 kind: HelmChart metadata: - name: navigator + name: openshell namespace: kube-system spec: - chart: https://%{KUBERNETES_API}%/static/charts/navigator-0.1.0.tgz - targetNamespace: navigator + chart: https://%{KUBERNETES_API}%/static/charts/openshell-0.1.0.tgz + targetNamespace: openshell createNamespace: true valuesContent: |- chartChecksum: __CHART_CHECKSUM__ image: - repository: ghcr.io/nvidia/nemoclaw/server + repository: ghcr.io/nvidia/openshell/server tag: latest pullPolicy: Always server: - sandboxImage: ghcr.io/nvidia/nemoclaw/sandbox:latest + sandboxImage: ghcr.io/nvidia/openshell/sandbox:latest sshGatewayHost: __SSH_GATEWAY_HOST__ sshGatewayPort: __SSH_GATEWAY_PORT__ sshHandshakeSecret: __SSH_HANDSHAKE_SECRET__ - grpcEndpoint: "https://navigator.navigator.svc.cluster.local:8080" + grpcEndpoint: "https://openshell.openshell.svc.cluster.local:8080" disableGatewayAuth: __DISABLE_GATEWAY_AUTH__ disableTls: __DISABLE_TLS__ tls: - certSecretName: navigator-server-tls - clientCaSecretName: navigator-server-client-ca - clientTlsSecretName: navigator-client-tls + certSecretName: openshell-server-tls + clientCaSecretName: openshell-server-client-ca + clientTlsSecretName: openshell-client-tls diff --git a/examples/gateway-deploy-connect.md b/examples/gateway-deploy-connect.md index 25b7c077e..1f084c9f1 100644 --- a/examples/gateway-deploy-connect.md +++ b/examples/gateway-deploy-connect.md @@ -190,7 +190,7 @@ openshell gateway info If the gateway is unreachable, inspect the container: ```bash -docker logs navigator-cluster-openshell +docker logs openshell-cluster-openshell ``` Re-running `gateway start` is idempotent -- it reuses existing diff --git a/examples/private-ip-routing/README.md b/examples/private-ip-routing/README.md index 1f419aedf..9dba91da6 100644 --- a/examples/private-ip-routing/README.md +++ b/examples/private-ip-routing/README.md @@ -37,7 +37,7 @@ docker push 127.0.0.1:5000/demo/private-api:latest Deploy the pod: ```bash -docker exec navigator-cluster-navigator \ +docker exec openshell-cluster-openshell \ kubectl run private-api \ --image=127.0.0.1:5000/demo/private-api:latest \ --port=8080 \ @@ -47,7 +47,7 @@ docker exec navigator-cluster-navigator \ Wait for it to be running and note the pod IP: ```bash -docker exec navigator-cluster-navigator kubectl get pod private-api -o wide +docker exec openshell-cluster-openshell kubectl get pod private-api -o wide ``` Example output: @@ -86,5 +86,5 @@ request returns `HTTP/1.1 403 Forbidden`. ## Cleanup ```bash -docker exec navigator-cluster-navigator kubectl delete pod private-api +docker exec openshell-cluster-openshell kubectl delete pod private-api ``` diff --git a/scripts/bin/openshell b/scripts/bin/openshell index 5d80aa198..f421766f2 100755 --- a/scripts/bin/openshell +++ b/scripts/bin/openshell @@ -88,7 +88,7 @@ else fi if [[ "$needs_build" == "1" ]]; then - echo "Recompiling navigator-cli..." >&2 + echo "Recompiling openshell..." >&2 cargo build --package navigator-cli --quiet # Persist state after successful build mkdir -p "$(dirname "$STATE_FILE")" diff --git a/scripts/remote-deploy.sh b/scripts/remote-deploy.sh index e8e68982e..eb23a72fc 100755 --- a/scripts/remote-deploy.sh +++ b/scripts/remote-deploy.sh @@ -252,8 +252,8 @@ mise exec -- tasks/scripts/docker-build-cluster.sh mise exec -- tasks/scripts/docker-build-component.sh server mise exec -- tasks/scripts/docker-build-component.sh sandbox -export OPENSHELL_CLUSTER_IMAGE="navigator/cluster:${IMAGE_TAG}" -export OPENSHELL_PUSH_IMAGES="navigator/server:${IMAGE_TAG},navigator/sandbox:${IMAGE_TAG}" +export OPENSHELL_CLUSTER_IMAGE="openshell/cluster:${IMAGE_TAG}" +export OPENSHELL_PUSH_IMAGES="openshell/server:${IMAGE_TAG},openshell/sandbox:${IMAGE_TAG}" start_args=( gateway diff --git a/tasks/docker.toml b/tasks/docker.toml index 32d09bb1b..76123a0d9 100644 --- a/tasks/docker.toml +++ b/tasks/docker.toml @@ -25,7 +25,7 @@ hide = true ["docker:build:sandbox:nvidia"] description = "Build the NVIDIA GPU sandbox Docker image" depends = ["docker:build:sandbox"] -run = "tasks/scripts/docker-build-component.sh sandbox nvidia --build-arg BASE_IMAGE=navigator/sandbox:${IMAGE_TAG:-dev}" +run = "tasks/scripts/docker-build-component.sh sandbox nvidia --build-arg BASE_IMAGE=openshell/sandbox:${IMAGE_TAG:-dev}" hide = true ["docker:build:server"] diff --git a/tasks/helm.toml b/tasks/helm.toml index 9f65c753a..65305af39 100644 --- a/tasks/helm.toml +++ b/tasks/helm.toml @@ -5,5 +5,5 @@ ["helm:lint"] description = "Lint the openshell helm chart" -run = "helm lint deploy/helm/navigator" +run = "helm lint deploy/helm/openshell" hide = true diff --git a/tasks/scripts/cluster-bootstrap.sh b/tasks/scripts/cluster-bootstrap.sh index ad1f67b2e..96c258ef0 100755 --- a/tasks/scripts/cluster-bootstrap.sh +++ b/tasks/scripts/cluster-bootstrap.sh @@ -5,6 +5,11 @@ set -euo pipefail +# Normalize cluster name: lowercase, replace invalid chars with hyphens +normalize_name() { + echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' +} + MODE=${1:-build} if [ "${MODE}" != "build" ] && [ "${MODE}" != "fast" ]; then echo "usage: $0 [build|fast]" >&2 @@ -24,9 +29,9 @@ LOCAL_REGISTRY_ADDR=127.0.0.1:5000 if [ -n "${CI:-}" ] && [ -n "${CI_REGISTRY_IMAGE:-}" ]; then IMAGE_REPO_BASE_DEFAULT=${CI_REGISTRY_IMAGE} elif [ "${MODE}" = "fast" ]; then - IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/navigator + IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/openshell else - IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/navigator + IMAGE_REPO_BASE_DEFAULT=${LOCAL_REGISTRY_ADDR}/openshell fi IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-${IMAGE_REPO_BASE_DEFAULT}}} @@ -34,7 +39,7 @@ REGISTRY_HOST=${OPENSHELL_REGISTRY_HOST:-${IMAGE_REPO_BASE%%/*}} REGISTRY_NAMESPACE_DEFAULT=${IMAGE_REPO_BASE#*/} if [ "${REGISTRY_NAMESPACE_DEFAULT}" = "${IMAGE_REPO_BASE}" ]; then - REGISTRY_NAMESPACE_DEFAULT=navigator + REGISTRY_NAMESPACE_DEFAULT=openshell fi has_env_key() { @@ -50,10 +55,12 @@ append_env_if_missing() { return fi if [ -f "${ENV_FILE}" ] && [ -s "${ENV_FILE}" ]; then - printf "\n%s=%s\n" "${key}" "${value}" >>"${ENV_FILE}" - else - printf "%s=%s\n" "${key}" "${value}" >>"${ENV_FILE}" + # Ensure file ends with newline before appending, but don't add extra blank line + if [ "$(tail -c1 "${ENV_FILE}" | wc -l)" -eq 0 ]; then + printf "\n" >>"${ENV_FILE}" + fi fi + printf "%s=%s\n" "${key}" "${value}" >>"${ENV_FILE}" } port_is_in_use() { @@ -90,6 +97,7 @@ pick_random_port() { } CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")} +CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}") if [ -n "${GATEWAY_PORT:-}" ]; then RESOLVED_GATEWAY_PORT=${GATEWAY_PORT} @@ -102,7 +110,6 @@ fi OPENSHELL_CLUSTER=${OPENSHELL_CLUSTER:-${CLUSTER_NAME}} GATEWAY_PORT=${RESOLVED_GATEWAY_PORT} -append_env_if_missing "CLUSTER_NAME" "${CLUSTER_NAME}" append_env_if_missing "GATEWAY_PORT" "${GATEWAY_PORT}" append_env_if_missing "OPENSHELL_CLUSTER" "${OPENSHELL_CLUSTER}" @@ -203,8 +210,8 @@ if is_local_registry_host; then ensure_local_registry fi -CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}" -VOLUME_NAME="navigator-cluster-${CLUSTER_NAME}" +CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}" +VOLUME_NAME="openshell-cluster-${CLUSTER_NAME}" if [ "${MODE}" = "fast" ]; then if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1 || docker volume inspect "${VOLUME_NAME}" >/dev/null 2>&1; then @@ -233,7 +240,7 @@ fi # `docker-build-cluster.sh` and contains the bundled Helm chart and # manifests from the current working tree. if [ -z "${OPENSHELL_CLUSTER_IMAGE:-}" ]; then - export OPENSHELL_CLUSTER_IMAGE="navigator/cluster:${IMAGE_TAG}" + export OPENSHELL_CLUSTER_IMAGE="openshell/cluster:${IMAGE_TAG}" fi DEPLOY_CMD=(openshell gateway start --name "${CLUSTER_NAME}" --port "${GATEWAY_PORT}" --update-kube-config) diff --git a/tasks/scripts/cluster-deploy-fast.sh b/tasks/scripts/cluster-deploy-fast.sh index 07f6c4ab8..61029805b 100755 --- a/tasks/scripts/cluster-deploy-fast.sh +++ b/tasks/scripts/cluster-deploy-fast.sh @@ -5,9 +5,15 @@ set -euo pipefail +# Normalize cluster name: lowercase, replace invalid chars with hyphens +normalize_name() { + echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' +} + CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")} -CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}" -IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/navigator}} +CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}") +CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}" +IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/openshell}} IMAGE_TAG=${IMAGE_TAG:-dev} RUST_BUILD_PROFILE=${RUST_BUILD_PROFILE:-debug} DEPLOY_FAST_MODE=${DEPLOY_FAST_MODE:-auto} @@ -169,7 +175,7 @@ matches_sandbox() { matches_helm() { local path=$1 case "${path}" in - deploy/helm/navigator/*) + deploy/helm/openshell/*) return 0 ;; *) @@ -196,7 +202,7 @@ compute_fingerprint() { committed_trees=$(git ls-tree HEAD Cargo.toml Cargo.lock proto/ deploy/docker/cross-build.sh crates/navigator-core/ crates/navigator-policy/ crates/navigator-providers/ crates/navigator-sandbox/ deploy/docker/sandbox/ python/ pyproject.toml uv.lock 2>/dev/null || true) ;; helm) - committed_trees=$(git ls-tree HEAD deploy/helm/navigator/ 2>/dev/null || true) + committed_trees=$(git ls-tree HEAD deploy/helm/openshell/ 2>/dev/null || true) ;; esac if [[ -n "${committed_trees}" ]]; then @@ -341,7 +347,7 @@ for component in server sandbox; do if [[ "${!var}" == "1" ]]; then # Tag may fail with AlreadyExists when the image digest hasn't changed; # this is harmless — the registry already has the correct image. - docker tag "navigator/${component}:${IMAGE_TAG}" "${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}" 2>/dev/null || true + docker tag "openshell/${component}:${IMAGE_TAG}" "${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}" 2>/dev/null || true pushed_images+=("${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}") built_components+=("${component}") fi @@ -381,21 +387,21 @@ if [[ "${needs_helm_upgrade}" == "1" ]]; then # sandbox callbacks to plaintext. # Retrieve the existing handshake secret from the running release, or generate # a new one if this is the first deploy with the mandatory secret. - EXISTING_SECRET=$(helm get values navigator -n navigator -o json 2>/dev/null \ + EXISTING_SECRET=$(helm get values openshell -n openshell -o json 2>/dev/null \ | grep -o '"sshHandshakeSecret":"[^"]*"' \ | cut -d'"' -f4) || true SSH_HANDSHAKE_SECRET="${EXISTING_SECRET:-$(openssl rand -hex 32)}" - helm upgrade navigator deploy/helm/navigator \ - --namespace navigator \ + helm upgrade openshell deploy/helm/openshell \ + --namespace openshell \ --set image.repository=${IMAGE_REPO_BASE}/server \ --set image.tag=${IMAGE_TAG} \ --set image.pullPolicy=Always \ - --set-string server.grpcEndpoint=https://navigator.navigator.svc.cluster.local:8080 \ + --set-string server.grpcEndpoint=https://openshell.openshell.svc.cluster.local:8080 \ --set server.sandboxImage=${IMAGE_REPO_BASE}/sandbox:${IMAGE_TAG} \ - --set server.tls.certSecretName=navigator-server-tls \ - --set server.tls.clientCaSecretName=navigator-server-client-ca \ - --set server.tls.clientTlsSecretName=navigator-client-tls \ + --set server.tls.certSecretName=openshell-server-tls \ + --set server.tls.clientCaSecretName=openshell-server-client-ca \ + --set server.tls.clientTlsSecretName=openshell-client-tls \ --set server.sshHandshakeSecret=${SSH_HANDSHAKE_SECRET} \ "${helm_wait_args[@]}" helm_end=$(date +%s) @@ -405,14 +411,14 @@ fi if [[ "${#pushed_images[@]}" -gt 0 ]]; then rollout_start=$(date +%s) echo "Restarting deployment to pick up updated images..." - if kubectl get statefulset/navigator -n navigator >/dev/null 2>&1; then - kubectl rollout restart statefulset/navigator -n navigator - kubectl rollout status statefulset/navigator -n navigator - elif kubectl get deployment/navigator -n navigator >/dev/null 2>&1; then - kubectl rollout restart deployment/navigator -n navigator - kubectl rollout status deployment/navigator -n navigator + if kubectl get statefulset/openshell -n openshell >/dev/null 2>&1; then + kubectl rollout restart statefulset/openshell -n openshell + kubectl rollout status statefulset/openshell -n openshell + elif kubectl get deployment/openshell -n openshell >/dev/null 2>&1; then + kubectl rollout restart deployment/openshell -n openshell + kubectl rollout status deployment/openshell -n openshell else - echo "Warning: no navigator workload found to roll out in namespace 'navigator'." + echo "Warning: no openshell workload found to roll out in namespace 'openshell'." fi rollout_end=$(date +%s) log_duration "Rollout" "${rollout_start}" "${rollout_end}" diff --git a/tasks/scripts/cluster-push-component.sh b/tasks/scripts/cluster-push-component.sh index c8e48da7f..38ecf9fa4 100755 --- a/tasks/scripts/cluster-push-component.sh +++ b/tasks/scripts/cluster-push-component.sh @@ -20,17 +20,23 @@ case "${component}" in ;; esac +# Normalize cluster name: lowercase, replace invalid chars with hyphens +normalize_name() { + echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' +} + IMAGE_TAG=${IMAGE_TAG:-dev} -IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/navigator}} +IMAGE_REPO_BASE=${IMAGE_REPO_BASE:-${OPENSHELL_REGISTRY:-127.0.0.1:5000/openshell}} CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")} -CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}" -SOURCE_IMAGE="navigator/${component}:${IMAGE_TAG}" +CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}") +CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}" +SOURCE_IMAGE="openshell/${component}:${IMAGE_TAG}" TARGET_IMAGE="${IMAGE_REPO_BASE}/${component}:${IMAGE_TAG}" source_candidates=( - "navigator/${component}:${IMAGE_TAG}" - "localhost:5000/navigator/${component}:${IMAGE_TAG}" - "127.0.0.1:5000/navigator/${component}:${IMAGE_TAG}" + "openshell/${component}:${IMAGE_TAG}" + "localhost:5000/openshell/${component}:${IMAGE_TAG}" + "127.0.0.1:5000/openshell/${component}:${IMAGE_TAG}" ) resolved_source_image="" @@ -42,15 +48,9 @@ for candidate in "${source_candidates[@]}"; do done if [ -z "${resolved_source_image}" ]; then - echo "missing local image for ${component}:${IMAGE_TAG}" >&2 - echo "checked candidates:" >&2 - for candidate in "${source_candidates[@]}"; do - echo " ${candidate}" >&2 - done - echo "build it first with either:" >&2 - echo " mise run docker:build:${component}" >&2 - echo " mise run cluster" >&2 - exit 1 + echo "Local image not found for ${component}:${IMAGE_TAG}, building..." + tasks/scripts/docker-build-component.sh "${component}" + resolved_source_image="openshell/${component}:${IMAGE_TAG}" fi docker tag "${resolved_source_image}" "${TARGET_IMAGE}" diff --git a/tasks/scripts/cluster.sh b/tasks/scripts/cluster.sh index 90273c66f..6dc6bf3d4 100755 --- a/tasks/scripts/cluster.sh +++ b/tasks/scripts/cluster.sh @@ -8,8 +8,14 @@ set -euo pipefail +# Normalize cluster name: lowercase, replace invalid chars with hyphens +normalize_name() { + echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' +} + CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")} -CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}" +CLUSTER_NAME=$(normalize_name "${CLUSTER_NAME}") +CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}" if ! docker ps -q --filter "name=${CONTAINER_NAME}" | grep -q .; then echo "No running cluster found. Bootstrapping..." diff --git a/tasks/scripts/docker-build-cluster.sh b/tasks/scripts/docker-build-cluster.sh index 24c626afe..9f52f54c4 100755 --- a/tasks/scripts/docker-build-cluster.sh +++ b/tasks/scripts/docker-build-cluster.sh @@ -16,7 +16,7 @@ set -euo pipefail IMAGE_TAG=${IMAGE_TAG:-dev} -IMAGE_NAME="navigator/cluster" +IMAGE_NAME="openshell/cluster" if [[ -n "${IMAGE_REGISTRY:-}" ]]; then IMAGE_NAME="${IMAGE_REGISTRY}/cluster" fi @@ -49,9 +49,9 @@ fi # Create build directory for charts mkdir -p deploy/docker/.build/charts -# Package navigator helm chart -echo "Packaging navigator helm chart..." -helm package deploy/helm/navigator -d deploy/docker/.build/charts/ +# Package helm chart +echo "Packaging helm chart..." +helm package deploy/helm/openshell -d deploy/docker/.build/charts/ # Build cluster image (no bundled component images — they are pulled at runtime # from the distribution registry; credentials are injected at deploy time) diff --git a/tasks/scripts/docker-build-component.sh b/tasks/scripts/docker-build-component.sh index eefea9c83..3156614ab 100755 --- a/tasks/scripts/docker-build-component.sh +++ b/tasks/scripts/docker-build-component.sh @@ -8,11 +8,11 @@ # # Components with a subdirectory layout (e.g. deploy/docker/sandbox/) support # an optional variant argument: -# docker-build-component.sh sandbox -> Dockerfile.base -> navigator/sandbox:dev -# docker-build-component.sh sandbox nvidia -> Dockerfile.nvidia -> navigator/sandbox-nvidia:dev +# docker-build-component.sh sandbox -> Dockerfile.base -> openshell/sandbox:dev +# docker-build-component.sh sandbox nvidia -> Dockerfile.nvidia -> openshell/sandbox-nvidia:dev # # Components without a subdirectory use the flat layout: -# docker-build-component.sh server -> Dockerfile.server -> navigator/server:dev +# docker-build-component.sh server -> Dockerfile.server -> openshell/server:dev # # Environment: # IMAGE_TAG - Image tag (default: dev) @@ -72,14 +72,14 @@ if [[ -d "${COMPONENT_DIR}" ]]; then VARIANT=${VARIANT:-base} DOCKERFILE="${COMPONENT_DIR}/Dockerfile.${VARIANT}" if [[ "${VARIANT}" == "base" ]]; then - IMAGE_NAME="navigator/${COMPONENT}" + IMAGE_NAME="openshell/${COMPONENT}" else - IMAGE_NAME="navigator/${COMPONENT}-${VARIANT}" + IMAGE_NAME="openshell/${COMPONENT}-${VARIANT}" fi else # Flat layout: deploy/docker/Dockerfile. DOCKERFILE="deploy/docker/Dockerfile.${COMPONENT}" - IMAGE_NAME="navigator/${COMPONENT}" + IMAGE_NAME="openshell/${COMPONENT}" fi if [[ ! -f "${DOCKERFILE}" ]]; then @@ -88,9 +88,9 @@ if [[ ! -f "${DOCKERFILE}" ]]; then fi # Prefix with registry when set (e.g. ghcr.io/org/repo/server:tag). -# Replaces the default "navigator/" prefix with the registry path. +# Replaces the default "openshell/" prefix with the registry path. if [[ -n "${IMAGE_REGISTRY:-}" ]]; then - _suffix="${IMAGE_NAME#navigator/}" + _suffix="${IMAGE_NAME#openshell/}" IMAGE_NAME="${IMAGE_REGISTRY}/${_suffix}" fi diff --git a/tasks/scripts/docker-publish-multiarch.sh b/tasks/scripts/docker-publish-multiarch.sh index 1c5d4ba01..918f16445 100755 --- a/tasks/scripts/docker-publish-multiarch.sh +++ b/tasks/scripts/docker-publish-multiarch.sh @@ -104,13 +104,13 @@ fi case "$MODE" in registry) REGISTRY=${DOCKER_REGISTRY:?Set DOCKER_REGISTRY to push multi-arch images (e.g. ghcr.io/myorg)} - IMAGE_PREFIX="navigator-" + IMAGE_PREFIX="openshell-" ;; ecr) AWS_ACCOUNT_ID=${AWS_ACCOUNT_ID:-524473328983} AWS_REGION=${AWS_REGION:-us-west-2} ECR_HOST="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" - REGISTRY="${ECR_HOST}/navigator" + REGISTRY="${ECR_HOST}/openshell" IMAGE_PREFIX="" EXTRA_BUILD_FLAGS="--provenance=false --sbom=false" ;; @@ -187,7 +187,7 @@ done # Step 2: Package helm charts (architecture-independent) # --------------------------------------------------------------------------- mkdir -p deploy/docker/.build/charts -echo "Packaging navigator helm chart..." +echo "Packaging helm chart..." helm package deploy/helm/navigator -d deploy/docker/.build/charts/ # --------------------------------------------------------------------------- diff --git a/tasks/scripts/sandbox.sh b/tasks/scripts/sandbox.sh index 4cf9bbca9..bf7b3cc63 100755 --- a/tasks/scripts/sandbox.sh +++ b/tasks/scripts/sandbox.sh @@ -15,7 +15,7 @@ set -euo pipefail SANDBOX_NAME="dev" CLUSTER_NAME=${CLUSTER_NAME:-$(basename "$PWD")} -CONTAINER_NAME="navigator-cluster-${CLUSTER_NAME}" +CONTAINER_NAME="openshell-cluster-${CLUSTER_NAME}" STATE_DIR=${SANDBOX_STATE_DIR:-.cache} SANDBOX_STATE_FILE=${STATE_DIR}/sandbox-dev.state DEPLOY_STATE_FILE=${DEPLOY_FAST_STATE_FILE:-${STATE_DIR}/cluster-deploy-fast.state}