mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-30 07:33:59 +08:00
467 lines
26 KiB
Markdown
467 lines
26 KiB
Markdown
# Cluster Bootstrap Architecture
|
|
|
|
This document describes how NemoClaw bootstraps a single-node k3s cluster 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.
|
|
- Keep Docker as the only runtime dependency for provisioning and lifecycle operations.
|
|
- Package the NemoClaw cluster 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.
|
|
|
|
Out of scope:
|
|
|
|
- Multi-node orchestration.
|
|
|
|
## Components
|
|
|
|
- `crates/navigator-cli/src/main.rs`: CLI entry point; `clap`-based command parsing.
|
|
- `crates/navigator-cli/src/run.rs`: CLI command implementations (`cluster_admin_deploy`, `cluster_admin_stop`, `cluster_admin_destroy`, `cluster_admin_info`, `cluster_admin_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-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/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.
|
|
- `crates/navigator-bootstrap/src/constants.rs`: Shared constants (image name, network name, container/volume naming).
|
|
- `deploy/docker/Dockerfile.cluster`: Container image definition (k3s base + Helm charts + manifests + entrypoint).
|
|
- `deploy/docker/cluster-entrypoint.sh`: Container entrypoint (DNS proxy, registry config, manifest injection).
|
|
- `deploy/docker/cluster-healthcheck.sh`: Docker HEALTHCHECK script.
|
|
- Docker daemon(s):
|
|
- Local daemon for local deploys.
|
|
- Remote daemon over SSH for remote deploy container operations.
|
|
|
|
## CLI Commands
|
|
|
|
All cluster lifecycle commands live under `ncl cluster admin`:
|
|
|
|
| Command | Description |
|
|
|---|---|
|
|
| `ncl cluster admin deploy [--name NAME] [--remote user@host] [--ssh-key PATH]` | Provision or update a cluster |
|
|
| `ncl cluster admin stop [--name NAME] [--remote user@host]` | Stop the container (preserves state) |
|
|
| `ncl cluster admin destroy [--name NAME] [--remote user@host]` | Destroy container, attached volumes, kubeconfig directory, metadata, and network |
|
|
| `ncl cluster admin info [--name NAME]` | Show deployment details (endpoint, kubeconfig path, SSH host) |
|
|
| `ncl cluster admin tunnel [--name NAME] [--remote user@host] [--print-command]` | Start or print SSH tunnel for kubectl access |
|
|
| `ncl cluster status` | Show gateway health via gRPC/HTTP |
|
|
| `ncl cluster use <name>` | Set the active cluster |
|
|
| `ncl cluster list` | List all clusters with metadata |
|
|
|
|
The `--name` flag defaults to `"nemoclaw"`. When omitted on commands that accept it, the CLI resolves the active cluster via: `--cluster` flag, then `NEMOCLAW_CLUSTER` env, then `~/.config/nemoclaw/active_cluster` file.
|
|
|
|
## Local Task Flows (`mise`)
|
|
|
|
Development task entrypoints split bootstrap behavior:
|
|
|
|
| Task | Behavior |
|
|
|---|---|
|
|
| `mise run cluster` | Fast recreate path: destroys existing local cluster resources for `CLUSTER_NAME` (if present), removes conflicting local NemoClaw clusters that occupy host port `6443`, pushes prebuilt local component images to the local registry, and deploys using local-registry image refs (`127.0.0.1:5000/navigator/*`) |
|
|
| `mise run cluster:build` | Full build path: builds cluster/server/sandbox images, pushes components, then deploys (preferred in CI) |
|
|
| `mise run cluster:deploy` | Iterative deploy path: 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 `NEMOCLAW_CLUSTER`. 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 `cluster` and `cluster:deploy` can push/pull local component images consistently.
|
|
|
|
## Bootstrap Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant U as User
|
|
participant C as navigator-cli
|
|
participant B as navigator-bootstrap
|
|
participant L as Local Docker daemon
|
|
participant R as Remote Docker daemon (SSH)
|
|
|
|
U->>C: ncl cluster admin deploy --remote user@host
|
|
C->>B: deploy_cluster(DeployOptions)
|
|
|
|
B->>B: create_ssh_docker_client (ssh://, 600s timeout)
|
|
B->>R: pull_remote_image (registry auth, platform-aware)
|
|
B->>R: tag image to local ref
|
|
|
|
Note over B,R: Docker socket APIs only, no extra host dependencies
|
|
|
|
B->>B: resolve SSH host for extra TLS SANs
|
|
B->>R: ensure_network (bridge, attachable)
|
|
B->>R: ensure_volume
|
|
B->>R: ensure_container (privileged, k3s server)
|
|
B->>R: start_container
|
|
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: 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)
|
|
```
|
|
|
|
## End-State Connectivity Diagram
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
subgraph WS[User workstation]
|
|
NAV[navigator-cli]
|
|
KUBECTL[kubectl]
|
|
KC[stored kubeconfig ~/.config/nemoclaw/clusters/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]
|
|
KAPI[Kubernetes API :6443]
|
|
G8080[Gateway :port -> :30051 mTLS default 8080]
|
|
SBX[Sandbox runtime]
|
|
end
|
|
|
|
KC --> KUBECTL
|
|
KUBECTL --> KAPI
|
|
TUN -. remote mode .-> KAPI
|
|
|
|
NAV --> G8080
|
|
MTLS -. client cert auth .-> G8080
|
|
|
|
DOCKER --> K3S
|
|
K3S --> KAPI
|
|
K3S --> G8080
|
|
K3S --> G80
|
|
K3S --> G443
|
|
```
|
|
|
|
## Deploy Flow
|
|
|
|
### 1) Entry and client selection
|
|
|
|
`deploy_cluster(DeployOptions)` in `crates/navigator-bootstrap/src/lib.rs` chooses execution mode:
|
|
|
|
- `DeployOptions` fields: `name: String`, `image_ref: Option<String>`, `remote: Option<RemoteOptions>`, `port: u16` (default 8080).
|
|
- `RemoteOptions` fields: `destination: String`, `ssh_key: Option<String>`.
|
|
- **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.
|
|
|
|
**Pre-deploy check** (CLI layer in `cluster_admin_deploy`): 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.
|
|
|
|
### 2) Image readiness
|
|
|
|
Image ref resolution in `default_cluster_image_ref()`:
|
|
|
|
1. If `NEMOCLAW_CLUSTER_IMAGE` is set and non-empty, use it verbatim.
|
|
2. Otherwise, use the published distribution image base (`<distribution-registry>/navigator/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).
|
|
|
|
### 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()`.
|
|
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()`:
|
|
- 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`.
|
|
- Extra host: `host.docker.internal:host-gateway`.
|
|
- Port mappings:
|
|
|
|
| Container Port | Host Port | Purpose |
|
|
|---|---|---|
|
|
| 6443/tcp | 6443 | Kubernetes API |
|
|
| 30051/tcp | configurable (default 8080) | NemoClaw service NodePort (mTLS) |
|
|
|
|
- Container environment variables (see [Container Environment Variables](#container-environment-variables) below).
|
|
- If the container exists with a different image ID (compared by inspecting the content-addressable ID), it is stopped, force-removed, and recreated. If the image matches, the existing container is reused.
|
|
5. **Start container** via `start_container()`. Tolerates already-running 409 conflict.
|
|
|
|
### 4) Readiness and artifact extraction
|
|
|
|
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/nemoclaw/clusters/{name}/kubeconfig` (or `$XDG_CONFIG_HOME/nemoclaw/clusters/{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 `NEMOCLAW_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:
|
|
- 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.
|
|
|
|
### 5) mTLS bundle capture
|
|
|
|
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/nemoclaw/clusters/{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:
|
|
|
|
- **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.
|
|
|
|
Metadata fields:
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `name` | `String` | Cluster name |
|
|
| `gateway_endpoint` | `String` | HTTPS endpoint with port (e.g., `https://127.0.0.1:8080`) |
|
|
| `is_remote` | `bool` | Whether cluster is remote |
|
|
| `gateway_port` | `u16` | Host port mapped to the gateway NodePort |
|
|
| `remote_host` | `Option<String>` | SSH destination (e.g., `user@host`) |
|
|
| `resolved_host` | `Option<String>` | Resolved hostname/IP from `ssh -G` |
|
|
|
|
Metadata location: `~/.config/nemoclaw/clusters/{name}_metadata.json`
|
|
|
|
Note: metadata is stored at the `clusters/` level (not nested inside `{name}/` like kubeconfig and mTLS).
|
|
|
|
After deploy, the CLI calls `save_active_cluster(name)`, writing the cluster name to `~/.config/nemoclaw/active_cluster`. Subsequent commands that don't specify `--cluster` or `NEMOCLAW_CLUSTER` resolve to this active cluster.
|
|
|
|
## Container Image
|
|
|
|
The cluster image is defined in `deploy/docker/Dockerfile.cluster`:
|
|
|
|
```
|
|
Base: rancher/k3s:v1.29.8-k3s1
|
|
```
|
|
|
|
Layers added:
|
|
|
|
1. Custom entrypoint: `deploy/docker/cluster-entrypoint.sh` -> `/usr/local/bin/cluster-entrypoint.sh`
|
|
2. Healthcheck script: `deploy/docker/cluster-healthcheck.sh` -> `/usr/local/bin/cluster-healthcheck.sh`
|
|
3. Packaged Helm charts: `deploy/docker/.build/charts/*.tgz` -> `/var/lib/rancher/k3s/server/static/charts/`
|
|
4. Kubernetes manifests: `deploy/kube/manifests/*.yaml` -> `/opt/navigator/manifests/`
|
|
|
|
Bundled manifests include:
|
|
- `navigator-helmchart.yaml` (NemoClaw Helm chart auto-deploy)
|
|
- `envoy-gateway-helmchart.yaml` (Envoy Gateway for Gateway API)
|
|
- `agent-sandbox.yaml`
|
|
|
|
The HEALTHCHECK is configured as: `--interval=5s --timeout=5s --start-period=20s --retries=60`.
|
|
|
|
## Entrypoint Script
|
|
|
|
`deploy/docker/cluster-entrypoint.sh` runs before k3s starts. It performs:
|
|
|
|
### DNS proxy setup
|
|
|
|
On Docker custom networks, `/etc/resolv.conf` contains `127.0.0.11` (Docker's internal DNS). k3s detects this loopback and falls back to `8.8.8.8`, which does not work on Docker Desktop. The entrypoint solves this by:
|
|
|
|
1. Discovering Docker's real DNS listener ports from the `DOCKER_OUTPUT` iptables chain.
|
|
2. Getting the container's `eth0` IP as a routable address.
|
|
3. Adding DNAT rules in PREROUTING to forward DNS from pod namespaces through to Docker's DNS.
|
|
4. Writing a custom resolv.conf pointing to the container IP.
|
|
5. Passing `--resolv-conf=/etc/rancher/k3s/resolv.conf` to k3s.
|
|
|
|
Falls back to `8.8.8.8` / `8.8.4.4` if iptables detection fails.
|
|
|
|
### Registry configuration
|
|
|
|
Writes `/etc/rancher/k3s/registries.yaml` from `REGISTRY_HOST`, `REGISTRY_ENDPOINT`, `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `REGISTRY_INSECURE` environment variables so that k3s/containerd can authenticate when pulling component images at runtime.
|
|
|
|
### Manifest injection
|
|
|
|
Copies bundled manifests from `/opt/navigator/manifests/` to `/var/lib/rancher/k3s/server/manifests/`. This is needed because the volume mount on `/var/lib/rancher/k3s` overwrites any files baked into that path at image build time.
|
|
|
|
### Image configuration overrides
|
|
|
|
When environment variables are set, the entrypoint modifies the HelmChart manifest at `/var/lib/rancher/k3s/server/manifests/navigator-helmchart.yaml`:
|
|
|
|
- `IMAGE_REPO_BASE`: Rewrites `repository:`, `sandboxImage:`, and `jobImage:` in the HelmChart.
|
|
- `PUSH_IMAGE_REFS`: In push mode, parses comma-separated image refs and rewrites the exact gateway, sandbox, and pki-job image references (matching on path component `/server:`, `/sandbox:`, `/pki-job:`).
|
|
- `IMAGE_TAG`: Replaces `:latest` tags with the specified tag on gateway, sandbox, and pki-job images. Handles both quoted and unquoted `tag: latest` formats.
|
|
- `IMAGE_PULL_POLICY`: Replaces `pullPolicy: Always` with the specified policy (e.g., `IfNotPresent`).
|
|
- `SSH_GATEWAY_HOST` / `SSH_GATEWAY_PORT`: Replaces `__SSH_GATEWAY_HOST__` and `__SSH_GATEWAY_PORT__` placeholders.
|
|
- `EXTRA_SANS`: Builds a YAML flow-style list from the comma-separated SANs and replaces `extraSANs: []`.
|
|
|
|
## Healthcheck Script
|
|
|
|
`deploy/docker/cluster-healthcheck.sh` validates cluster readiness through a series of checks:
|
|
|
|
1. **Kubernetes API**: `kubectl get --raw='/readyz'`
|
|
2. **NemoClaw StatefulSet**: Checks that `statefulset/navigator` in namespace `navigator` exists and has 1 ready replica.
|
|
3. **Gateway**: Checks that `gateway/navigator-gateway` in namespace `navigator` has the `Programmed` condition.
|
|
4. **mTLS secret** (conditional): If `NAV_GATEWAY_TLS_ENABLED` is true (or inferred from the HelmChart manifest using the same two-path detection logic as the bootstrap code), checks that secret `navigator-cli-client` exists with non-empty `ca.crt`, `tls.crt`, and `tls.key` data.
|
|
|
|
## Remote Image Transfer
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
B[bootstrap] -->|query| RP[Remote platform: Docker version API]
|
|
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]
|
|
```
|
|
|
|
- Remote platform is queried via `Docker::version()` and normalized (e.g., `x86_64` -> `amd64`, `aarch64` -> `arm64`).
|
|
- Distribution registry credentials are XOR-encoded in the binary (lightweight obfuscation, not a security boundary).
|
|
- If the image ref looks local (no `/` in repository), the `latest` tag is used from the distribution registry regardless of the local `IMAGE_TAG`.
|
|
|
|
## Access Model
|
|
|
|
### Kubernetes API access
|
|
|
|
- Kubeconfig always targets `https://127.0.0.1:6443`.
|
|
- For remote clusters, the user must open an SSH local-forward tunnel:
|
|
|
|
```bash
|
|
ssh -L 6443:127.0.0.1:6443 -N user@host
|
|
```
|
|
|
|
CLI helper:
|
|
|
|
```bash
|
|
ncl cluster admin tunnel --name <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.
|
|
|
|
### Gateway endpoint exposure
|
|
|
|
- Local: `https://127.0.0.1:{port}` (or `https://{docker_host}:{port}` when `DOCKER_HOST` is a non-loopback TCP endpoint). Default port is 8080.
|
|
- Remote: `https://<resolved-remote-host>:{port}`.
|
|
- The host port (configurable via `--port`, default 8080) maps to container port 30051 (NemoClaw service NodePort).
|
|
|
|
## Lifecycle Operations
|
|
|
|
### stop
|
|
|
|
`ClusterHandle::stop()` calls `stop_container()`, which tolerates 404 (not found) and 409 (already stopped).
|
|
|
|
### destroy
|
|
|
|
**Bootstrap layer** (`ClusterHandle::destroy()` -> `destroy_cluster_resources()`):
|
|
|
|
1. Stop the container.
|
|
2. Remove the container (`force=true`). Tolerates 404.
|
|
3. Remove the volume (`force=true`). Tolerates 404.
|
|
4. Remove the stored kubeconfig file.
|
|
5. Remove the network if no containers remain attached (`cleanup_network_if_unused()`).
|
|
|
|
**CLI layer** (`cluster_admin_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.
|
|
|
|
## Idempotency and Error Behavior
|
|
|
|
- Re-running deploy is safe:
|
|
- 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.
|
|
- Error handling surfaces:
|
|
- Docker API failures from inspect/create/start/remove.
|
|
- SSH connection failures when creating the remote Docker client.
|
|
- Kubeconfig readiness timeout (60s) with recent container logs in the error message.
|
|
- Health check timeout (6 min) with recent container logs.
|
|
- Container exit during any polling phase (kubeconfig, health, mTLS) with diagnostic information (exit code, OOM status, recent logs).
|
|
- mTLS secret polling timeout (3 min).
|
|
- Local image ref without registry prefix: clear error with build instructions rather than a failed Docker Hub pull.
|
|
|
|
## Auto-Bootstrap from `sandbox create`
|
|
|
|
When `ncl sandbox create` cannot connect to a cluster (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 `"nemoclaw"`, sets it as active, and returns fresh `TlsOptions` pointing to the newly-written mTLS certs.
|
|
|
|
## Container Environment Variables
|
|
|
|
Variables set on the container by `ensure_container()` in `docker.rs`:
|
|
|
|
| Variable | Value | When Set |
|
|
|---|---|---|
|
|
| `REGISTRY_MODE` | `"external"` | Always |
|
|
| `REGISTRY_HOST` | Distribution registry host (or `NEMOCLAW_REGISTRY_HOST` override) | Always |
|
|
| `REGISTRY_INSECURE` | `"true"` or `"false"` | Always |
|
|
| `IMAGE_REPO_BASE` | `{registry_host}/{namespace}` (or `IMAGE_REPO_BASE`/`NEMOCLAW_IMAGE_REPO_BASE` override) | Always |
|
|
| `REGISTRY_ENDPOINT` | Custom endpoint URL | When `NEMOCLAW_REGISTRY_ENDPOINT` is set |
|
|
| `REGISTRY_USERNAME` | Registry auth username | When credentials available |
|
|
| `REGISTRY_PASSWORD` | Registry auth password | When credentials available |
|
|
| `EXTRA_SANS` | Comma-separated extra TLS SANs | When extra SANs computed |
|
|
| `SSH_GATEWAY_HOST` | Resolved remote hostname/IP | Remote deploys only |
|
|
| `SSH_GATEWAY_PORT` | Configured host port (default `8080`) | Remote deploys only |
|
|
| `IMAGE_TAG` | Image tag (e.g., `"dev"`) | When `IMAGE_TAG` env is set or push mode |
|
|
| `IMAGE_PULL_POLICY` | `"IfNotPresent"` | Push mode only |
|
|
| `PUSH_IMAGE_REFS` | Comma-separated image refs | Push mode only |
|
|
|
|
## Host-Side Environment Variables
|
|
|
|
Environment variables that affect bootstrap behavior when set on the host:
|
|
|
|
| Variable | Effect |
|
|
|---|---|
|
|
| `NEMOCLAW_CLUSTER_IMAGE` | Overrides entire image ref if set and non-empty |
|
|
| `IMAGE_TAG` | Sets image tag (default: `"dev"`) when `NEMOCLAW_CLUSTER_IMAGE` is not set |
|
|
| `NAV_GATEWAY_TLS_ENABLED` | Overrides HelmChart manifest for TLS enabled check (`true`/`1`/`yes`/`false`/`0`/`no`) |
|
|
| `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 |
|
|
| `NEMOCLAW_PUSH_IMAGES` | Comma-separated image refs to push into the cluster's containerd (local deploy only) |
|
|
| `NEMOCLAW_REGISTRY_HOST` | Override the distribution registry host |
|
|
| `NEMOCLAW_REGISTRY_NAMESPACE` | Override the registry namespace (default: `"navigator"`) |
|
|
| `IMAGE_REPO_BASE` / `NEMOCLAW_IMAGE_REPO_BASE` | Override the image repository base path |
|
|
| `NEMOCLAW_REGISTRY_INSECURE` | Use HTTP instead of HTTPS for registry mirror |
|
|
| `NEMOCLAW_REGISTRY_ENDPOINT` | Custom registry mirror endpoint |
|
|
| `NEMOCLAW_REGISTRY_USERNAME` | Override registry auth username |
|
|
| `NEMOCLAW_REGISTRY_PASSWORD` | Override registry auth password |
|
|
| `NEMOCLAW_CLUSTER` | Set the active cluster name for CLI commands |
|
|
|
|
## File System Layout
|
|
|
|
Artifacts stored under `$XDG_CONFIG_HOME/nemoclaw/` (default `~/.config/nemoclaw/`):
|
|
|
|
```
|
|
nemoclaw/
|
|
active_cluster # plain text: active cluster name
|
|
clusters/
|
|
{name}_metadata.json # ClusterMetadata JSON
|
|
{name}/
|
|
kubeconfig # rewritten kubeconfig
|
|
mtls/ # mTLS bundle (when TLS enabled)
|
|
ca.crt
|
|
tls.crt
|
|
tls.key
|
|
```
|
|
|
|
## Implementation References
|
|
|
|
- `crates/navigator-bootstrap/src/lib.rs` -- public API, deploy orchestration
|
|
- `crates/navigator-bootstrap/src/docker.rs` -- Docker API wrappers
|
|
- `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/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
|
|
- `crates/navigator-bootstrap/src/paths.rs` -- XDG path helpers
|
|
- `crates/navigator-cli/src/main.rs` -- CLI command definitions
|
|
- `crates/navigator-cli/src/run.rs` -- CLI command implementations
|
|
- `crates/navigator-cli/src/bootstrap.rs` -- auto-bootstrap from sandbox create
|
|
- `deploy/docker/Dockerfile.cluster` -- container image definition
|
|
- `deploy/docker/cluster-entrypoint.sh` -- container entrypoint script
|
|
- `deploy/docker/cluster-healthcheck.sh` -- Docker HEALTHCHECK script
|
|
- `deploy/kube/manifests/navigator-helmchart.yaml` -- NemoClaw Helm chart manifest
|
|
- `deploy/kube/manifests/envoy-gateway-helmchart.yaml` -- Envoy Gateway manifest
|