#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" read_coolify_env() { key="$1" default_value="$2" current_value="${!key:-}" if [ -n "$current_value" ]; then printf '%s\n' "$current_value" return fi if [ -f .env ]; then env_value="$(grep -E "^${key}=" .env 2>/dev/null | tail -n1 | cut -d= -f2- | sed "s/^['\"]//; s/['\"]$//")" if [ -n "$env_value" ]; then printf '%s\n' "$env_value" return fi fi printf '%s\n' "$default_value" } coold_vm_count() { local count count="$(read_coolify_env COOLIFY_COOLD_VM_COUNT 2)" if [ "$count" != "1" ] && [ "$count" != "2" ]; then echo "ERROR: COOLIFY_COOLD_VM_COUNT supports 1 or 2 for now." >&2 exit 1 fi printf '%s\n' "$count" } coold_vm_instance() { local index="$1" local base base="$(read_coolify_env COOLIFY_COOLD_LIMA_INSTANCE coold-dev)" if [ "$index" = "1" ]; then printf '%s\n' "$base" return fi read_coolify_env "COOLIFY_COOLD_LIMA_INSTANCE_${index}" "${base}-${index}" } coold_vm_wg_ip() { local index="$1" read_coolify_env "COOLIFY_COOLD_VM_WG_IP_${index}" "100.64.0.${index}" } coold_vm_wg_port() { local index="$1" read_coolify_env "COOLIFY_COOLD_VM_WG_PORT_${index}" "$((51820 + index))" } coold_vm_container_subnet() { local index="$1" read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_SUBNET_${index}" "10.210.$((index - 1)).0/24" } coold_vm_container_gateway() { local index="$1" read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_GATEWAY_${index}" "10.210.$((index - 1)).1" } host_arch() { case "$(uname -m)" in x86_64|amd64) printf '%s\n' amd64 ;; arm64|aarch64) printf '%s\n' arm64 ;; *) echo "ERROR: unsupported host architecture: $(uname -m)" >&2 exit 1 ;; esac } host_os() { case "$(uname -s)" in Darwin) printf '%s\n' darwin ;; Linux) printf '%s\n' linux ;; *) echo "ERROR: unsupported host OS: $(uname -s)" >&2 exit 1 ;; esac } cooldctl_bin() { printf '%s\n' "$ROOT/.dev/bin/cooldctl" } build_local_cooldctl() { local bin local coold_repo bin="$(cooldctl_bin)" coold_repo="$(read_coolify_env COOLIFY_COOLD_REPO /Users/heyandras/devel/coold)" if [ ! -d "$coold_repo/cooldctl" ]; then echo "ERROR: macOS cooldctl release artifact is not available yet and local coold repo was not found at ${coold_repo}." >&2 echo "Set COOLIFY_COOLD_REPO=/path/to/coold or build/copy cooldctl to ${bin}." >&2 exit 1 fi echo "==> Building local macOS cooldctl from ${coold_repo}" (cd "$coold_repo" && cargo build -p cooldctl) mkdir -p "$(dirname "$bin")" install -m 0755 "$coold_repo/target/debug/cooldctl" "$bin" } ensure_cooldctl() { local bin local version local arch local os local url local tmpdir bin="$(cooldctl_bin)" version="$(read_coolify_env COOLIFY_COOLDCTL_VERSION "$(read_coolify_env COOLIFY_COOLD_VERSION nightly)")" arch="$(host_arch)" os="$(host_os)" if [ "$os" = "darwin" ]; then build_local_cooldctl return fi if [ -x "$bin" ] && "$bin" --version >/dev/null 2>&1 && [ "${COOLIFY_COOLDCTL_FORCE_DOWNLOAD:-false}" != "true" ]; then return fi url="https://github.com/coollabsio/coold/releases/download/${version}/cooldctl-${os}-${arch}.tar.gz" echo "==> Installing cooldctl from ${url}" mkdir -p "$(dirname "$bin")" tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' RETURN curl -fsSL --retry 3 --max-time 120 -o "$tmpdir/cooldctl.tar.gz" "$url" tar -xzf "$tmpdir/cooldctl.tar.gz" -C "$tmpdir" install -m 0755 "$tmpdir/cooldctl" "$bin" } lima_ssh_target() { local index="$1" local instance local ssh instance="$(coold_vm_instance "$index")" ssh="$(limactl list 2>/dev/null | awk -v name="$instance" 'NR > 1 && $1 == name {print $3}')" if [ -z "$ssh" ] || [ "$ssh" = "-" ]; then echo "ERROR: could not determine SSH target for Lima VM ${instance}. Start it first with scripts/dev.sh up." >&2 exit 1 fi printf '%s\n' "$ssh" } cooldctl_nodes_arg() { local count local nodes="" count="$(coold_vm_count)" for index in $(seq 1 "$count"); do if [ -n "$nodes" ]; then nodes="${nodes}," fi nodes="${nodes}$(lima_ssh_target "$index")" done printf '%s\n' "$nodes" } cooldctl_wg_listen_overrides_arg() { local count local overrides="" local node count="$(coold_vm_count)" for index in $(seq 1 "$count"); do node="$(lima_ssh_target "$index")" if [ -n "$overrides" ]; then overrides="${overrides}," fi overrides="${overrides}${node}=$(coold_vm_wg_port "$index")" done printf '%s\n' "$overrides" } cooldctl_wg_endpoint_overrides_arg() { local count local overrides="" local node count="$(coold_vm_count)" for index in $(seq 1 "$count"); do node="$(lima_ssh_target "$index")" if [ -n "$overrides" ]; then overrides="${overrides}," fi overrides="${overrides}${node}=host.lima.internal:$(coold_vm_wg_port "$index")" done printf '%s\n' "$overrides" } cooldctl_ssh_key() { read_coolify_env COOLIFY_COOLDCTL_SSH_KEY "$HOME/.lima/_config/user" } cooldctl_ssh_user() { read_coolify_env COOLIFY_COOLDCTL_SSH_USER "$USER" } cooldctl_bootstrap_command() { ensure_cooldctl cat < Commands: install Download/install the nightly cooldctl dev binary path Print the local cooldctl path bootstrap-command Print the dev Lima bootstrap command without running it run Run cooldctl with arbitrary args Example: scripts/dev.sh cooldctl bootstrap-command USAGE ;; *) echo "unknown cooldctl command: $command" >&2 echo "Run: scripts/dev.sh cooldctl help" >&2 exit 1 ;; esac } coold_vm() { local index="$1" shift COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance "$index")" \ COOLIFY_COOLD_VM_WG_IP="$(coold_vm_wg_ip "$index")" \ COOLIFY_COOLD_VM_CONTAINER_SUBNET="$(coold_vm_container_subnet "$index")" \ COOLIFY_COOLD_VM_CONTAINER_GATEWAY="$(coold_vm_container_gateway "$index")" \ scripts/coold-vm.sh "$@" } mint_host_jwt_for_host() { local host_id="$1" local attempts=60 local output local caps local builder_capacity builder_capacity="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)" caps="coold" if [ "$builder_capacity" != "0" ]; then caps="coold,builder" fi for attempt in $(seq 1 "$attempts"); do if output="$(spin exec -T coolify php artisan flux:dev "$host_id" --caps="$caps" 2>&1)"; then printf '%s\n' "$output" | tail -n 1 return 0 fi echo "==> Waiting for Flux dev JWT key for ${host_id} (${attempt}/${attempts})..." >&2 printf '%s\n' "$output" | tail -n 3 | sed 's/^/[coolify] /' >&2 sleep 2 done echo "ERROR: Could not mint Flux dev host JWT for ${host_id}." >&2 return 1 } follow_logs() { local coold_vm_enabled="$1" local count local vm_logs_pids="" count="$(coold_vm_count)" echo "==> Following dev logs. Press Ctrl-C to stop the dev environment." cleanup_logs() { for pid in $vm_logs_pids; do kill "$pid" >/dev/null 2>&1 || true done } stop_from_signal() { trap - INT TERM EXIT cleanup_logs echo echo "==> Ctrl-C received; stopping dev environment..." down exit 130 } trap cleanup_logs EXIT trap stop_from_signal INT TERM if [ "$coold_vm_enabled" != "false" ]; then for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" coold_vm "$index" logs-agent | sed "s/^/[${instance}] /" & vm_logs_pids="$vm_logs_pids $!" done fi spin logs -f } configure_flux_dev_for_vm() { local index="$1" local host_id local host_jwt local flux_url host_id="$(coold_vm_wg_ip "$index")" flux_url="$(read_coolify_env COOLIFY_COOLD_VM_FLUX_URL http://host.lima.internal:6443)" echo "==> Minting Flux dev host JWT for ${host_id}..." host_jwt="$(mint_host_jwt_for_host "$host_id")" echo "==> Installing Flux dev env/JWT into $(coold_vm_instance "$index")..." coold_vm "$index" install-host-jwt "$host_jwt" COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance "$index")" scripts/coold-vm.sh shell </dev/null < Starting ${count} Coolify coold VM(s) before Spin..." vm_pids="" for index in $(seq 1 "$count"); do ( coold_vm "$index" up ) & vm_pids="${vm_pids} $!" done for pid in $vm_pids; do wait "$pid" done else echo "==> COOLIFY_COOLD_VM_ENABLED=false; skipping coold VM." fi echo "==> Starting Coolify Docker stack with Spin..." spin up -d "$@" if [ "$coold_vm_enabled" != "false" ]; then echo "==> Bootstrapping coold VM mesh with cooldctl..." cooldctl_bootstrap for index in $(seq 1 "$count"); do configure_flux_dev_for_vm "$index" done fi if [ "$follow_dev_logs" = "false" ]; then echo "==> Dev environment is ready. Use 'spin logs -f' or 'scripts/coold-vm.sh logs-agent' to follow logs." return fi follow_logs "$coold_vm_enabled" } down() { local coold_vm_enabled local stop_coold_vm coold_vm_enabled="$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)" stop_coold_vm="$(read_coolify_env COOLIFY_COOLD_VM_STOP_ON_DOWN false)" if [ "$coold_vm_enabled" != "false" ]; then for index in $(seq 1 "$(coold_vm_count)"); do echo "==> Stopping coold VM agent service on $(coold_vm_instance "$index")..." coold_vm "$index" stop-agent || true done fi echo "==> Stopping Coolify Docker stack with Spin..." spin down "$@" if [ "$stop_coold_vm" = "true" ]; then echo "==> Stopping Coolify coold VM..." for index in $(seq 1 "$(coold_vm_count)"); do coold_vm "$index" stop done fi } clean_vms() { local count count="$(coold_vm_count)" echo "==> Deleting ${count} coold Lima VM(s). This removes VM-local state." for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" echo "==> Deleting ${instance}..." limactl stop --force --tty=false "$instance" >/dev/null 2>&1 || true if ! run_with_timeout 60 limactl delete --force --tty=false "$instance"; then echo "WARN: limactl delete timed out for ${instance}; killing matching limactl clients." >&2 pkill -f "limactl.*${instance}" >/dev/null 2>&1 || true rm -rf "$HOME/.lima/${instance}" fi done } run_with_timeout() { local timeout_seconds="$1" shift local pid local elapsed=0 "$@" & pid="$!" while kill -0 "$pid" >/dev/null 2>&1; do if [ "$elapsed" -ge "$timeout_seconds" ]; then kill "$pid" >/dev/null 2>&1 || true sleep 2 kill -9 "$pid" >/dev/null 2>&1 || true wait "$pid" >/dev/null 2>&1 || true return 124 fi sleep 1 elapsed=$((elapsed + 1)) done wait "$pid" } corrosion_for_each_vm() { local label="$1" shift local count local script count="$(coold_vm_count)" script="$(cat)" for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" echo "--- ${instance}: ${label} ---" printf '%s\n' "$script" | COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell "$@" done } corrosion_check() { local count count="$(coold_vm_count)" for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" peer_index=1 if [ "$index" = "1" ]; then peer_index=2 fi peer_ip="$(coold_vm_wg_ip "$peer_index")" echo "--- ${instance}: check ---" COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell </dev/null 2>&1 && echo active || echo unavailable printf 'peer ping (${peer_ip}): ' ping -c 1 -W 2 ${peer_ip} >/dev/null 2>&1 && echo ok || echo failed printf 'gossip: ' awk '/^\[gossip\]/{section=1; next} /^\[/{section=0} section && /^addr =|^bootstrap =/ {printf "%s ", \$0} END {print ""}' /etc/corrosion/config.toml printf 'registered containers: ' sudo sqlite3 /var/lib/corrosion/corrosion.db 'select count(*) from service_endpoints;' 2>/dev/null || echo unavailable SH done } corrosion_containers() { corrosion_for_each_vm containers <<'SH' echo '[corrosion service_endpoints]' sudo sqlite3 -header -column /var/lib/corrosion/corrosion.db \ 'select container_id, container_name, namespace, host_mgmt_ip, container_ip, state, health, updated_at from service_endpoints order by updated_at desc;' 2>/dev/null \ || echo 'service_endpoints table unavailable' echo echo '[rootful podman]' sudo podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null \ || echo 'rootful podman unavailable' echo echo '[rootless podman]' podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null \ || echo 'rootless podman unavailable' SH } corrosion_config() { corrosion_for_each_vm config <<'SH' sudo cat /etc/corrosion/config.toml SH } corrosion_logs() { local index="${1:-1}" coold_vm "$index" logs-agent } corrosion_sql() { local query="$*" if [ -z "$query" ]; then echo "Usage: scripts/dev.sh corrosion sql " >&2 exit 1 fi corrosion_for_each_vm sql < Commands: check Show Corrosion service, WireGuard, gossip, and row-count state containers List registered service_endpoints rows on each VM config Print /etc/corrosion/config.toml from each VM logs [n] Follow coold/corrosion logs for VM n (default: 1) sql Run a read-only sqlite query against each Corrosion DB USAGE ;; *) echo "unknown corrosion command: $command" >&2 echo "Run: scripts/dev.sh corrosion help" >&2 exit 1 ;; esac } example_nginx_name() { local index="$1" if [ "$index" = "1" ]; then printf '%s\n' coolify-example-nginx return fi printf 'coolify-example-nginx-%s\n' "$index" } example_nginx_up() { local count count="$(coold_vm_count)" for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" name="$(example_nginx_name "$index")" gateway="$(coold_vm_container_gateway "$index")" echo "--- ${instance}: starting ${name} with coold DNS (${gateway}) ---" COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell </dev/null 2>&1 || true sudo podman run -d \\ --name ${name} \\ --network coolify-default-mesh \\ --dns ${gateway} \\ --dns-search default.coolify.internal \\ docker.io/library/nginx:alpine SH done } example_nginx_down() { local count count="$(coold_vm_count)" for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" name="$(example_nginx_name "$index")" echo "--- ${instance}: removing ${name} ---" COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell </dev/null 2>&1 || true SH done } example_nginx_check_dns() { COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance 1)" scripts/coold-vm.sh shell <<'SH' set -e echo '--- resolv.conf ---' sudo podman exec coolify-example-nginx cat /etc/resolv.conf echo echo '--- coold DNS lookup through search domain ---' sudo podman exec coolify-example-nginx nslookup coolify-example-nginx-2 echo echo '--- coold DNS lookup by full name ---' sudo podman exec coolify-example-nginx nslookup coolify-example-nginx-2.default.coolify.internal SH } example_nginx_help() { cat <<'USAGE' Usage: scripts/dev.sh example-nginx Commands: up Start one nginx container on each coold VM with coold DNS configured down Remove the example nginx containers check-dns Verify host 1 nginx can resolve host 2 nginx through coold DNS USAGE } example_nginx() { local command="${1:-help}" if [ $# -gt 0 ]; then shift fi case "$command" in up) example_nginx_up ;; down) example_nginx_down ;; check-dns) example_nginx_check_dns ;; -h|--help|help) example_nginx_help ;; *) echo "unknown example-nginx command: $command" >&2 echo "Run: scripts/dev.sh example-nginx help" >&2 exit 1 ;; esac } firewall_help() { cat <<'USAGE' Usage: scripts/dev.sh firewall Commands: allow [proto] [port] Allow traffic on every coold VM (proto/port optional) revoke [id|src] [dst] [proto] [port] Remove an allow rule from every coold VM list List allow rules on every coold VM reconcile Re-apply firewall snapshot on every coold VM Examples: scripts/dev.sh firewall allow 10.210.0.2 10.210.1.2 tcp 80 scripts/dev.sh firewall revoke scripts/dev.sh firewall revoke 10.210.0.2 10.210.1.2 tcp 80 scripts/dev.sh firewall revoke 3ba6e0c235a6 scripts/dev.sh firewall list USAGE } firewall_api_for_each_vm() { local label="$1" local method="$2" local path="$3" local body="${4:-}" local count count="$(coold_vm_count)" for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" api_ip="$(coold_vm_wg_ip "$index")" echo "--- ${instance}: ${label} ---" COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell <&2 exit 1 fi if [ -n "$port" ] && [ -z "$proto" ]; then echo "ERROR: port requires proto (tcp or udp)." >&2 exit 1 fi body="$(printf '{"namespace":"default","src":"%s","dst":"%s"' "$src" "$dst")" if [ -n "$proto" ]; then body="${body}$(printf ',"proto":"%s"' "$proto")" fi if [ -n "$port" ]; then body="${body}$(printf ',"port":%s' "$port")" fi body="${body}}" firewall_api_for_each_vm "allow ${src} -> ${dst}" POST /api/v1/firewall/allow "$body" } firewall_rule_id() { local src="$1" local dst="$2" local proto="${3:-}" local port="${4:-0}" if [ -z "$proto" ]; then port=0 fi printf 'default|%s|%s|%s|%s' "$src" "$dst" "$proto" "$port" \ | shasum -a 256 \ | awk '{print substr($1, 1, 12)}' } firewall_revoke() { local id_or_src="${1:-}" local dst="${2:-}" local proto="${3:-}" local port="${4:-}" local id if [ -z "$id_or_src" ]; then echo "Current firewall allow rule IDs:" firewall_list echo echo "Revoke one with: scripts/dev.sh firewall revoke " return fi if [ -z "$dst" ]; then id="$id_or_src" else id="$(firewall_rule_id "$id_or_src" "$dst" "$proto" "$port")" fi firewall_api_for_each_vm "revoke ${id}" DELETE "/api/v1/firewall/allow/${id}" } firewall_list() { firewall_api_for_each_vm list GET '/api/v1/firewall/allow?namespace=default' } firewall_reconcile() { firewall_api_for_each_vm reconcile POST /api/v1/firewall/reconcile } firewall() { local command="${1:-help}" if [ $# -gt 0 ]; then shift fi case "$command" in allow) firewall_allow "$@" ;; revoke|remove|delete|deny) firewall_revoke "$@" ;; list) firewall_list ;; reconcile) firewall_reconcile ;; -h|--help|help) firewall_help ;; *) echo "unknown firewall command: $command" >&2 echo "Run: scripts/dev.sh firewall help" >&2 exit 1 ;; esac } usage() { cat <<'USAGE' Usage: scripts/dev.sh [spin args] Commands: up Start the coold VM, Spin stack, and dev coold agent down Stop the dev coold agent and Spin stack shell [n] Open a shell inside coold VM n (default: 1) list Show Lima instances clean-vms Delete the coold Lima VMs and all VM-local runtime state corrosion Inspect Corrosion state, config, logs, and registered containers firewall Manage dev coold firewall allow rules example-nginx Start/check example nginx containers with coold DNS cooldctl Install/run the released cooldctl dev helper USAGE } cmd="${1:-}" if [ $# -gt 0 ]; then shift fi case "$cmd" in up) up "$@" ;; down) down "$@" ;; shell) coold_vm "${1:-1}" shell ;; list) limactl list ;; clean-vms|clean-vm|reset-vms) clean_vms ;; corrosion) corrosion "$@" ;; firewall) firewall "$@" ;; example-nginx) example_nginx "$@" ;; cooldctl) cooldctl_dev "$@" ;; -h|--help|help|"") usage ;; *) echo "unknown command: $cmd" >&2 usage >&2 exit 1 ;; esac