Files
coolify/scripts/dev.sh
T
Andras Bacsai 1b1b0726a2 feat(dev): add coold VM dev workflow
Add scripts and Lima config for managing local coold endpoint VMs,
mint Flux dev host JWTs, expose coold host details on the V5 home
page, and document the local development workflow.
2026-06-16 11:39:36 +02:00

681 lines
17 KiB
Bash
Executable File

#!/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 <<SH
set -e
printf 'services: '
systemctl is-active corrosion.service || true
printf 'coold: '
systemctl is-active coold.service || true
printf 'wireguard: '
sudo wg show wg0 >/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 <query>" >&2
exit 1
fi
corrosion_for_each_vm sql <<SH
sudo sqlite3 -header -column /var/lib/corrosion/corrosion.db $(printf '%q' "$query")
SH
}
corrosion() {
local command="${1:-help}"
if [ $# -gt 0 ]; then
shift
fi
case "$command" in
check)
corrosion_check
;;
containers|registered-containers)
corrosion_containers
;;
config)
corrosion_config
;;
logs)
corrosion_logs "${1:-1}"
;;
sql)
corrosion_sql "$@"
;;
-h|--help|help)
cat <<'USAGE'
Usage: scripts/dev.sh corrosion <command>
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 <query> 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 <<SH
set -e
sudo podman rm -f ${name} >/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 <<SH
sudo podman rm -f ${name} >/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 <command>
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 <command>
Commands:
allow <src> <dst> [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 <<SH
set -e
token="\$(sudo cat /etc/coolify/api-token)"
curl_args=(-fsS -X "${method}" "http://${api_ip}:8443${path}" -H "Authorization: Bearer \${token}")
if [ -n '${body}' ]; then
curl_args+=(-H 'Content-Type: application/json' -d '${body}')
fi
curl "\${curl_args[@]}"
echo
SH
done
}
firewall_allow() {
local src="${1:-}"
local dst="${2:-}"
local proto="${3:-}"
local port="${4:-}"
local body
if [ -z "$src" ] || [ -z "$dst" ]; then
firewall_help >&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 <id>"
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 <command> [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 <command> Inspect Corrosion state, config, logs, and registered containers
firewall <command> Manage dev coold firewall allow rules
example-nginx <command> 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