Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
509 changes: 262 additions & 247 deletions .github/workflows/on-pr-dory-smoke.yaml

Large diffs are not rendered by default.

92 changes: 79 additions & 13 deletions .github/workflows/on-pr-kind-smoke.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: kind backend smoke

# End-to-end check of `hops local` on the kind backend, which is the CI
# path (ubuntu runners ship kind, docker, kubectl, and helm). Exercises the
# registry NodePort mapping and BOTH containerd certs.d trust names, plus the
# stop/start resume path.
# End-to-end check of `hops local` on the kind backend (ubuntu runners).
# Registry round-trip + path-based config install (same fixture as dory smoke).

on:
pull_request:
Expand All @@ -17,6 +15,27 @@ jobs:

- uses: Swatinem/rust-cache@v2

- name: Install up CLI
run: |
set -euxo pipefail
# Required for `hops config install --path` (up project build).
curl -sL "https://cli.upbound.io" | sh
echo "$HOME/bin" >> "$GITHUB_PATH"
echo "$HOME/.up/bin" >> "$GITHUB_PATH"
# Installer may place the binary in different locations.
if [ -x "$HOME/bin/up" ]; then
echo "$HOME/bin" >> "$GITHUB_PATH"
elif [ -x /usr/local/bin/up ]; then
true
else
# Fall back: download latest release asset if shell installer differs.
mkdir -p "$HOME/bin"
curl -sL "https://cli.upbound.io/stable/current/linux_amd64/up" -o "$HOME/bin/up"
chmod +x "$HOME/bin/up"
echo "$HOME/bin" >> "$GITHUB_PATH"
fi
up version

- name: Build hops
run: cargo build

Expand All @@ -29,8 +48,14 @@ jobs:
- name: Registry round-trip through both pull names
run: |
set -euxo pipefail
docker pull public.ecr.aws/docker/library/busybox:stable
docker tag public.ecr.aws/docker/library/busybox:stable localhost:30500/smoke/busybox:ci
# Single-platform materialization — avoid multiplatform push warnings.
PLATFORM=linux/amd64
SRC=public.ecr.aws/docker/library/busybox:stable
docker pull --platform "$PLATFORM" "$SRC"
docker build --platform "$PLATFORM" --provenance=false --sbom=false \
-t localhost:30500/smoke/busybox:ci - <<EOF
FROM ${SRC}
EOF
docker push localhost:30500/smoke/busybox:ci

# Service-name pull: containerd resolves via its certs.d alias.
Expand All @@ -47,14 +72,55 @@ jobs:
pod/smoke-svc-name pod/smoke-localhost --timeout=180s
kubectl --context kind-hops delete pod smoke-svc-name smoke-localhost --wait=false

- name: Stop/start resume path (uses persisted backend, no flag)
- name: Path-based config install (fixture ConfigMap)
run: |
set -euxo pipefail
./target/debug/hops-cli local stop
./target/debug/hops-cli local start
kubectl --context kind-hops -n crossplane-system wait \
--for=condition=Available deployment/crossplane deployment/registry --timeout=300s
./target/debug/hops-cli local doctor
FIXTURE=tests/fixtures/config-smoke
test -d "$FIXTURE"
test -f "$FIXTURE/upbound.yaml"

./target/debug/hops-cli config install --path "$FIXTURE" --backend kind

for i in $(seq 1 90); do
healthy="$(kubectl --context kind-hops get configuration.pkg.crossplane.io hops-ops-config-smoke \
-o jsonpath='{.status.conditions[?(@.type=="Healthy")].status}' 2>/dev/null || true)"
installed="$(kubectl --context kind-hops get configuration.pkg.crossplane.io hops-ops-config-smoke \
-o jsonpath='{.status.conditions[?(@.type=="Installed")].status}' 2>/dev/null || true)"
if [ "$healthy" = "True" ] && [ "$installed" = "True" ]; then
echo "Configuration hops-ops-config-smoke Healthy after ${i} attempts"
break
fi
if [ "$i" -eq 90 ]; then
kubectl --context kind-hops get configuration.pkg.crossplane.io hops-ops-config-smoke -o yaml || true
kubectl --context kind-hops get configurationrevisions.pkg.crossplane.io -o wide || true
kubectl --context kind-hops get functions.pkg.crossplane.io -o wide || true
exit 1
fi
sleep 5
done

