Add panel-api and panelctl scripts for container management
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
commit
8986399a92
4 changed files with 930 additions and 0 deletions
335
panelctl.sh
Normal file
335
panelctl.sh
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_DIR="${PANEL_BASE_DIR:-/home/reudy/containers}"
|
||||
STACKS_DIR="${BASE_DIR}/stacks"
|
||||
VOLUMES_DIR="${BASE_DIR}/volumes"
|
||||
ROUTES_DIR="${BASE_DIR}/routes"
|
||||
STATE_DIR="${BASE_DIR}/state"
|
||||
APPS_DIR="${STATE_DIR}/apps"
|
||||
|
||||
FORWARD_AUTH_BLOCK=' forward_auth 127.0.0.1:9091 {
|
||||
uri /api/authz/forward-auth
|
||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||
}
|
||||
'
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
panelctl - minimal app panel helper
|
||||
|
||||
Usage:
|
||||
panelctl init <name> <domain> <port> [auth]
|
||||
panelctl render-route <name>
|
||||
panelctl deploy <name>
|
||||
panelctl stop <name>
|
||||
panelctl remove <name> [--keep-volumes]
|
||||
panelctl list
|
||||
panelctl show <name>
|
||||
|
||||
Examples:
|
||||
panelctl init whoami whoami.srazka.com 18080 true
|
||||
panelctl deploy whoami
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ensure_base_dirs() {
|
||||
mkdir -p "${STACKS_DIR}" "${VOLUMES_DIR}" "${ROUTES_DIR}" "${APPS_DIR}"
|
||||
}
|
||||
|
||||
validate_name() {
|
||||
local name="$1"
|
||||
[[ "${name}" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || fail "invalid name '${name}' (use lowercase slug)"
|
||||
}
|
||||
|
||||
validate_domain() {
|
||||
local domain="$1"
|
||||
[[ "${domain}" =~ ^[A-Za-z0-9.-]+$ ]] || fail "invalid domain '${domain}'"
|
||||
[[ "${domain}" == *.* ]] || fail "domain must include a dot"
|
||||
}
|
||||
|
||||
validate_port() {
|
||||
local port="$1"
|
||||
[[ "${port}" =~ ^[0-9]+$ ]] || fail "port must be numeric"
|
||||
(( port >= 1024 && port <= 65535 )) || fail "port must be in range 1024-65535"
|
||||
}
|
||||
|
||||
app_manifest() {
|
||||
local name="$1"
|
||||
echo "${APPS_DIR}/${name}.env"
|
||||
}
|
||||
|
||||
app_stack_dir() {
|
||||
local name="$1"
|
||||
echo "${STACKS_DIR}/${name}"
|
||||
}
|
||||
|
||||
app_volume_dir() {
|
||||
local name="$1"
|
||||
echo "${VOLUMES_DIR}/${name}"
|
||||
}
|
||||
|
||||
app_route_file() {
|
||||
local name="$1"
|
||||
echo "${ROUTES_DIR}/${name}.caddy"
|
||||
}
|
||||
|
||||
load_app() {
|
||||
local name="$1"
|
||||
local manifest
|
||||
manifest="$(app_manifest "${name}")"
|
||||
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
|
||||
# shellcheck disable=SC1090
|
||||
source "${manifest}"
|
||||
}
|
||||
|
||||
compose_command() {
|
||||
if podman compose version >/dev/null 2>&1; then
|
||||
echo "podman compose"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v podman-compose >/dev/null 2>&1; then
|
||||
echo "podman-compose"
|
||||
return
|
||||
fi
|
||||
|
||||
fail "no compose command available (need 'podman compose' or 'podman-compose')"
|
||||
}
|
||||
|
||||
write_default_compose() {
|
||||
local name="$1"
|
||||
local port="$2"
|
||||
local stack_dir
|
||||
local volume_dir
|
||||
stack_dir="$(app_stack_dir "${name}")"
|
||||
volume_dir="$(app_volume_dir "${name}")"
|
||||
|
||||
cat >"${stack_dir}/compose.yaml" <<EOF
|
||||
services:
|
||||
app:
|
||||
image: traefik/whoami:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:${port}:80"
|
||||
volumes:
|
||||
- ${volume_dir}/data:/data
|
||||
EOF
|
||||
}
|
||||
|
||||
write_manifest() {
|
||||
local name="$1"
|
||||
local domain="$2"
|
||||
local port="$3"
|
||||
local auth="$4"
|
||||
local manifest
|
||||
local stack_dir
|
||||
local volume_dir
|
||||
local route_file
|
||||
|
||||
manifest="$(app_manifest "${name}")"
|
||||
stack_dir="$(app_stack_dir "${name}")"
|
||||
volume_dir="$(app_volume_dir "${name}")"
|
||||
route_file="$(app_route_file "${name}")"
|
||||
|
||||
cat >"${manifest}" <<EOF
|
||||
APP_NAME="${name}"
|
||||
APP_DOMAIN="${domain}"
|
||||
APP_PORT="${port}"
|
||||
APP_UPSTREAM="127.0.0.1:${port}"
|
||||
APP_AUTH_PROTECTED="${auth}"
|
||||
APP_STACK_DIR="${stack_dir}"
|
||||
APP_COMPOSE_FILE="${stack_dir}/compose.yaml"
|
||||
APP_VOLUME_DIR="${volume_dir}"
|
||||
APP_ROUTE_FILE="${route_file}"
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd_init() {
|
||||
local name="$1"
|
||||
local domain="$2"
|
||||
local port="$3"
|
||||
local auth="${4:-true}"
|
||||
|
||||
validate_name "${name}"
|
||||
validate_domain "${domain}"
|
||||
validate_port "${port}"
|
||||
[[ "${auth}" == "true" || "${auth}" == "false" ]] || fail "auth must be true or false"
|
||||
|
||||
ensure_base_dirs
|
||||
|
||||
local manifest
|
||||
local stack_dir
|
||||
local volume_dir
|
||||
manifest="$(app_manifest "${name}")"
|
||||
stack_dir="$(app_stack_dir "${name}")"
|
||||
volume_dir="$(app_volume_dir "${name}")"
|
||||
|
||||
[[ ! -f "${manifest}" ]] || fail "app '${name}' already exists"
|
||||
|
||||
mkdir -p "${stack_dir}" "${volume_dir}/data"
|
||||
write_default_compose "${name}" "${port}"
|
||||
write_manifest "${name}" "${domain}" "${port}" "${auth}"
|
||||
cmd_render_route "${name}"
|
||||
|
||||
echo "initialized app '${name}'"
|
||||
}
|
||||
|
||||
cmd_render_route() {
|
||||
local name="$1"
|
||||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
local auth_block=""
|
||||
if [[ "${APP_AUTH_PROTECTED}" == "true" ]]; then
|
||||
auth_block="${FORWARD_AUTH_BLOCK}"
|
||||
fi
|
||||
|
||||
cat >"${APP_ROUTE_FILE}" <<EOF
|
||||
${APP_DOMAIN} {
|
||||
${auth_block} reverse_proxy ${APP_UPSTREAM}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "rendered route ${APP_ROUTE_FILE}"
|
||||
}
|
||||
|
||||
maybe_reload_caddy() {
|
||||
if [[ -x /run/current-system/sw/bin/caddy && -f /etc/caddy/Caddyfile ]]; then
|
||||
/run/current-system/sw/bin/caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
fi
|
||||
|
||||
if [[ "${EUID}" -eq 0 ]]; then
|
||||
systemctl reload caddy
|
||||
echo "reloaded caddy"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
|
||||
sudo -n systemctl reload caddy
|
||||
echo "reloaded caddy via sudo"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "caddy reload requires root; run: sudo systemctl reload caddy"
|
||||
}
|
||||
|
||||
cmd_deploy() {
|
||||
local name="$1"
|
||||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
local compose
|
||||
compose="$(compose_command)"
|
||||
|
||||
cmd_render_route "${name}"
|
||||
${compose} -f "${APP_COMPOSE_FILE}" up -d
|
||||
maybe_reload_caddy
|
||||
|
||||
echo "deployed app '${name}'"
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
local name="$1"
|
||||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
local compose
|
||||
compose="$(compose_command)"
|
||||
${compose} -f "${APP_COMPOSE_FILE}" down
|
||||
echo "stopped app '${name}'"
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
local name="$1"
|
||||
local keep_volumes="${2:-}"
|
||||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
local compose
|
||||
compose="$(compose_command)"
|
||||
${compose} -f "${APP_COMPOSE_FILE}" down || true
|
||||
|
||||
rm -f "${APP_ROUTE_FILE}" "$(app_manifest "${name}")"
|
||||
rm -rf "${APP_STACK_DIR}"
|
||||
|
||||
if [[ "${keep_volumes}" != "--keep-volumes" ]]; then
|
||||
rm -rf "${APP_VOLUME_DIR}"
|
||||
fi
|
||||
|
||||
maybe_reload_caddy
|
||||
echo "removed app '${name}'"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
ensure_base_dirs
|
||||
local found=0
|
||||
for mf in "${APPS_DIR}"/*.env; do
|
||||
[[ -e "${mf}" ]] || continue
|
||||
found=1
|
||||
# shellcheck disable=SC1090
|
||||
source "${mf}"
|
||||
echo "${APP_NAME} ${APP_DOMAIN} ${APP_UPSTREAM} auth=${APP_AUTH_PROTECTED}"
|
||||
done
|
||||
|
||||
if [[ "${found}" -eq 0 ]]; then
|
||||
echo "no apps found"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_show() {
|
||||
local name="$1"
|
||||
validate_name "${name}"
|
||||
local mf
|
||||
mf="$(app_manifest "${name}")"
|
||||
[[ -f "${mf}" ]] || fail "app '${name}' does not exist"
|
||||
cat "${mf}"
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
|
||||
case "${cmd}" in
|
||||
init)
|
||||
[[ $# -ge 4 ]] || fail "usage: panelctl init <name> <domain> <port> [auth]"
|
||||
cmd_init "$2" "$3" "$4" "${5:-true}"
|
||||
;;
|
||||
render-route)
|
||||
[[ $# -eq 2 ]] || fail "usage: panelctl render-route <name>"
|
||||
cmd_render_route "$2"
|
||||
;;
|
||||
deploy)
|
||||
[[ $# -eq 2 ]] || fail "usage: panelctl deploy <name>"
|
||||
cmd_deploy "$2"
|
||||
;;
|
||||
stop)
|
||||
[[ $# -eq 2 ]] || fail "usage: panelctl stop <name>"
|
||||
cmd_stop "$2"
|
||||
;;
|
||||
remove)
|
||||
[[ $# -ge 2 ]] || fail "usage: panelctl remove <name> [--keep-volumes]"
|
||||
cmd_remove "$2" "${3:-}"
|
||||
;;
|
||||
list)
|
||||
[[ $# -eq 1 ]] || fail "usage: panelctl list"
|
||||
cmd_list
|
||||
;;
|
||||
show)
|
||||
[[ $# -eq 2 ]] || fail "usage: panelctl show <name>"
|
||||
cmd_show "$2"
|
||||
;;
|
||||
""|-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
fail "unknown command '${cmd}'"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue