diff --git a/apps/kaneo/2.26.0-postgres16-digest-721873c/.env.sample b/apps/kaneo/2.26.0-postgres16-digest-721873c/.env.sample new file mode 100644 index 000000000..a92b6d019 --- /dev/null +++ b/apps/kaneo/2.26.0-postgres16-digest-721873c/.env.sample @@ -0,0 +1,10 @@ +CONTAINER_NAME=kaneo +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=5173 +KANEO_CLIENT_URL=http://localhost:5173 +DISABLE_REGISTRATION=false +POSTGRES_DB=kaneo +POSTGRES_USER=kaneo +POSTGRES_PASSWORD=generate +AUTH_SECRET=generate +APP_DATA_DIR=./data diff --git a/apps/kaneo/2.26.0-postgres16-digest-721873c/data.yml b/apps/kaneo/2.26.0-postgres16-digest-721873c/data.yml new file mode 100644 index 000000000..0fc03fec2 --- /dev/null +++ b/apps/kaneo/2.26.0-postgres16-digest-721873c/data.yml @@ -0,0 +1,156 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 5173 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: http://localhost:5173 + edit: true + envKey: KANEO_CLIENT_URL + labelEn: Public URL + labelZh: 公开访问地址 + label: + en: Public URL + zh: 公开访问地址 + zh-Hant: 公開存取網址 + ja: 公開 URL + ko: 공개 URL + ru: Публичный URL + ms: URL awam + pt-br: URL publica + required: true + type: text + - default: "false" + edit: true + envKey: DISABLE_REGISTRATION + labelEn: Disable Registration + labelZh: 禁止新用户注册 + label: + en: Disable Registration + zh: 禁止新用户注册 + zh-Hant: 停用新使用者註冊 + ja: 新規登録を無効化 + ko: 신규 등록 비활성화 + ru: Отключить регистрацию + ms: Nyahdayakan pendaftaran + pt-br: Desativar cadastro + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" + - default: kaneo + edit: true + envKey: POSTGRES_DB + labelEn: Database Name + labelZh: 数据库名称 + label: + en: Database Name + zh: 数据库名称 + zh-Hant: 資料庫名稱 + ja: データベース名 + ko: 데이터베이스 이름 + ru: Имя базы данных + ms: Nama pangkalan data + pt-br: Nome do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: kaneo + edit: true + envKey: POSTGRES_USER + labelEn: Database User + labelZh: 数据库用户 + label: + en: Database User + zh: 数据库用户 + zh-Hant: 資料庫使用者 + ja: データベースユーザー + ko: 데이터베이스 사용자 + ru: Пользователь базы данных + ms: Pengguna pangkalan data + pt-br: Usuario do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: POSTGRES_PASSWORD + labelEn: Database Password + labelZh: 数据库密码 + label: + en: Database Password + zh: 数据库密码 + zh-Hant: 資料庫密碼 + ja: データベースパスワード + ko: 데이터베이스 비밀번호 + ru: Пароль базы данных + ms: Kata laluan pangkalan data + pt-br: Senha do banco de dados + required: true + type: password + - default: generate + edit: true + envKey: AUTH_SECRET + labelEn: Authentication Secret + labelZh: 认证密钥 + label: + en: Authentication Secret + zh: 认证密钥 + zh-Hant: 驗證密鑰 + ja: 認証シークレット + ko: 인증 비밀 키 + ru: Секрет аутентификации + ms: Rahsia pengesahan + pt-br: Segredo de autenticacao + required: true + type: password + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/kaneo/2.26.0-postgres16-digest-721873c/docker-compose.yml b/apps/kaneo/2.26.0-postgres16-digest-721873c/docker-compose.yml new file mode 100644 index 000000000..f466baf3a --- /dev/null +++ b/apps/kaneo/2.26.0-postgres16-digest-721873c/docker-compose.yml @@ -0,0 +1,73 @@ +services: + kaneo-postgres: + image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea + container_name: ${CONTAINER_NAME}-postgres + restart: unless-stopped + user: "70:70" + networks: + - kaneo-internal + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - "${APP_DATA_DIR}/postgres:/var/lib/postgresql/data" + read_only: true + tmpfs: + - /tmp:mode=1777,size=64m + - /var/run/postgresql:uid=70,gid=70,mode=0755,size=16m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + labels: + createdBy: "Apps" + + kaneo: + image: ghcr.io/usekaneo/kaneo:2.26.0@sha256:549af9f6faf74825171badeda63db124461125bf9151425519b580513d4e4f5b + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1001:1001" + depends_on: + kaneo-postgres: + condition: service_healthy + networks: + - kaneo-internal + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:5173" + environment: + KANEO_CLIENT_URL: ${KANEO_CLIENT_URL} + POSTGRES_HOST: kaneo-postgres + POSTGRES_PORT: "5432" + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + AUTH_SECRET: ${AUTH_SECRET} + DISABLE_REGISTRATION: ${DISABLE_REGISTRATION} + tmpfs: + - /tmp:mode=1777,size=64m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5173/api/health"] + interval: 15s + timeout: 5s + retries: 8 + start_period: 60s + labels: + createdBy: "Apps" + +networks: + kaneo-internal: + internal: true + 1panel-network: + external: true diff --git a/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/init.sh b/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/init.sh new file mode 100755 index 000000000..0881dba08 --- /dev/null +++ b/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/init.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +read_env_value() { + local key="$1" + local value + value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +configured_value() { + local key="$1" + local default_value="$2" + local value + value="${!key:-}" + if [[ -z "$value" ]]; then + value="$(read_env_value "$key")" + fi + printf '%s\n' "${value:-$default_value}" +} + +resolve_app_path() { + local key="$1" + local raw="$2" + local clean candidate resolved current part + local -a parts=() + case "$raw" in + ""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;; + esac + if [[ "$raw" =~ [[:cntrl:]] ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + clean="${raw#./}" + [[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; } + command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; } + candidate="$ROOT_DIR/$clean" + resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; } + case "$resolved" in + "$ROOT_DIR"/*) ;; + *) echo "unsafe ${key} path" >&2; return 1 ;; + esac + current="$ROOT_DIR" + IFS='/' read -r -a parts <<< "$clean" + for part in "${parts[@]}"; do + [[ -z "$part" || "$part" == "." ]] && continue + current="$current/$part" + if [[ -L "$current" ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + done + printf '%s\n' "$resolved" +} + +set_env_value() { + local key="$1" + local value + local temp_file + case "$key" in + POSTGRES_PASSWORD|AUTH_SECRET) value="${!key}" ;; + *) fail "unsupported environment key: $key" ;; + esac + temp_file="$(mktemp "${ROOT_DIR}/.kaneo-env.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { written = 0 } + $0 ~ "^" key "=" { + if (!written) { + print key "=" value + written = 1 + } + next + } + { print } + END { if (!written) print key "=" value } + ' "$ENV_FILE" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +validate_ipv4() { + local key="$1" + local value="$2" + local octet + local -a octets + [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "$key must be an IPv4 address" + IFS=. read -r -a octets <<< "$value" + for octet in "${octets[@]}"; do + ((10#$octet <= 255)) || fail "$key contains an invalid IPv4 octet" + done +} + +validate_port() { + local key="$1" + local value="$2" + [[ "$value" =~ ^[0-9]+$ ]] || fail "$key must be an integer" + ((10#$value >= 1 && 10#$value <= 65535)) || fail "$key must be between 1 and 65535" +} + +generate_alphanumeric() { + local length="$1" + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge $length ]] || fail "unable to generate sufficient random material" + printf '%s\n' "${material:0:length}" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "Kaneo init must run as root" +command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" + +validate_ipv4 PANEL_APP_BIND_ADDRESS "$(read_env_value PANEL_APP_BIND_ADDRESS)" +validate_port PANEL_APP_PORT_HTTP "$(read_env_value PANEL_APP_PORT_HTTP)" + +client_url="$(read_env_value KANEO_CLIENT_URL)" +[[ "$client_url" =~ ^https?://([A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?|localhost)(:([0-9]{1,5}))?$ ]] || + fail "KANEO_CLIENT_URL must be an HTTP(S) origin without a path" +[[ "$client_url" != *..* ]] || fail "KANEO_CLIENT_URL contains an invalid hostname" +if [[ -n "${BASH_REMATCH[4]:-}" ]]; then + validate_port KANEO_CLIENT_URL_PORT "${BASH_REMATCH[4]}" +fi + +disable_registration="$(read_env_value DISABLE_REGISTRATION)" +[[ "$disable_registration" == true || "$disable_registration" == false ]] || + fail "DISABLE_REGISTRATION must be true or false" + +postgres_db="$(read_env_value POSTGRES_DB)" +postgres_user="$(read_env_value POSTGRES_USER)" +[[ "$postgres_db" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_DB is invalid" +[[ "$postgres_user" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_USER is invalid" + +POSTGRES_PASSWORD="$(read_env_value POSTGRES_PASSWORD)" +if [[ -z "$POSTGRES_PASSWORD" || "$POSTGRES_PASSWORD" == generate ]]; then + POSTGRES_PASSWORD="$(generate_alphanumeric 48)" +fi +[[ "$POSTGRES_PASSWORD" =~ ^[A-Za-z0-9]{32,128}$ ]] || + fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters" + +AUTH_SECRET="$(read_env_value AUTH_SECRET)" +if [[ -z "$AUTH_SECRET" || "$AUTH_SECRET" == generate ]]; then + AUTH_SECRET="$(openssl rand -hex 32)" +fi +[[ "$AUTH_SECRET" =~ ^[A-Fa-f0-9]{64,128}$ ]] || + fail "AUTH_SECRET must contain 64 to 128 hexadecimal characters" + +data_raw="$(configured_value APP_DATA_DIR ./data)" +data_dir="$(resolve_app_path APP_DATA_DIR "$data_raw")" + +install -d -m 0700 -- "$data_dir/postgres" +resolved_data="$(realpath -e -- "$data_dir")" +case "$resolved_data" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +chown -R 70:70 -- "$resolved_data/postgres" +chmod 0700 -- "$resolved_data/postgres" + +set_env_value POSTGRES_PASSWORD +set_env_value AUTH_SECRET +chmod 600 "$ENV_FILE" diff --git a/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/uninstall.sh b/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/uninstall.sh new file mode 100755 index 000000000..5c199242b --- /dev/null +++ b/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --remove-orphans +else + docker compose down --remove-orphans +fi diff --git a/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/upgrade.sh b/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/upgrade.sh new file mode 100755 index 000000000..fc9c84487 --- /dev/null +++ b/apps/kaneo/2.26.0-postgres16-digest-721873c/scripts/upgrade.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +read_env_value() { + local file="$1" + local key="$2" + local value + value="$(sed -n "s/^${key}=//p" "$file" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +resolve_app_path() { + local key="$1" + local raw="$2" + local clean candidate resolved current part + local -a parts=() + case "$raw" in + ""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;; + esac + if [[ "$raw" =~ [[:cntrl:]] ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + clean="${raw#./}" + [[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; } + command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; } + candidate="$ROOT_DIR/$clean" + resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; } + case "$resolved" in + "$ROOT_DIR"/*) ;; + *) echo "unsafe ${key} path" >&2; return 1 ;; + esac + current="$ROOT_DIR" + IFS='/' read -r -a parts <<< "$clean" + for part in "${parts[@]}"; do + [[ -z "$part" || "$part" == "." ]] && continue + current="$current/$part" + if [[ -L "$current" ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + done + printf '%s\n' "$resolved" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ENV_FILE}.kaneo-upgrade.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { written = 0 } + $0 ~ "^" key "=" { + if (!written) { + print key "=" value + written = 1 + } + next + } + { print } + END { if (!written) print key "=" value } + ' "$ENV_FILE" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +restore_credentials_from_env() { + local candidate="$1" + local current_container current_db current_user current_password current_auth + local candidate_container candidate_db candidate_user + local candidate_password candidate_auth + + [[ -f "$candidate" && ! -L "$candidate" ]] || return 1 + current_password="$(read_env_value "$ENV_FILE" POSTGRES_PASSWORD)" + current_auth="$(read_env_value "$ENV_FILE" AUTH_SECRET)" + current_container="$(read_env_value "$ENV_FILE" CONTAINER_NAME)" + current_db="$(read_env_value "$ENV_FILE" POSTGRES_DB)" + current_user="$(read_env_value "$ENV_FILE" POSTGRES_USER)" + candidate_container="$(read_env_value "$candidate" CONTAINER_NAME)" + candidate_db="$(read_env_value "$candidate" POSTGRES_DB)" + candidate_user="$(read_env_value "$candidate" POSTGRES_USER)" + [[ "$candidate_container" == "$current_container" && + "$candidate_db" == "$current_db" && + "$candidate_user" == "$current_user" ]] || return 1 + + candidate_password="$(read_env_value "$candidate" POSTGRES_PASSWORD)" + candidate_auth="$(read_env_value "$candidate" AUTH_SECRET)" + [[ "$candidate_password" =~ ^[A-Za-z0-9]{32,128}$ ]] || return 1 + [[ "$candidate_auth" =~ ^[A-Fa-f0-9]{64,128}$ ]] || return 1 + if [[ "$current_password" == "" || "$current_password" == generate ]]; then + set_env_value POSTGRES_PASSWORD "$candidate_password" + fi + if [[ "$current_auth" == "" || "$current_auth" == generate ]]; then + set_env_value AUTH_SECRET "$candidate_auth" + fi + return 0 +} + +restore_credentials_from_backup() ( + local panel_root app_key install_name backup_dir archive archive_entry + local temp_dir inner_archive env_entry latest_mtime archive_mtime + + panel_root="${ROOT_DIR%%/apps/*}" + [[ "$panel_root" != "$ROOT_DIR" ]] || exit 1 + app_key="$(basename "$(dirname "$ROOT_DIR")")" + install_name="$(basename "$ROOT_DIR")" + [[ "$app_key" =~ ^[A-Za-z0-9._-]+$ && "$install_name" =~ ^[A-Za-z0-9._-]+$ ]] || exit 1 + backup_dir="$panel_root/backup/app/local${app_key}/${install_name}" + [[ -d "$backup_dir" && ! -L "$backup_dir" ]] || exit 1 + + archive="" + latest_mtime=0 + while IFS= read -r -d '' candidate; do + archive_mtime="$(stat -c '%Y' "$candidate")" + if [[ -z "$archive" ]] || (( archive_mtime > latest_mtime )); then + archive="$candidate" + latest_mtime="$archive_mtime" + fi + done < <(find "$backup_dir" -maxdepth 1 -type f -name 'upgrade_backup_*.tar.gz' -print0) + [[ -n "$archive" ]] || exit 1 + + temp_dir="$(mktemp -d "$ROOT_DIR/.kaneo-upgrade-backup.XXXXXX")" + trap 'rm -rf -- "$temp_dir"' EXIT + inner_archive="$temp_dir/app.tar.gz" + archive_entry="$(tar -tzf "$archive" | awk '$0 ~ /\/app\.tar\.gz$/ { print; exit }')" + [[ -n "$archive_entry" ]] || exit 1 + tar -xOf "$archive" "$archive_entry" > "$inner_archive" + env_entry="$install_name/.env" + tar -tzf "$inner_archive" | grep -Fqx "$env_entry" || exit 1 + tar -xOf "$inner_archive" "$env_entry" > "$temp_dir/old.env" + restore_credentials_from_env "$temp_dir/old.env" +) + +restore_previous_credentials() { + local current_password current_auth candidate + local -a snapshots + + current_password="$(read_env_value "$ENV_FILE" POSTGRES_PASSWORD)" + current_auth="$(read_env_value "$ENV_FILE" AUTH_SECRET)" + if [[ "$current_password" != "" && "$current_password" != generate && + "$current_auth" != "" && "$current_auth" != generate ]]; then + return 0 + fi + + shopt -s nullglob + snapshots=(/tmp/1panel-app-upgrade-*/.env) + shopt -u nullglob + for candidate in "${snapshots[@]}"; do + restore_credentials_from_env "$candidate" && return 0 + done + restore_credentials_from_backup +} + +[[ -f "$ENV_FILE" ]] || { + printf '%s\n' "$ENV_FILE not found" >&2 + exit 1 +} +if ! restore_previous_credentials; then + data_raw="$(read_env_value "$ENV_FILE" APP_DATA_DIR)" + data_dir="$(resolve_app_path APP_DATA_DIR "$data_raw")" + if [[ -f "$data_dir/postgres/PG_VERSION" ]]; then + printf '%s\n' 'Kaneo upgrade could not recover existing database credentials' >&2 + exit 1 + fi +fi +exec "$SCRIPT_DIR/init.sh"