# Provider: kubectl (Kubernetes / k3s)
Use this provider when your Dune server runs inside a Kubernetes cluster (e.g. k3s on a VM) and dune-admin runs on your local machine or another host with SSH access to that VM.
All commands run over SSH — no exposed ports, no VPN.
```
your machine
└─ SSH tunnel → VM
├─ kubectl → battlegroup CRDs / pod logs
└─ TCP tunnel → PostgreSQL (pod IP:15432)
```
## Prerequisites
| Requirement | Notes |
|-------------|-------|
| **Go 1.21+** | `brew install go` or |
| **SSH key** | Private key authorised on the VM |
| **VM access** | Port 22 reachable; SSH user needs passwordless `sudo kubectl` |
## Quick start (wizard)
```bash
# Place SSH key (checked automatically in this order):
# ./sshKey │ ~/.dune-admin/sshKey │ ~/.ssh/dune │ ~/.ssh/id_ed25519 │ ~/.ssh/id_rsa
cp /path/to/key ./sshKey && chmod 600 ./sshKey
make setup # prompts for VM host:port, discovers namespace + DB password automatically
make build # builds frontend + dune-admin binary
./dune-admin
```
The wizard:
1. Locates your SSH key
2. SSHes into the VM
3. Runs `kubectl get pods -A` to find the database pod and namespace
4. Reads `~/.dune/.yaml` on the VM for DB credentials
5. Writes `~/.dune-admin/config.yaml`
## External VM deploy example (`dune@192.168.0.72`)
```bash
cd /Volumes/Engineering/Icehunter/dune-admin
# Pull kubeconfig from VM and point kubectl at the external cluster.
mkdir -p ~/.kube
ssh dune@192.168.0.72 "sudo cat /etc/rancher/k3s/k3s.yaml" > ~/.kube/dune-external.yaml
sed -i '' 's/127.0.0.1/192.168.0.72/g' ~/.kube/dune-external.yaml
export KUBECONFIG=~/.kube/dune-external.yaml
kubectl get nodes
# Configure dune-admin runtime values.
make setup
# Ensure these are set in ~/.dune-admin/config.yaml for container runtime:
# market_bot_enabled: true
# market_bot_item_data: /app/item-data.json
# market_bot_cache_db: /data/market-bot-cache.db
# Build local image and import it into k3s runtime on the VM.
docker buildx build --platform linux/amd64 -f deploy/Dockerfile -t dune-admin:local --load .
docker save dune-admin:local | ssh dune@192.168.0.72 "sudo k3s ctr images import -"
# Render deployment manifest from ~/.dune-admin/config.yaml and switch image tag.
make render-k8s
sed -i '' 's#ghcr.io/icehunter/dune-admin:latest#dune-admin:local#g' deploy/k8s/dune-admin.rendered.yaml
# Deploy and wait for readiness.
kubectl apply -f deploy/k8s/dune-admin.rendered.yaml
kubectl -n dune-admin rollout status deploy/dune-admin
kubectl -n dune-admin get pods,svc
# Verify API and bot from inside the cluster.
kubectl -n dune-admin run curl --rm -it --restart=Never --image=curlimages/curl -- \
sh -c "curl -s http://dune-admin:8080/api/v1/market-bot/status"
# Local access from your laptop.
kubectl -n dune-admin port-forward svc/dune-admin 8080:8080
```
## Scripted deploy (Linux/macOS + Windows)
From repo root:
```bash
./deploy.sh
```
```powershell
./deploy.ps1
```
> On Windows, if script execution is blocked, run once:
> `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`
Both scripts run the full k8s flow:
1. Pull kubeconfig from VM (`dune@192.168.0.72` by default) unless skipped
2. Build a fresh timestamped image tag (`dune-admin:local-` by default)
3. Import image into VM k3s runtime (`k3s ctr images import`)
4. Render and apply `deploy/k8s/dune-admin.rendered.yaml`
5. Auto-fix embedded bot deployment settings in the rendered manifest:
- `MARKET_BOT_ENABLED: "true"`
- `market_bot_enabled: true`
- `market_bot_item_data: /app/item-data.json`
- `market_bot_cache_db: /data/market-bot-cache.db`
6. Restart rollout, wait for old terminating pods to drain, then run in-cluster health checks for:
- `/api/v1/status` reachable
- `/` returns HTTP 200 (no UI 404)
- `/api/v1/market-bot/status` reports `"enabled":true`
7. Open `kubectl port-forward` on `127.0.0.1:8080` (unless disabled)
SSH auth behavior in both scripts:
- If `./sshKey` exists, it is used first (`-i ./sshKey`)
- If key auth fails or no key is present, SSH falls back to password prompt
### Script options
| Purpose | Bash | PowerShell |
|---|---|---|
| VM user | `--vm-user dune` | `-VmUser dune` |
| VM host | `--vm-host 192.168.0.72` | `-VmHost 192.168.0.72` |
| SSH key path | `--ssh-key ./sshKey` | `-SshKeyPath .\sshKey` |
| Kubeconfig path | `--kubeconfig ~/.kube/dune-external.yaml` | `-KubeconfigPath "$HOME/.kube/dune-external.yaml"` |
| Image tag | `--image dune-admin:local` | `-Image dune-admin:local` |
| Namespace | `--namespace dune-admin` | `-Namespace dune-admin` |
| Manifest path | `--manifest deploy/k8s/dune-admin.rendered.yaml` | `-Manifest deploy/k8s/dune-admin.rendered.yaml` |
| Skip kubeconfig pull | `--skip-kubeconfig` | `-SkipKubeconfig` |
| Skip image build | `--skip-build` | `-SkipBuild` |
| Skip VM image import | `--skip-image-import` | `-SkipImageImport` |
| Skip port-forward | `--no-port-forward` | `-NoPortForward` |
### First deploy vs quick redeploy
First deploy:
```bash
./deploy.sh
```
Quick redeploy (reuse kubeconfig, no auto port-forward):
```bash
./deploy.sh --skip-kubeconfig --no-port-forward
```
Quick redeploy with the exact same image tag (advanced):
```bash
./deploy.sh --image dune-admin:local --skip-kubeconfig --no-port-forward
```
Override defaults with flags (same names on both scripts), e.g.:
```bash
./deploy.sh --vm-user dune --vm-host 192.168.0.72 --image dune-admin:local --no-port-forward
```
```powershell
./deploy.ps1 -VmUser dune -VmHost 192.168.0.72 -Image dune-admin:local -NoPortForward
```
## Manual config (`~/.dune-admin/config.yaml`)
```yaml
control: kubectl
ssh_host: 192.168.0.72:22 # VM host:port
ssh_user: dune # SSH user
ssh_key: /home/you/.ssh/key # absolute path; omit to use auto-detection
db_host: 127.0.0.1 # unused for kubectl — pod IP is discovered automatically
db_port: 15432
db_user: postgres
db_pass: yourpassword
db_name: dune
db_schema: dune
# Optional — discovered automatically if omitted:
control_namespace: funcom-seabass-mybattlegroup
# Optional broker command path:
broker_game_addr: 10.43.48.246:5672
broker_admin_addr: 10.43.189.193:5672
broker_tls: true
# Optional:
backup_dir: /funcom/artifacts/database-dumps/mybattlegroup
listen_addr: :8080
scrip_currency: 1
```
## What works
| Feature | Supported |
|---------|-----------|
| Battlegroup status (phase, servers) | Yes |
| Start / stop / restart | Yes — `kubectl patch battlegroup` |
| Update / backup | Yes — `battlegroup.sh` |
| Pod list | Yes |
| Log streaming | Yes — `kubectl logs -f` |
| DB access | Yes — tunnelled through SSH to pod IP |
| RabbitMQ broker commands | Yes — `kubectl exec` into broker pod |
| Backup download / upload / restore | Yes |
## Backward compatibility
Existing `.env` files with just `SSH_HOST`, `SSH_USER`, `DB_PASS`, etc. continue to work unchanged. The control plane defaults to `kubectl` whenever `SSH_HOST` is set, and the namespace is auto-discovered at startup.
## Troubleshooting
**Battlegroup tab shows nothing** — the namespace was not discovered. Check that the SSH user can run `sudo kubectl get pods -A`. You can also pin it explicitly with `control_namespace` in config.yaml.
**DB connection fails** — pod discovery succeeded but DB password is wrong. Delete `~/.dune-admin/config.yaml` and re-run `make setup`.
**"sudo: kubectl: command not found"** — kubectl is not in the sudo-safe PATH on the VM. Add `/usr/local/bin` (or wherever kubectl lives) to `/etc/sudoers` `secure_path`.
**Port-forward works but `http://127.0.0.1:8080` returns 404** — you are running an old image that does not contain the built frontend (`/app/dist`). Rebuild and redeploy with the deploy script.
**Market bot panel shows inactive after deploy** — verify:
1. `market_bot_enabled: true` in `~/.dune-admin/config.yaml`
2. `market_bot_item_data: /app/item-data.json`
3. `market_bot_cache_db: /data/market-bot-cache.db`
Then redeploy and check:
```bash
kubectl -n dune-admin logs deploy/dune-admin --tail=120
kubectl -n dune-admin run curl --rm -it --restart=Never --image=curlimages/curl -- \
sh -c "curl -s http://dune-admin:8080/api/v1/market-bot/status"
```