#!/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.$((9 + 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" } 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 } setup_wireguard_mesh() { local count="$1" if [ "$count" -lt 2 ]; then return fi echo "==> Configuring WireGuard mesh for ${count} coold VMs..." local pub1 pub2 ip1 ip2 port1 port2 subnet1 subnet2 pub1="$(coold_vm 1 wg-public-key)" pub2="$(coold_vm 2 wg-public-key)" ip1="$(coold_vm_wg_ip 1)" ip2="$(coold_vm_wg_ip 2)" port1="$(coold_vm_wg_port 1)" port2="$(coold_vm_wg_port 2)" subnet1="$(coold_vm_container_subnet 1)" subnet2="$(coold_vm_container_subnet 2)" coold_vm 1 setup-wireguard "$ip1" "$ip2" "host.lima.internal" "$pub2" "$port1" "$port2" "$subnet2" coold_vm 2 setup-wireguard "$ip2" "$ip1" "host.lima.internal" "$pub1" "$port2" "$port1" "$subnet1" } 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 } up() { local coold_vm_enabled local follow_dev_logs local count coold_vm_enabled="$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)" follow_dev_logs="$(read_coolify_env COOLIFY_DEV_FOLLOW_LOGS true)" count="$(coold_vm_count)" if [ "$coold_vm_enabled" != "false" ]; then echo "==> Starting ${count} Coolify coold VM(s) before Spin..." for index in $(seq 1 "$count"); do coold_vm "$index" up done setup_wireguard_mesh "$count" 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 for index in $(seq 1 "$count"); do host_id="$(coold_vm_instance "$index")" echo "==> Minting Flux dev host JWT for ${host_id}..." host_jwt="$(mint_host_jwt_for_host "$host_id")" echo "==> Installing host JWT into ${host_id}..." coold_vm "$index" install-host-jwt "$host_jwt" done for index in $(seq 1 "$count"); do echo "==> Starting coold VM agent service on $(coold_vm_instance "$index")..." if [ "$count" -ge 2 ]; then peer_index=1 if [ "$index" = "1" ]; then peer_index=2 fi COOLIFY_COOLD_VM_WG_PEER_IP="$(coold_vm_wg_ip "$peer_index")" coold_vm "$index" start-agent else coold_vm "$index" start-agent fi 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 } 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 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 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 ;; corrosion) corrosion "$@" ;; firewall) firewall "$@" ;; example-nginx) example_nginx "$@" ;; -h|--help|help|"") usage ;; *) echo "unknown command: $cmd" >&2 usage >&2 exit 1 ;; esac