diff --git a/Makefile b/Makefile index ac86b94d..e763af23 100644 --- a/Makefile +++ b/Makefile @@ -389,6 +389,12 @@ build-k8s-cluster: \ ./bin/start-kind.sh .PHONY: build-k8s-cluster +build-k8s-cluster-orbstack: ## setup the kubernetes environment on OrbStack's built-in cluster (macOS) +build-k8s-cluster-orbstack: \ + env.d/development/kube-secret + ./bin/start-orbstack.sh +.PHONY: build-k8s-cluster-orbstack + start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak DEV_ENV=dev-keycloak tilt up --namespace=meet -f ./bin/Tiltfile .PHONY: build-k8s-cluster diff --git a/bin/Tiltfile b/bin/Tiltfile index 1990a9c0..f206074f 100644 --- a/bin/Tiltfile +++ b/bin/Tiltfile @@ -1,5 +1,11 @@ load('ext://uibutton', 'cmd_button', 'bool_input', 'location') load('ext://namespace', 'namespace_create', 'namespace_inject') + +# OrbStack's built-in cluster (macOS) is a supported alternative to kind. +# Recent Tilt versions (>= 0.33) detect it as a local dev cluster; this is +# a no-op for kind and a safety net for older Tilt versions. +allow_k8s_contexts('orbstack') + namespace_create('meet') DEV_ENV = os.getenv('DEV_ENV', 'dev-keycloak') diff --git a/bin/start-orbstack.sh b/bin/start-orbstack.sh new file mode 100755 index 00000000..9aff3062 --- /dev/null +++ b/bin/start-orbstack.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# +# Bootstrap the local dev environment on OrbStack's built-in Kubernetes +# cluster (macOS) instead of kind. +# +# This replicates what bin/start-kind.sh (numerique-gouv/tools +# kind/create_cluster.sh) provides, minus what OrbStack makes unnecessary: +# - no kind cluster: OrbStack ships a lightweight single-node cluster +# - no local registry (kind-registry): OrbStack's cluster shares the +# Docker image store, so images built by Tilt are directly visible +# to pods. Tilt detects the "orbstack" context as a local cluster +# and skips pushing images entirely. +# +# Requirements: OrbStack (with Kubernetes enabled), kubectl, mkcert, curl. +set -o errexit + +APPLICATION=${1:-meet} +CONTEXT="orbstack" + +echo "0. Check OrbStack Kubernetes is available" +if ! command -v mkcert >/dev/null 2>&1; then + echo "❌ mkcert is not installed. Install it first: brew install mkcert" + exit 1 +fi +if ! kubectl config get-contexts -o name | grep -qx "${CONTEXT}"; then + echo "Context '${CONTEXT}' not found. Trying to start OrbStack Kubernetes..." + if command -v orb >/dev/null 2>&1; then + orb start k8s + else + echo "❌ Enable Kubernetes in OrbStack (Settings > Kubernetes) and retry." + exit 1 + fi +fi +kubectl config use-context "${CONTEXT}" + +echo "0b. Check ports 80/443 are free on localhost" +# OrbStack forwards LoadBalancer service ports to 127.0.0.1. If the kind +# cluster is still running, its docker proxy already holds 80/443. +# Skip the check if ingress-nginx is already installed here: in that case +# the listener on 80/443 is our own LoadBalancer. +if ! kubectl -n ingress-nginx get deployment ingress-nginx-controller >/dev/null 2>&1; then + for port in 80 443; do + if lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then + echo "❌ Port ${port} is already in use on the host." + echo " If the kind cluster is running, delete it first:" + echo " kind delete cluster --name suite" + exit 1 + fi + done +fi + +echo "1. Create ca" +CURRENT_DIR=$(pwd) +mkcert -install +cd /tmp +mkcert "127.0.0.1.nip.io" "*.127.0.0.1.nip.io" +cd "${CURRENT_DIR}" + +echo "2. Install ingress-nginx (cloud provider: LoadBalancer service)" +# OrbStack exposes LoadBalancer services on 127.0.0.1, so the cloud +# manifest replaces kind's hostPort-based deploy. Every sub-step below is +# guarded individually so the script is safe to re-run after a partial +# failure (unlike the upstream kind script, which guards the whole block +# on namespace existence). + +# Make sure no stale registry configmap tells Tilt to push to localhost:5001 +# (there is no registry on OrbStack). +kubectl -n kube-public delete configmap local-registry-hosting --ignore-not-found + +if ! kubectl -n ingress-nginx get deployment ingress-nginx-controller >/dev/null 2>&1; then + kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml +fi +if ! kubectl -n ingress-nginx get deployment nginx-errors >/dev/null 2>&1; then + kubectl apply -n ingress-nginx -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/refs/heads/main/docs/examples/customization/custom-errors/custom-default-backend.yaml +fi +kubectl -n ingress-nginx create secret tls mkcert --key /tmp/127.0.0.1.nip.io+1-key.pem --cert /tmp/127.0.0.1.nip.io+1.pem || echo ok + +# The meet charts render Ingresses without ingressClassName. The kind +# provider manifest handles this via --watch-ingress-without-class=true; +# the cloud manifest does not, so add it here (otherwise: 404 everywhere). +if ! kubectl -n ingress-nginx get deployment ingress-nginx-controller -o jsonpath='{.spec.template.spec.containers[0].args}' | grep -q 'watch-ingress-without-class'; then + kubectl -n ingress-nginx patch deployments.apps ingress-nginx-controller --type 'json' -p '[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--watch-ingress-without-class=true"},{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--default-ssl-certificate=ingress-nginx/mkcert"},{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--default-backend-service=ingress-nginx/nginx-errors"} +]' +fi +if ! kubectl -n ingress-nginx get deployment nginx-errors -o jsonpath='{.spec.template.spec.containers[0].image}' | grep -q 'error-pages'; then + kubectl -n ingress-nginx patch deployment nginx-errors --type=json -p='[ + {"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "ghcr.io/tarampampam/error-pages:3.3.0"}, + {"op": "add", "path": "/spec/template/spec/containers/0/env", "value": [{"name": "TEMPLATE_NAME", "value": "ghost"}, {"name": "SHOW_DETAILS", "value": "false"}, {"name": "SEND_SAME_HTTP_CODE", "value": "true"}]} +]' +fi +cat </tmp/Corefile.orbstack + kubectl -n kube-system create configmap coredns --from-file=Corefile=/tmp/Corefile.orbstack --dry-run=client -o yaml | kubectl apply -f - + kubectl -n kube-system rollout restart deployments/coredns +fi + +if ! kubectl get ns "${APPLICATION}" >/dev/null 2>&1; then + echo "4. Setup namespace" + kubectl create ns "${APPLICATION}" +fi +kubectl config set-context --current --namespace="${APPLICATION}" +kubectl -n "${APPLICATION}" create secret generic mkcert --from-file=rootCA.pem="$(mkcert -CAROOT)/rootCA.pem" || echo ok + +if ! kubectl get configmap certifi -n "${APPLICATION}" >/dev/null 2>&1; then + echo "5. Inject our custom CA in a configmap for certifi" + curl https://raw.githubusercontent.com/certifi/python-certifi/refs/heads/master/certifi/cacert.pem -o /tmp/cacert.pem + cat "$(mkcert -CAROOT)/rootCA.pem" >>/tmp/cacert.pem + kubectl -n "${APPLICATION}" create configmap certifi --from-file=cacert.pem=/tmp/cacert.pem + kubectl -n "${APPLICATION}" create secret generic certifi --from-file=/tmp/cacert.pem || echo ok +fi + +echo "5b. Smoke test: the ingress chain answers on https://127.0.0.1" +# Before Tilt deploys the app this returns the styled 404 from the default +# backend — that still proves LB -> controller works. 000 means the +# LoadBalancer is not bound to localhost. +HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 https://127.0.0.1/ || true) +if [ "${HTTP_CODE}" = "000" ]; then + echo "⚠️ Nothing answered on https://127.0.0.1 — check the LoadBalancer:" + echo " kubectl -n ingress-nginx get svc ingress-nginx-controller" +else + echo "✅ Ingress reachable (HTTP ${HTTP_CODE})" +fi + +echo "6. Check pod readiness across all namespaces..." + +sleep_interval=10 + +echo "Initial wait time: $((sleep_interval * 2)) seconds…" +sleep $((sleep_interval * 2)) + +check_pods_ready() { + local max_attempts=60 # Maximum number of attempts (10 minutes with 10s intervals) + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt/$max_attempts - Checking pod status..." + + not_ready_count=$( kubectl get po -A --no-headers | grep -v -E "Running|Completed"| wc -l | tr -d ' ') + + if [ "$not_ready_count" -eq 0 ]; then + echo "✅ All pods are ready!" + return 0 + else + echo "⏳ $not_ready_count pod(s) still not ready. Waiting $sleep_interval seconds…" + sleep $sleep_interval + ((attempt++)) + fi + done + + echo "❌ Timeout: Some pods are still not ready after 10 minutes" + echo "Final pod status:" + kubectl get po -A + return 1 +} + +if check_pods_ready; then + echo "🎉 Cluster is fully ready!" +else + echo "⚠️ Some pods may need manual intervention" + exit 1 +fi \ No newline at end of file diff --git a/docs/developping_locally.md b/docs/developping_locally.md index f72f58f4..1cc12030 100644 --- a/docs/developping_locally.md +++ b/docs/developping_locally.md @@ -143,3 +143,24 @@ $ make start-tilt-keycloak ``` Monitor Tilt’s progress at [http://localhost:10350/](http://localhost:10350/). After Tilt actions finish, you can access the app at [https://meet.127.0.0.1.nip.io/](https://meet.127.0.0.1.nip.io/). + +### Alternative: OrbStack's built-in Kubernetes (macOS) + +If you use [OrbStack](https://orbstack.dev/) on macOS, you can run the stack on its built-in Kubernetes cluster instead of kind. It uses noticeably less RAM (no nested kubeadm node container) and no local registry is needed: OrbStack's cluster shares the Docker image store, so Tilt uses images directly without pushing. + +Enable Kubernetes in OrbStack (Settings > Kubernetes), then: + +```shellscript +$ make build-k8s-cluster-orbstack +``` + +This installs ingress-nginx (exposed by OrbStack on `127.0.0.1:80/443`), the mkcert TLS certificates, and the CoreDNS rewrite for `*.127.0.0.1.nip.io`, then you start Tilt as usual: + +```shellscript +$ make start-tilt-keycloak +``` + +Notes: +- Ports 80/443 must be free: delete the kind cluster first if you used it (`kind delete cluster --name suite`). +- If you "Reset Kubernetes" in OrbStack, re-run `make build-k8s-cluster-orbstack`. +- kind remains the reference setup (matches CI and lets you pin the Kubernetes version).