kubectl --context kind-hops wait --for=condition=Established \
crd/configsmokes.ci.hops.ops.com.ai --timeout=180s

kubectl --context kind-hops apply -f "$FIXTURE/local/ci-xr.yaml"

kubectl --context kind-hops -n hops-ci wait --for=condition=Ready \
configsmoke.ci.hops.ops.com.ai/hops-ci-smoke --timeout=300s

kubectl --context kind-hops -n hops-ci get configmap hops-ci-smoke -o yaml
smoke_val="$(kubectl --context kind-hops -n hops-ci get configmap hops-ci-smoke \
-o jsonpath='{.data.smoke}')"
test "$smoke_val" = "ok"

- name: Soft cleanup (CI fixture only)
if: always()
run: |
set +e
kubectl --context kind-hops delete pod smoke-svc-name smoke-localhost --wait=false 2>/dev/null || true
kubectl --context kind-hops -n hops-ci delete configsmoke hops-ci-smoke --wait=false 2>/dev/null || true
kubectl --context kind-hops -n hops-ci delete configmap hops-ci-smoke --wait=false 2>/dev/null || true
kubectl --context kind-hops -n hops-ci delete object hops-ci-smoke --wait=false 2>/dev/null || true

- name: hops local destroy
run: ./target/debug/hops-cli local destroy
if: always()
run: ./target/debug/hops-cli local destroy || true
226 changes: 202 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,14 @@ hops config install --repo hops-ops/aws-auto-eks-cluster --version v0.11.0
daemon: Docker Desktop, colima's dockerd, or CI runners. No VM of its own,
so sizing flags don't apply (size the docker daemon instead); requires
kind >= v0.27.
- **dory** — [dory](https://augani.github.io/dory)'s built-in k3s, driven
headlessly through the `dory` CLI (`dory k8s enable/disable/status`).
Requires the Dory app running (it provides the engine and forwards
published ports to localhost) and a `dory` CLI with headless k8s support.
hops writes `~/.dory/k8s/registries.yaml` (k3s' native registry trust) and
publishes the registry NodePort at cluster create. The VM is sized in the
Dory app, so hops sizing flags don't apply.
- **dory** — [dory](https://augani.github.io/dory) stock app: shared Apple
Silicon engine + product k3s. Enable Kubernetes **in the Dory app** (hops
does not fork Dory or call `dory k8s enable`). Package installs use the same
**in-cluster** `registry:2` as other backends (Crossplane pulls
`registry.crossplane-system.svc.cluster.local:5000`). Docker push uses the
k3s **NodePort** on the engine docker bridge (`{dory-k8s-ip}:30500`) because
dockerd runs *inside* the engine — Mac `localhost` is the wrong plane. The
VM is sized in the Dory app, so hops sizing flags don't apply.

Select with the global `--backend` flag:

Expand All @@ -243,31 +244,207 @@ persisted choice > existing cluster detection (colima wins) > platform
default (macOS: colima, otherwise kind).

Unless `--context` is given, kubectl commands automatically use the backend's
kubeconfig context (`colima`, `kind-hops`, or `dory`), regardless of your
current-context. For dory, hops also prepends `~/.kube/dory-config` (where
dory keeps its kubeconfig) to `KUBECONFIG` for its own kubectl/helm calls.
kubeconfig context (`colima`, `kind-hops`, or `hops-dory`), regardless of your
current-context.

#### Using dory

With a `dory` CLI that supports `dory k8s enable` (headless Kubernetes),
use the native backend:
Stock Dory only (brew cask / [Dory.app](https://augani.github.io/dory)). No hops
fork of Dory required.

```bash
# 1. Open Dory.app — engine healthy (not "needs attention")
# 2. Enable Kubernetes in the app; wait until the cluster is running
# (product container is usually named dory-k8s)
# 3. Bootstrap Crossplane + local package registry
hops local start --backend dory
```

For `hops provider install` / `hops config install` builds, point your docker
CLI at dory's engine (`export DOCKER_HOST=unix://$HOME/.dory/engine.sock`) so
image builds/pushes land on the daemon that reaches the registry.
On start/activate, hops:

Without the headless CLI, dory still exposes a real docker socket, so the
kind backend works against it:
- merges stock `~/.kube/dory-config` into `~/.kube/config` as context **`hops-dory`**
(override with `--name <name>` or `HOPS_DORY_NAME`; persisted in `~/.hops/local/dory-name`)
- runs `kubectl config use-context hops-dory`
- creates/uses a docker context of the same name → `unix://$HOME/.dory/dory.sock`

So you should **not** need:

```bash
export KUBECONFIG=$HOME/.kube/dory-config
export DOCKER_HOST=unix://$HOME/.dory/dory.sock
```

```bash
docker context use dory # or: export DOCKER_HOST=unix://$HOME/.dory/dory.sock
hops local start --backend dory # name defaults to hops-dory
hops local start --backend dory --name mine # custom kube+docker context name

kubectl get nodes # context hops-dory
docker info # context hops-dory
hops local doctor
hops local github -o hops-ops
hops config install --path … --backend dory
```

Alternatively, use kind on Dory's docker socket (no product k3s):

```bash
docker context use dory # product context from Dory.app
hops local start --backend kind
```

**CI:** `.github/workflows/on-pr-dory-smoke.yaml` runs on a **self-hosted**
Apple Silicon Mac labeled `hops-dory` (opt-in with PR label `test-dory` or
`workflow_dispatch`). Stock Dory only — no fork build, no Colima sock.
Offline runner → job waits. The session is **env-only** so your desktop
defaults never change: `DOCKER_HOST=unix://$HOME/.dory/dory.sock`, a
job-private `KUBECONFIG`, and `HOPS_DORY_DESKTOP=0` (hops skips
`use-context` / docker context switching). No `destroy`/`stop` of
`dory-k8s`. After start/doctor/registry, it path-installs the in-repo
fixture `tests/fixtures/config-smoke` (`hops config install --path …`) and
applies a namespaced ConfigMap XR under `hops-ci`. See the workflow header
for runner setup.

#### Dory architecture (why troubleshooting is different)

Two network planes matter. Mixing them up is the usual failure mode.

| Plane | Who | Where | Role |
|-------|-----|--------|------|
| **Engine Docker** | `docker` CLI via `~/.dory/dory.sock` | Linux VM (dockerd) | build/push images |
| **Cluster** | kubectl / Crossplane pods | k3s inside `dory-k8s` | run control plane + pull packages |

Package registry model (same idea as colima/kind, adapted to Dory):

- **Pull (Crossplane):** in-cluster Service
`registry.crossplane-system.svc.cluster.local:5000` over **HTTPS**
- **Push (docker on the engine):** k3s **NodePort** on the docker bridge
`{dory-k8s-ip}:30500` (not Mac `localhost:30500`)
- **TLS:** hops generates a self-signed CA/server cert (`Secret hops-local-registry-tls`)
and patches Crossplane to trust that CA. Plain HTTP fails package unpack
(`http: server gave HTTP response to HTTPS client`).

Do **not** put `host.dory.internal` in Crossplane package refs. That name exists
on the **node** `/etc/hosts`, not in pod CoreDNS, and is the wrong plane for
Crossplane's package manager.

#### Dory troubleshooting

**Engine won't start / thrash / `HV_BAD_ARGUMENT` (big Macs)**

The failure we hit in practice: on a **large laptop** (e.g. **128 GB** host
RAM), Dory's UI **Recommended** / host-scaled default for guest engine memory
was **~half of host RAM → 64 GB** (`DORYD_MEMORY_MB=65536`). That is not
"you need 64 GB of containers" — it is an aggressive ceiling. On Apple Silicon,
Hypervisor.framework often rejects creating a VM that large
(`HV_BAD_ARGUMENT`), so the engine never stays healthy and the UI looks broken
even though a **4 GB** engine works fine.

```bash
# Pin sane resources (4 GiB / 4 CPUs is enough for hops local)
defaults write com.pythonxi.Dory dory.engineMemoryMB -int 4096
defaults write com.pythonxi.Dory dory.engineCPUCount -int 4

# Confirm the LaunchAgent is not still on 65536:
launchctl print "gui/$(id -u)/dev.dory.doryd" 2>/dev/null | grep DORYD_MEMORY
# Live engine cmdline should show --mem-mb 4096, not 65536:
ps aux | grep 'dory-hv engine' | grep -v grep
```

In the Dory app **Resources** panel: set memory to **~4 GB** (8 GB is usually
fine too). **Do not apply Recommended** if it offers tens of GB on a high-RAM
Mac.

Then fully restart the engine (quit Dory, ensure no thrashing `doryd`/`dory-hv`,
reopen or `dory engine wake`). Once guest RAM is modest, the rest of hops local
is ordinary.

**Docker socket path**

Stock Docker API socket is:

```text
unix://$HOME/.dory/dory.sock
```

There is no `~/.dory/engine.sock` for the host Docker API. hops uses `dory.sock`
only. Prefer the docker context hops creates (`hops-dory`) over manual
`DOCKER_HOST`.

**Kubernetes missing / `no dory-k8s container`**

k3s is product-owned. hops does not run `dory k8s enable`.

1. Install the Kubernetes component if needed: `dory component install kubernetes`
2. Enable Kubernetes in the Dory app UI
3. Wait until a `dory-k8s` container is running: `docker ps` (with context hops-dory)
4. Re-run `hops local start --backend dory`

**`k ctx` has no dory / hops-dory entry**

Stock Dory writes `~/.kube/dory-config` (context often named `default`). hops
merges that into `~/.kube/config` as **`hops-dory`** on activate/start. If the
merge is missing:

```bash
hops local doctor --backend dory
kubectl config get-contexts # expect hops-dory
kubectl config use-context hops-dory
```

**Configuration Installed=False / HTTPS error**

```text
http: server gave HTTP response to HTTPS client
```

Crossplane always pulls packages with HTTPS. The local registry must be TLS
(hops sets this up). If you have an old HTTP-only registry Deployment:

```bash
kubectl -n crossplane-system delete deploy registry
kubectl -n crossplane-system delete secret hops-local-registry-tls
hops local start --backend dory # recreates TLS secret + registry + CA patch
```

**docker push to localhost:30500 fails (connection refused / HTTPS to HTTP)**

With the docker context pointed at Dory, **`localhost` is the engine VM**, not
the Mac. hops pushes to `{dory-k8s container IP}:30500` and marks the engine
docker bridge as an insecure registry for that self-signed TLS endpoint.
Check:

```bash
docker context show # hops-dory
hops local doctor # "package push registry reachable (…:30500)"
```

Do not rely on Mac `kubectl port-forward` for engine-side `docker push`.

**Engine docker broken after daemon.json / dockerd kill**

Prefer Dory's own recovery:

```bash
dory repair dockerd --apply
dory engine wake
dory readiness
```

Avoid raw `kill` of dockerd inside the guest unless you are prepared to wait for
`dory repair` / LaunchAgent recovery (`live-restore` helps but is not magic).

**Useful diagnostics**

```bash
dory doctor
dory readiness
hops local doctor --backend dory
docker context show
kubectl config current-context
kubectl get configuration,provider -A
kubectl -n crossplane-system get deploy,svc,secret | grep -E 'registry|crossplane|hops-local'
```

### Local provider setup and auth

`hops local aws`, `hops local github`, and `hops local zitadel` install the provider package and bootstrap auth into a local control plane. The exception is `--refresh`, which updates credentials only.
Expand Down Expand Up @@ -399,12 +576,13 @@ Notes:
- Runs `brew install colima`.
- `local reset`
- Runs `colima kubernetes reset`.
- `local start`
- Runs `colima start --kubernetes --cpu 8 --memory 16 --disk 60`
- Installs Crossplane from `crossplane-stable/crossplane`
- Applies manifests from `bootstrap/` for runtime config, providers, provider configs, and registry (embedded in the binary at build time)
- Configures Docker in Colima for insecure pulls from `registry.crossplane-system.svc.cluster.local:5000`
- Adds host mapping in Colima VM for the registry service DNS name
- `local start [--bootstrap]`
- Brings up the selected backend cluster (colima / kind / dory)
- If Crossplane, k8s+helm providers, and the package registry are already
Healthy/Available, skips helm repo update/upgrade and bootstrap reapply
(fast resume). Pass `--bootstrap` to force a full helm upgrade + reapply.
- Cold path: installs Crossplane, applies `bootstrap/` DRCs/providers/PCs,
deploys the local HTTPS package registry, wires node/engine registry trust
- `local stop`
- Runs `colima stop`.
- `local destroy`
Expand Down
Loading
Loading