mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 08:23:25 +00:00
Move V5 source, migrations, UI, scripts, and tests into documentation, then remove V5 routes, models, jobs, configuration, dependencies, and application hooks.
1192 lines
32 KiB
Plaintext
1192 lines
32 KiB
Plaintext
#!/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_dns_name() {
|
|
local index="$1"
|
|
printf '%s.local\n' "$(coold_vm_instance "$index")"
|
|
}
|
|
|
|
coold_asset_arch() {
|
|
case "$(uname -m)" in
|
|
arm64|aarch64)
|
|
printf 'arm64\n'
|
|
;;
|
|
x86_64|amd64)
|
|
printf 'amd64\n'
|
|
;;
|
|
*)
|
|
echo "ERROR: Unsupported coold asset architecture: $(uname -m)" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
resolve_coold_asset_checksum() {
|
|
local version="$1"
|
|
local asset="$2"
|
|
local arch
|
|
local url
|
|
|
|
arch="$(coold_asset_arch)"
|
|
url="https://github.com/coollabsio/coold/releases/download/${version}/${asset}-linux-musl-${arch}.tar.gz.sha256"
|
|
|
|
curl -fsSL --retry 3 --max-time 30 "$url" | awk '{ print $1; exit }'
|
|
}
|
|
|
|
prepare_coold_asset_cache_bust() {
|
|
local flux_version
|
|
local cli_version
|
|
local cache_dir="$ROOT/.dev/coold-assets"
|
|
local cache_file="$cache_dir/docker-cache-key"
|
|
local previous_key=""
|
|
local current_key
|
|
local resolved_checksum
|
|
|
|
flux_version="$(read_coolify_env COOLIFY_FLUX_VERSION nightly)"
|
|
cli_version="$(read_coolify_env COOLIFY_CLI_VERSION nightly)"
|
|
|
|
if [ -z "${COOLIFY_FLUX_CHECKSUM:-}" ]; then
|
|
if resolved_checksum="$(resolve_coold_asset_checksum "$flux_version" flux 2>/dev/null)"; then
|
|
export COOLIFY_FLUX_CHECKSUM="$resolved_checksum"
|
|
else
|
|
echo "WARN: Could not resolve Flux checksum for ${flux_version}; Docker may reuse a cached Flux layer." >&2
|
|
export COOLIFY_FLUX_CHECKSUM=unknown
|
|
fi
|
|
fi
|
|
|
|
if [ -z "${COOLIFY_CLI_CHECKSUM:-}" ]; then
|
|
if resolved_checksum="$(resolve_coold_asset_checksum "$cli_version" coolify 2>/dev/null)"; then
|
|
export COOLIFY_CLI_CHECKSUM="$resolved_checksum"
|
|
else
|
|
echo "WARN: Could not resolve Coolify CLI checksum for ${cli_version}; Docker may reuse a cached CLI layer." >&2
|
|
export COOLIFY_CLI_CHECKSUM=unknown
|
|
fi
|
|
fi
|
|
|
|
current_key="flux=${flux_version}:${COOLIFY_FLUX_CHECKSUM};cli=${cli_version}:${COOLIFY_CLI_CHECKSUM}"
|
|
|
|
if [ -f "$cache_file" ]; then
|
|
previous_key="$(cat "$cache_file")"
|
|
fi
|
|
|
|
mkdir -p "$cache_dir"
|
|
printf '%s\n' "$current_key" > "$cache_file"
|
|
|
|
if [ "$previous_key" != "$current_key" ]; then
|
|
COOLD_ASSETS_CHANGED=changed
|
|
return
|
|
fi
|
|
|
|
COOLD_ASSETS_CHANGED=unchanged
|
|
}
|
|
|
|
resolve_lima_dns_name() {
|
|
local name="$1"
|
|
local ip=""
|
|
|
|
if command -v dscacheutil >/dev/null 2>&1; then
|
|
ip="$(dscacheutil -q host -a name "$name" 2>/dev/null | awk '/ip_address:/ && $2 ~ /^[0-9.]+$/ { print $2; exit }')"
|
|
fi
|
|
|
|
if [ -z "$ip" ] && command -v getent >/dev/null 2>&1; then
|
|
ip="$(getent ahostsv4 "$name" 2>/dev/null | awk '{ print $1; exit }')"
|
|
fi
|
|
|
|
if [ -z "$ip" ]; then
|
|
ip="$(ping -c 1 -W 1 "$name" 2>/dev/null | sed -n 's/^PING .* (\([0-9.]*\)).*$/\1/p' | head -n 1)"
|
|
fi
|
|
|
|
if [ -z "$ip" ]; then
|
|
echo "ERROR: Could not resolve ${name} from the host." >&2
|
|
echo "Check that the Lima VM is running, bridged networking is enabled, and Avahi/mDNS finished provisioning." >&2
|
|
return 1
|
|
fi
|
|
|
|
printf '%s\n' "$ip"
|
|
}
|
|
|
|
sync_lima_hosts_into_coolify_container() {
|
|
local count
|
|
local hosts_file="$ROOT/.dev/lima/hosts"
|
|
local name
|
|
local ip
|
|
|
|
count="$(coold_vm_count)"
|
|
mkdir -p "$(dirname "$hosts_file")"
|
|
: > "$hosts_file"
|
|
|
|
for index in $(seq 1 "$count"); do
|
|
name="$(coold_vm_dns_name "$index")"
|
|
ip="$(resolve_lima_dns_name "$name")" || return 1
|
|
printf '%s %s\n' "$ip" "$name" >> "$hosts_file"
|
|
done
|
|
|
|
echo "==> Syncing Lima .local host records into the Coolify container..."
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T -u root coolify sh -lc '
|
|
set -e
|
|
records=/tmp/coolify-lima-hosts
|
|
next=/tmp/coolify-hosts-next
|
|
cat > "$records"
|
|
cp /etc/hosts "$next"
|
|
while read -r ip name; do
|
|
if [ -z "$ip" ] || [ -z "$name" ]; then
|
|
continue
|
|
fi
|
|
|
|
awk -v name="$name" '\''$0 !~ ("(^|[[:space:]])" name "([[:space:]]|$)") { print }'\'' "$next" > "${next}.filtered"
|
|
printf "%s %s\n" "$ip" "$name" >> "${next}.filtered"
|
|
mv "${next}.filtered" "$next"
|
|
done < "$records"
|
|
cat "$next" > /etc/hosts
|
|
rm -f "$records" "$next" "${next}.filtered"
|
|
' < "$hosts_file"
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
coolify_cli_bin() {
|
|
printf '%s\n' '/usr/local/bin/coolify'
|
|
}
|
|
|
|
lima_ssh_target() {
|
|
local index="$1"
|
|
coold_vm_dns_name "$index"
|
|
}
|
|
|
|
coolify_nodes_arg() {
|
|
local count
|
|
local nodes=""
|
|
local node
|
|
count="$(coold_vm_count)"
|
|
|
|
for index in $(seq 1 "$count"); do
|
|
node="$(lima_ssh_target "$index")" || return 1
|
|
if [ -n "$nodes" ]; then
|
|
nodes="${nodes},"
|
|
fi
|
|
nodes="${nodes}${node}"
|
|
done
|
|
|
|
printf '%s\n' "$nodes"
|
|
}
|
|
|
|
coolify_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"
|
|
}
|
|
|
|
coolify_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}=$(coold_vm_dns_name "$index"):$(coold_vm_wg_port "$index")"
|
|
done
|
|
|
|
printf '%s\n' "$overrides"
|
|
}
|
|
|
|
coolify_ssh_key() {
|
|
read_coolify_env COOLIFY_CLI_SSH_KEY "/var/www/html/.dev/lima/ssh_key"
|
|
}
|
|
|
|
coolify_host_ssh_key_source() {
|
|
read_coolify_env COOLIFY_CLI_HOST_SSH_KEY "$HOME/.lima/_config/user"
|
|
}
|
|
|
|
ensure_coolify_container_ssh_key() {
|
|
local source_key
|
|
local target_key="$ROOT/.dev/lima/ssh_key"
|
|
|
|
source_key="$(coolify_host_ssh_key_source)"
|
|
if [ ! -f "$source_key" ]; then
|
|
echo "ERROR: SSH key for dev Lima VMs was not found at ${source_key}." >&2
|
|
echo "Set COOLIFY_CLI_HOST_SSH_KEY to the host-side private key that Lima authorized." >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$target_key")"
|
|
install -m 0600 "$source_key" "$target_key"
|
|
}
|
|
|
|
coolify_ssh_user() {
|
|
read_coolify_env COOLIFY_CLI_SSH_USER coolify
|
|
}
|
|
|
|
coolify_bootstrap_concurrency() {
|
|
read_coolify_env COOLIFY_COOLD_BOOTSTRAP_CONCURRENCY 1
|
|
}
|
|
|
|
coolify_bootstrap_ssh_timeout() {
|
|
read_coolify_env COOLIFY_COOLD_BOOTSTRAP_SSH_TIMEOUT 90s
|
|
}
|
|
|
|
coolify_bootstrap_command() {
|
|
local nodes
|
|
local listen_overrides
|
|
local endpoint_overrides
|
|
|
|
nodes="$(coolify_nodes_arg)" || return 1
|
|
listen_overrides="$(coolify_wg_listen_overrides_arg)" || return 1
|
|
endpoint_overrides="$(coolify_wg_endpoint_overrides_arg)" || return 1
|
|
|
|
cat <<CMD
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify $(coolify_cli_bin) init bootstrap \\
|
|
--nodes "${nodes}" \\
|
|
--ssh-key "$(coolify_ssh_key)" \\
|
|
--ssh-user "$(coolify_ssh_user)" \\
|
|
--concurrency "$(coolify_bootstrap_concurrency)" \\
|
|
--ssh-timeout "$(coolify_bootstrap_ssh_timeout)" \\
|
|
--debug \\
|
|
--wg-listen-port-overrides "${listen_overrides}" \\
|
|
--wg-endpoint-overrides "${endpoint_overrides}" \\
|
|
--coold-version "$(read_coolify_env COOLIFY_COOLD_VERSION nightly)" \\
|
|
--corrosion-version "$(read_coolify_env COOLIFY_CORROSION_VERSION v1.0.0)" \\
|
|
--yes
|
|
CMD
|
|
}
|
|
|
|
coolify_bootstrap() {
|
|
local nodes
|
|
local listen_overrides
|
|
local endpoint_overrides
|
|
ensure_coolify_container_ssh_key
|
|
|
|
nodes="$(coolify_nodes_arg)" || return 1
|
|
listen_overrides="$(coolify_wg_listen_overrides_arg)" || return 1
|
|
endpoint_overrides="$(coolify_wg_endpoint_overrides_arg)" || return 1
|
|
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify "$(coolify_cli_bin)" init bootstrap \
|
|
--nodes "$nodes" \
|
|
--ssh-key "$(coolify_ssh_key)" \
|
|
--ssh-user "$(coolify_ssh_user)" \
|
|
--concurrency "$(coolify_bootstrap_concurrency)" \
|
|
--ssh-timeout "$(coolify_bootstrap_ssh_timeout)" \
|
|
--debug \
|
|
--wg-listen-port-overrides "$listen_overrides" \
|
|
--wg-endpoint-overrides "$endpoint_overrides" \
|
|
--coold-version "$(read_coolify_env COOLIFY_COOLD_VERSION nightly)" \
|
|
--corrosion-version "$(read_coolify_env COOLIFY_CORROSION_VERSION v1.0.0)" \
|
|
--yes
|
|
}
|
|
|
|
diagnose_coold_bootstrap_failure() {
|
|
local count
|
|
local instance
|
|
count="$(coold_vm_count)"
|
|
|
|
echo "==> Bootstrap failed; collecting quick VM diagnostics..." >&2
|
|
for index in $(seq 1 "$count"); do
|
|
instance="$(coold_vm_instance "$index")"
|
|
echo "--- ${instance}: diagnostics ---" >&2
|
|
run_with_timeout 45 env COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell <<'SH' >&2 || true
|
|
set +e
|
|
echo '[cloud-init]'
|
|
cloud-init status 2>/dev/null || true
|
|
echo '[systemd]'
|
|
systemctl is-system-running 2>/dev/null || true
|
|
echo '[apt/dpkg locks]'
|
|
for lock in /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock; do
|
|
fuser "$lock" 2>/dev/null && echo "busy: $lock" || true
|
|
done
|
|
echo '[failed units]'
|
|
systemctl --failed --no-pager 2>/dev/null || true
|
|
echo '[coolify services]'
|
|
systemctl --no-pager --full status wg-quick@wg0 podman.socket corrosion coold 2>/dev/null | tail -n 120 || true
|
|
echo '[disk]'
|
|
df -h / /var 2>/dev/null || true
|
|
SH
|
|
done
|
|
}
|
|
|
|
coolify_bootstrap_with_retry() {
|
|
local attempt
|
|
local attempts=5
|
|
|
|
for attempt in $(seq 1 "$attempts"); do
|
|
if coolify_bootstrap; then
|
|
return
|
|
fi
|
|
|
|
diagnose_coold_bootstrap_failure
|
|
|
|
if [ "$attempt" = "$attempts" ]; then
|
|
return 1
|
|
fi
|
|
|
|
echo "WARN: coolify bootstrap failed on attempt ${attempt}/${attempts}; retrying because fresh Lima hosts can finish setup after partial bootstrap phases..." >&2
|
|
sleep 3
|
|
done
|
|
}
|
|
|
|
coolify_dev() {
|
|
local command="${1:-help}"
|
|
if [ $# -gt 0 ]; then
|
|
shift
|
|
fi
|
|
|
|
case "$command" in
|
|
install)
|
|
echo "==> coolify CLI is provided by the Coolify dev container."
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify "$(coolify_cli_bin)" --version
|
|
;;
|
|
path)
|
|
coolify_cli_bin
|
|
;;
|
|
bootstrap-command)
|
|
coolify_bootstrap_command
|
|
;;
|
|
run)
|
|
exec docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify "$(coolify_cli_bin)" "$@"
|
|
;;
|
|
-h|--help|help)
|
|
cat <<'USAGE'
|
|
Usage: scripts/dev.sh coolify <command>
|
|
|
|
Commands:
|
|
install Print the coolify CLI version from the dev container
|
|
path Print the coolify CLI path inside the dev container
|
|
bootstrap-command Print the dev Lima bootstrap command without running it
|
|
run <args> Run coolify with arbitrary args
|
|
|
|
Example:
|
|
scripts/dev.sh coolify bootstrap-command
|
|
USAGE
|
|
;;
|
|
*)
|
|
echo "unknown coolify command: $command" >&2
|
|
echo "Run: scripts/dev.sh coolify 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 "$@"
|
|
}
|
|
|
|
coold_vm_shell() {
|
|
local instance="${1:-}"
|
|
|
|
if [ -z "$instance" ]; then
|
|
instance="$(coold_vm_instance 1)"
|
|
fi
|
|
|
|
if [[ "$instance" =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: Use the Lima hostname, not a numeric VM index." >&2
|
|
echo "Example: scripts/dev.sh shell $(coold_vm_instance 1)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell
|
|
}
|
|
|
|
coold_vm_up_with_retry() {
|
|
local index="$1"
|
|
local attempt
|
|
local attempts=2
|
|
local instance
|
|
|
|
instance="$(coold_vm_instance "$index")"
|
|
|
|
for attempt in $(seq 1 "$attempts"); do
|
|
if coold_vm "$index" up; then
|
|
return
|
|
fi
|
|
|
|
if [ "$attempt" = "$attempts" ]; then
|
|
echo "ERROR: ${instance} did not become ready after ${attempts} attempts." >&2
|
|
return 1
|
|
fi
|
|
|
|
echo "WARN: ${instance} did not become ready; deleting and retrying with a fresh Lima instance..." >&2
|
|
limactl stop --force --tty=false "$instance" >/dev/null 2>&1 || true
|
|
cleanup_lima_instance_processes "$instance"
|
|
limactl delete --force --tty=false "$instance" >/dev/null 2>&1 || true
|
|
cleanup_lima_instance_processes "$instance"
|
|
done
|
|
}
|
|
|
|
mint_host_jwt_for_host() {
|
|
local host_id="$1"
|
|
local attempts=60
|
|
local output
|
|
|
|
for attempt in $(seq 1 "$attempts"); do
|
|
if output="$(docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify php artisan flux:dev "$host_id" 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
|
|
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f
|
|
}
|
|
|
|
sync_v5_dev_lima_servers() {
|
|
local count
|
|
local index
|
|
local instance
|
|
local server_args=()
|
|
local ssh_port
|
|
local ssh_user
|
|
|
|
count="$(coold_vm_count)"
|
|
ssh_user="$(coolify_ssh_user)"
|
|
|
|
for index in $(seq 1 "$count"); do
|
|
instance="$(coold_vm_instance "$index")"
|
|
server_args+=(--server="${instance}|$(coold_vm_dns_name "$index")|${ssh_user}|22|$(coold_vm_wg_ip "$index")")
|
|
done
|
|
|
|
echo "==> Running pending migrations before syncing v5 dev Lima state..."
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify php artisan migrate --force
|
|
|
|
echo "==> Seeding dev Lima VM(s) into v5 clusters/servers..."
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T \
|
|
-e COOLIFY_CLI_SSH_USER="$ssh_user" \
|
|
coolify php artisan v5:sync-dev-lima-servers \
|
|
"${server_args[@]}"
|
|
}
|
|
|
|
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 <<SH
|
|
set -e
|
|
sudo install -d -m 0755 /etc/systemd/system/coold.service.d
|
|
sudo tee /etc/systemd/system/coold.service.d/10-flux-dev.conf >/dev/null <<UNIT
|
|
[Service]
|
|
Environment=COOLIFY_COOLD_FLUX_URL=${flux_url}
|
|
Environment=COOLIFY_COOLD_HOST_JWT_PATH=/etc/coolify/host-jwt
|
|
UNIT
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl restart coold.service
|
|
SH
|
|
}
|
|
|
|
up() {
|
|
local coold_vm_enabled
|
|
local follow_dev_logs
|
|
local count
|
|
local naked=false
|
|
local compose_args=()
|
|
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)"
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--naked)
|
|
naked=true
|
|
shift
|
|
;;
|
|
*)
|
|
compose_args+=("$1")
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ "$coold_vm_enabled" != "false" ]; then
|
|
echo "==> Starting ${count} Coolify coold VM(s) before Docker Compose..."
|
|
for index in $(seq 1 "$count"); do
|
|
coold_vm_up_with_retry "$index"
|
|
done
|
|
else
|
|
echo "==> COOLIFY_COOLD_VM_ENABLED=false; skipping coold VM."
|
|
fi
|
|
|
|
echo "==> Starting Coolify Docker stack with Docker Compose..."
|
|
prepare_coold_asset_cache_bust
|
|
if [ "$COOLD_ASSETS_CHANGED" = "changed" ]; then
|
|
echo "==> Coold nightly assets changed; rebuilding the Coolify dev image..."
|
|
COOLIFY_CLI_SSH_USER="$(coolify_ssh_user)" docker compose -f docker-compose.yml -f docker-compose.dev.yml build coolify
|
|
fi
|
|
|
|
if [ "${#compose_args[@]}" -gt 0 ]; then
|
|
COOLIFY_CLI_SSH_USER="$(coolify_ssh_user)" docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d "${compose_args[@]}"
|
|
else
|
|
COOLIFY_CLI_SSH_USER="$(coolify_ssh_user)" docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
|
fi
|
|
|
|
if [ "$coold_vm_enabled" != "false" ]; then
|
|
sync_lima_hosts_into_coolify_container
|
|
fi
|
|
|
|
if [ "$naked" = "true" ]; then
|
|
echo "==> --naked enabled. Skipping coolify bootstrap and Flux VM wiring. Use /v5 to bootstrap hosts from the UI."
|
|
return
|
|
fi
|
|
|
|
if [ "$coold_vm_enabled" != "false" ]; then
|
|
echo "==> Bootstrapping coold VM mesh with coolify..."
|
|
coolify_bootstrap_with_retry
|
|
|
|
for index in $(seq 1 "$count"); do
|
|
configure_flux_dev_for_vm "$index"
|
|
done
|
|
|
|
sync_v5_dev_lima_servers
|
|
fi
|
|
|
|
if [ "$follow_dev_logs" = "false" ]; then
|
|
echo "==> Dev environment is ready. Use 'docker compose -f docker-compose.yml -f docker-compose.dev.yml 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
|
|
local cleanup=false
|
|
local compose_args=()
|
|
coold_vm_enabled="$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)"
|
|
stop_coold_vm="$(read_coolify_env COOLIFY_COOLD_VM_STOP_ON_DOWN false)"
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--cleanup)
|
|
cleanup=true
|
|
shift
|
|
;;
|
|
*)
|
|
compose_args+=("$1")
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
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 Docker Compose..."
|
|
if [ "${#compose_args[@]}" -gt 0 ]; then
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml down "${compose_args[@]}"
|
|
else
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml down
|
|
fi
|
|
|
|
if [ "$cleanup" = "true" ]; then
|
|
clean_vms
|
|
return
|
|
fi
|
|
|
|
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
|
|
}
|
|
|
|
kill_matching_processes() {
|
|
local pattern="$1"
|
|
local pids
|
|
|
|
pids="$(pgrep -f "$pattern" 2>/dev/null || true)"
|
|
if [ -z "$pids" ]; then
|
|
return
|
|
fi
|
|
|
|
kill $pids >/dev/null 2>&1 || true
|
|
sleep 1
|
|
kill -9 $pids >/dev/null 2>&1 || true
|
|
}
|
|
|
|
cleanup_lima_instance_processes() {
|
|
local instance="$1"
|
|
|
|
kill_matching_processes "limactl hostagent .*${instance}"
|
|
kill_matching_processes "ssh: .*/.lima/${instance}/ssh.sock"
|
|
kill_matching_processes "ssh .*ControlPath=.*${instance}/ssh.sock"
|
|
rm -f "$HOME/.lima/${instance}/ssh.sock" "$HOME/.lima/${instance}/ha.sock"
|
|
}
|
|
|
|
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
|
|
cleanup_lima_instance_processes "$instance"
|
|
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
|
|
cleanup_lima_instance_processes "$instance"
|
|
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 <<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_require_pair() {
|
|
if [ "$(coold_vm_count)" = "1" ]; then
|
|
echo "ERROR: example-nginx ping command requires COOLIFY_COOLD_VM_COUNT=2." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
example_nginx_container_ip() {
|
|
local index="$1"
|
|
local name
|
|
name="$(example_nginx_name "$index")"
|
|
|
|
COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance "$index")" scripts/coold-vm.sh shell <<SH
|
|
set -e
|
|
sudo podman inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${name}
|
|
SH
|
|
}
|
|
|
|
example_nginx_ping() {
|
|
example_nginx_require_pair
|
|
|
|
COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance 1)" scripts/coold-vm.sh shell <<'SH'
|
|
set -e
|
|
sudo podman exec coolify-example-nginx wget -qO- --timeout=3 http://coolify-example-nginx-2.default.coolify.internal/ >/dev/null
|
|
echo 'ok: coolify-example-nginx can reach coolify-example-nginx-2 on tcp/80'
|
|
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
|
|
ping Verify host 1 nginx can reach host 2 nginx on tcp/80
|
|
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
|
|
;;
|
|
ping)
|
|
example_nginx_ping
|
|
;;
|
|
-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
|
|
}
|
|
|
|
|
|
refresh_test_host_key() {
|
|
echo "==> Refreshing /tmp/testhostkey inside coolify..."
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify php artisan tinker --execute='file_put_contents("/tmp/testhostkey", \App\Models\PrivateKey::query()->where("name", "Testing Host Key")->sole()->private_key); chmod("/tmp/testhostkey", 0600);'
|
|
}
|
|
|
|
recreate_naked_lima_vm() {
|
|
local instance="${1:-coolify-naked-test}"
|
|
local config="${2:-.dev/lima/coolify-naked-test.yaml}"
|
|
local attempt
|
|
local attempts=2
|
|
local start_timeout
|
|
|
|
start_timeout="$(read_coolify_env COOLIFY_NAKED_VM_START_TIMEOUT "$(read_coolify_env COOLIFY_COOLD_VM_START_TIMEOUT 300)")"
|
|
|
|
if [ ! -f "$config" ]; then
|
|
echo "ERROR: naked Lima config not found: ${config}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> Recreating naked Lima VM: ${instance}..."
|
|
for attempt in $(seq 1 "$attempts"); do
|
|
limactl stop --force --tty=false "$instance" >/dev/null 2>&1 || true
|
|
cleanup_lima_instance_processes "$instance"
|
|
limactl delete --force --tty=false "$instance" >/dev/null 2>&1 || true
|
|
cleanup_lima_instance_processes "$instance"
|
|
|
|
if run_with_timeout "$start_timeout" limactl start --tty=false --name="$instance" "$config"; then
|
|
return
|
|
fi
|
|
|
|
echo "WARN: ${instance} did not become ready on attempt ${attempt}/${attempts}; deleting and retrying..." >&2
|
|
done
|
|
|
|
echo "ERROR: ${instance} did not become ready after ${attempts} attempts." >&2
|
|
return 1
|
|
}
|
|
|
|
fresh() {
|
|
echo "==> Recreating coold Lima VMs and Coolify dev stack..."
|
|
down --cleanup
|
|
COOLIFY_DEV_FOLLOW_LOGS=false up
|
|
|
|
echo "==> Refreshing Coolify database with seed data..."
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify php artisan migrate:fresh --seed --force
|
|
|
|
if [ "$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)" != "false" ]; then
|
|
echo "==> Re-syncing seeded v5 Lima servers after DB refresh..."
|
|
sync_v5_dev_lima_servers
|
|
fi
|
|
|
|
recreate_naked_lima_vm
|
|
refresh_test_host_key
|
|
|
|
echo "==> Restarting Horizon so workers use the latest code..."
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec -T coolify php artisan horizon:terminate || true
|
|
|
|
echo "==> Fresh dev environment is ready."
|
|
limactl list | grep -E 'NAME|coold-dev|coolify-naked-test' || true
|
|
}
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: scripts/dev.sh <command> [docker compose -f docker-compose.yml -f docker-compose.dev.yml args]
|
|
|
|
Commands:
|
|
up Start the coold VM, Docker Compose stack, and dev coold agent
|
|
fresh Recreate coold/naked Lima VMs, refresh DB, seed, sync v5 dev servers
|
|
up --naked
|
|
Start the coold VM(s) and Docker Compose stack only; skip host bootstrap so /v5 can bootstrap
|
|
down Stop the dev coold agent and Docker Compose stack
|
|
down --cleanup
|
|
Stop the dev stack, then delete the coold Lima VM(s) and VM-local state
|
|
shell [hostname]
|
|
Open a shell inside a coold VM by Lima hostname (default: coold-dev)
|
|
list Show Lima instances
|
|
clean-vms Delete the coold Lima VMs and all VM-local runtime state (alias for down --cleanup)
|
|
naked-vm Recreate the naked Lima VM used for bootstrap testing
|
|
corrosion <command> Inspect Corrosion state, config, logs, and registered containers
|
|
example-nginx <command> Start/check example nginx containers with coold DNS
|
|
coolify <command> Install/run the released coolify dev helper
|
|
USAGE
|
|
}
|
|
|
|
cmd="${1:-}"
|
|
if [ $# -gt 0 ]; then
|
|
shift
|
|
fi
|
|
|
|
case "$cmd" in
|
|
up)
|
|
up "$@"
|
|
;;
|
|
down)
|
|
down "$@"
|
|
;;
|
|
fresh)
|
|
fresh
|
|
;;
|
|
shell)
|
|
coold_vm_shell "${1:-}"
|
|
;;
|
|
list)
|
|
limactl list
|
|
;;
|
|
clean-vms|clean-vm|reset-vms)
|
|
down --cleanup
|
|
;;
|
|
naked-vm)
|
|
recreate_naked_lima_vm
|
|
;;
|
|
corrosion)
|
|
corrosion "$@"
|
|
;;
|
|
example-nginx)
|
|
example_nginx "$@"
|
|
;;
|
|
coolify)
|
|
coolify_dev "$@"
|
|
;;
|
|
-h|--help|help|"")
|
|
usage
|
|
;;
|
|
*)
|
|
echo "unknown command: $cmd" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|