panel: Forgejo integration, ssh deploy key and per-app environment variables
Forgejo:
- panel.nix passes the local Forgejo's public, API and ssh URLs (derived
from forgejo.nix) to panel-api.
- Settings dialog: connect a Forgejo access token (verified against
/api/v1/user, stored 0600 in state/panel/forgejo-token).
- New-app dialog gets a Forgejo repository picker with search and a branch
dropdown; private repos are cloned over https with the stored token, or
over ssh with the deploy key when no token is connected. The app name and
domain are filled in from the repository name.
- Commit and compare links in the Source tab point at Forgejo; cards show
the provider ("Forgejo · main").
Git over ssh:
- ssh:// and git@host:owner/repo URLs are accepted; the panel generates an
ed25519 deploy key in state/panel/ssh and uses it for clone/fetch
(BatchMode, accept-new host keys). openssh added to the service path.
- Credential redaction only applies to http(s) URLs, so ssh usernames are
kept; git errors now report the meaningful line instead of git's advice.
Environment variables:
- Stored per app in state/env/<app>.env (0600), outside the repo and stack.
- panelctl passes them to every compose command via env(1), so ${VAR}
interpolation works; by default deploy/restart also generate a compose
override listing the keys under every service's environment (values are
read from compose's environment, never quoted into YAML).
- Environment tab (and a section in the new-app dialog) with .env paste
import, hidden values, validation of names (reserved podman/compose vars
rejected), hints for ${VAR}s the compose file uses but aren't set, and
Save / Save & deploy. Removing an app deletes its variables.
The API still accepts the old source_type "github" / github_* fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UbWSNkXxZhYf7eqHTyx3Bf
This commit is contained in:
parent
2d3b30d078
commit
db46c5e793
5 changed files with 1258 additions and 100 deletions
114
panelctl.sh
114
panelctl.sh
|
|
@ -7,8 +7,15 @@ VOLUMES_DIR="${BASE_DIR}/volumes"
|
|||
ROUTES_DIR="${BASE_DIR}/routes"
|
||||
STATE_DIR="${BASE_DIR}/state"
|
||||
APPS_DIR="${STATE_DIR}/apps"
|
||||
ENV_DIR="${STATE_DIR}/env"
|
||||
BACKUPS_DIR="${BASE_DIR}/backups"
|
||||
|
||||
# Set by load_app: compose file arguments, and the app's environment variables
|
||||
# as KEY=VALUE words (passed to compose via env(1), never sourced).
|
||||
COMPOSE_ARGS=()
|
||||
APP_ENV_ARGS=()
|
||||
APP_ENV_KEYS=()
|
||||
|
||||
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
|
||||
|
|
@ -186,6 +193,75 @@ load_app() {
|
|||
done
|
||||
APP_ROUTES="${routes}"
|
||||
fi
|
||||
|
||||
load_app_env "${name}"
|
||||
}
|
||||
|
||||
app_env_file() {
|
||||
echo "${ENV_DIR}/$1.env"
|
||||
}
|
||||
|
||||
# Generated compose override that passes the app's variables into every service.
|
||||
app_env_override() {
|
||||
echo "${STACKS_DIR}/$1/.panel-env.yaml"
|
||||
}
|
||||
|
||||
load_app_env() {
|
||||
local name="$1"
|
||||
local file line key override
|
||||
file="$(app_env_file "${name}")"
|
||||
APP_ENV_ARGS=()
|
||||
APP_ENV_KEYS=()
|
||||
if [[ -f "${file}" ]]; then
|
||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||
[[ -z "${line}" || "${line}" == \#* || "${line}" != *=* ]] && continue
|
||||
key="${line%%=*}"
|
||||
[[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
||||
APP_ENV_ARGS+=("${line}")
|
||||
APP_ENV_KEYS+=("${key}")
|
||||
done <"${file}"
|
||||
fi
|
||||
|
||||
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}")
|
||||
override="$(app_env_override "${name}")"
|
||||
if [[ -f "${override}" ]]; then
|
||||
COMPOSE_ARGS+=(-f "${override}")
|
||||
fi
|
||||
}
|
||||
|
||||
# (Re)generate the env override before containers are created. Variables are
|
||||
# always available for ${VAR} interpolation; with APP_ENV_INJECT (default true)
|
||||
# every service also receives them. Bare keys make compose read the values from
|
||||
# its own environment, so values never have to be quoted into YAML.
|
||||
prepare_env_override() {
|
||||
local name="$1"
|
||||
local override services svc key tmp
|
||||
override="$(app_env_override "${name}")"
|
||||
|
||||
if [[ ${#APP_ENV_KEYS[@]} -eq 0 || "${APP_ENV_INJECT:-true}" != "true" ]]; then
|
||||
rm -f "${override}"
|
||||
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}")
|
||||
return
|
||||
fi
|
||||
|
||||
services="$(run_compose -f "${APP_COMPOSE_FILE}" config --services 2>/dev/null)" \
|
||||
|| fail "could not list compose services to pass environment variables (is the compose file valid?)"
|
||||
|
||||
tmp="$(mktemp)"
|
||||
{
|
||||
echo "# Generated by panelctl from the app's environment variables. Do not edit."
|
||||
echo "services:"
|
||||
while IFS= read -r svc; do
|
||||
[[ "${svc}" =~ ^[A-Za-z0-9._-]+$ ]] || continue
|
||||
printf ' "%s":\n environment:\n' "${svc}"
|
||||
for key in "${APP_ENV_KEYS[@]}"; do
|
||||
printf ' - %s\n' "${key}"
|
||||
done
|
||||
done <<<"${services}"
|
||||
} >"${tmp}"
|
||||
install -m 0640 "${tmp}" "${override}"
|
||||
rm -f "${tmp}"
|
||||
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}" -f "${override}")
|
||||
}
|
||||
|
||||
compose_command() {
|
||||
|
|
@ -252,11 +328,11 @@ run_compose() {
|
|||
|
||||
if [[ "${compose}" == *" compose" ]]; then
|
||||
local podman_bin="${compose% compose}"
|
||||
"${podman_bin}" compose "$@"
|
||||
env "${APP_ENV_ARGS[@]}" "${podman_bin}" compose "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
"${compose}" "$@"
|
||||
env "${APP_ENV_ARGS[@]}" "${compose}" "$@"
|
||||
}
|
||||
|
||||
write_default_compose() {
|
||||
|
|
@ -404,11 +480,12 @@ cmd_deploy() {
|
|||
log info "Starting deployment for app '${name}'"
|
||||
|
||||
cmd_render_route "${name}"
|
||||
prepare_env_override "${name}"
|
||||
|
||||
# Capture compose output so callers (the web UI) can show why a deploy failed,
|
||||
# and still forward it to the journal.
|
||||
local output
|
||||
if ! output="$(run_compose -f "${APP_COMPOSE_FILE}" up -d --build --remove-orphans 2>&1)"; then
|
||||
if ! output="$(run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1)"; then
|
||||
printf '%s\n' "${output}" | systemd-cat -t panelctl -p err 2>/dev/null || true
|
||||
printf '%s\n' "${output}" >&2
|
||||
log err "Deployment failed for app '${name}'"
|
||||
|
|
@ -427,9 +504,10 @@ cmd_restart() {
|
|||
|
||||
log info "Restarting app '${name}'"
|
||||
|
||||
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans || fail "compose down failed"
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed"
|
||||
prepare_env_override "${name}"
|
||||
|
||||
if ! run_compose -f "${APP_COMPOSE_FILE}" up -d --build --remove-orphans 2>&1; then
|
||||
if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1; then
|
||||
fail "compose up failed during restart"
|
||||
fi
|
||||
|
||||
|
|
@ -441,7 +519,7 @@ cmd_stop() {
|
|||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans || fail "compose down failed"
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed"
|
||||
log info "stopped app '${name}'"
|
||||
}
|
||||
|
||||
|
|
@ -450,8 +528,8 @@ cmd_status() {
|
|||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
run_compose -f "${APP_COMPOSE_FILE}" ps --format json 2>/dev/null || \
|
||||
run_compose -f "${APP_COMPOSE_FILE}" ps 2>/dev/null || \
|
||||
run_compose "${COMPOSE_ARGS[@]}" ps --format json 2>/dev/null || \
|
||||
run_compose "${COMPOSE_ARGS[@]}" ps 2>/dev/null || \
|
||||
log info "no containers running"
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +552,7 @@ cmd_logs() {
|
|||
esac
|
||||
done
|
||||
|
||||
run_compose -f "${APP_COMPOSE_FILE}" logs --tail "${tail_lines}" 2>&1 || log info "no logs available"
|
||||
run_compose "${COMPOSE_ARGS[@]}" logs --tail "${tail_lines}" 2>&1 || log info "no logs available"
|
||||
}
|
||||
|
||||
cmd_validate_compose() {
|
||||
|
|
@ -482,11 +560,11 @@ cmd_validate_compose() {
|
|||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
if run_compose -f "${APP_COMPOSE_FILE}" config >/dev/null 2>&1; then
|
||||
if run_compose "${COMPOSE_ARGS[@]}" config >/dev/null 2>&1; then
|
||||
log info "compose file is valid"
|
||||
else
|
||||
local output
|
||||
output="$(run_compose -f "${APP_COMPOSE_FILE}" config 2>&1 || true)"
|
||||
output="$(run_compose "${COMPOSE_ARGS[@]}" config 2>&1 || true)"
|
||||
fail "compose validation failed: ${output}"
|
||||
fi
|
||||
}
|
||||
|
|
@ -497,7 +575,7 @@ cmd_remove() {
|
|||
validate_name "${name}"
|
||||
load_app "${name}"
|
||||
|
||||
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true
|
||||
|
||||
# Remove this app's block from the aggregate routes file.
|
||||
local route_file
|
||||
|
|
@ -512,7 +590,7 @@ cmd_remove() {
|
|||
routes_unlock
|
||||
fi
|
||||
|
||||
rm -f "$(app_manifest "${name}")"
|
||||
rm -f "$(app_manifest "${name}")" "$(app_env_file "${name}")"
|
||||
rm -rf "${APP_STACK_DIR}"
|
||||
|
||||
if [[ "${keep_volumes}" != "--keep-volumes" ]]; then
|
||||
|
|
@ -578,10 +656,10 @@ cmd_backup() {
|
|||
|
||||
# Stop containers before backup for consistency
|
||||
local was_running=false
|
||||
if run_compose -f "${APP_COMPOSE_FILE}" ps --format json 2>/dev/null | grep -q '"running"' 2>/dev/null; then
|
||||
if run_compose "${COMPOSE_ARGS[@]}" ps --format json 2>/dev/null | grep -q '"running"' 2>/dev/null; then
|
||||
was_running=true
|
||||
log info "stopping containers for consistent backup..."
|
||||
run_compose -f "${APP_COMPOSE_FILE}" down 2>/dev/null || true
|
||||
run_compose "${COMPOSE_ARGS[@]}" down 2>/dev/null || true
|
||||
fi
|
||||
|
||||
(cd "${volume_dir}" && zip -r "${backup_file}" .) || fail "zip failed"
|
||||
|
|
@ -596,7 +674,7 @@ cmd_backup() {
|
|||
# Restart if it was running
|
||||
if [[ "${was_running}" == "true" ]]; then
|
||||
log info "restarting containers after backup..."
|
||||
run_compose -f "${APP_COMPOSE_FILE}" up -d 2>/dev/null || true
|
||||
run_compose "${COMPOSE_ARGS[@]}" up -d 2>/dev/null || true
|
||||
fi
|
||||
|
||||
local size
|
||||
|
|
@ -633,7 +711,7 @@ cmd_volume_clear() {
|
|||
load_app "${name}"
|
||||
|
||||
log info "clearing volume data for app '${name}'"
|
||||
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true
|
||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true
|
||||
|
||||
local data_dir="${APP_VOLUME_DIR}/data"
|
||||
if [[ -d "${data_dir}" ]]; then
|
||||
|
|
@ -670,7 +748,7 @@ cmd_restore() {
|
|||
|
||||
# Stop containers before restore
|
||||
log info "stopping containers for restore..."
|
||||
run_compose -f "${APP_COMPOSE_FILE}" down 2>/dev/null || true
|
||||
run_compose "${COMPOSE_ARGS[@]}" down 2>/dev/null || true
|
||||
|
||||
# Clear existing volume data and extract backup
|
||||
rm -rf "${volume_dir:?}"/*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue