From c1d6783d5564e5e4cc930aa2c54f4f24c98a2477 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:02:01 +0800 Subject: [PATCH] Add questdb app Maintainer-workflow: appstore-pr-maintainer --- apps/questdb/9.4.3/.env.sample | 9 ++ apps/questdb/9.4.3/data.yml | 139 +++++++++++++++++ apps/questdb/9.4.3/data/.gitkeep | 1 + apps/questdb/9.4.3/docker-compose.yml | 34 ++++ apps/questdb/9.4.3/scripts/init.sh | 189 +++++++++++++++++++++++ apps/questdb/9.4.3/scripts/uninstall.sh | 8 + apps/questdb/9.4.3/scripts/upgrade.sh | 5 + apps/questdb/README.md | 57 +++++++ apps/questdb/data.yml | 31 ++++ apps/questdb/latest/.env.sample | 9 ++ apps/questdb/latest/data.yml | 139 +++++++++++++++++ apps/questdb/latest/data/.gitkeep | 1 + apps/questdb/latest/docker-compose.yml | 34 ++++ apps/questdb/latest/scripts/init.sh | 189 +++++++++++++++++++++++ apps/questdb/latest/scripts/uninstall.sh | 8 + apps/questdb/latest/scripts/upgrade.sh | 5 + apps/questdb/logo.png | Bin 0 -> 4453 bytes 17 files changed, 858 insertions(+) create mode 100644 apps/questdb/9.4.3/.env.sample create mode 100644 apps/questdb/9.4.3/data.yml create mode 100644 apps/questdb/9.4.3/data/.gitkeep create mode 100644 apps/questdb/9.4.3/docker-compose.yml create mode 100755 apps/questdb/9.4.3/scripts/init.sh create mode 100755 apps/questdb/9.4.3/scripts/uninstall.sh create mode 100755 apps/questdb/9.4.3/scripts/upgrade.sh create mode 100644 apps/questdb/README.md create mode 100644 apps/questdb/data.yml create mode 100644 apps/questdb/latest/.env.sample create mode 100644 apps/questdb/latest/data.yml create mode 100644 apps/questdb/latest/data/.gitkeep create mode 100644 apps/questdb/latest/docker-compose.yml create mode 100755 apps/questdb/latest/scripts/init.sh create mode 100755 apps/questdb/latest/scripts/uninstall.sh create mode 100755 apps/questdb/latest/scripts/upgrade.sh create mode 100644 apps/questdb/logo.png diff --git a/apps/questdb/9.4.3/.env.sample b/apps/questdb/9.4.3/.env.sample new file mode 100644 index 000000000..18379ca1f --- /dev/null +++ b/apps/questdb/9.4.3/.env.sample @@ -0,0 +1,9 @@ +PANEL_APP_PORT_HTTP=9000 +PANEL_APP_PORT_PG=8812 +QUESTDB_HTTP_USER=admin +QUESTDB_HTTP_PASSWORD=replace-with-at-least-32-safe-characters +QUESTDB_PG_USER=admin +QUESTDB_PG_PASSWORD=replace-with-another-32-safe-characters +QUESTDB_TELEMETRY_ENABLED=false +APP_DATA_DIR=./data +CONTAINER_NAME=questdb diff --git a/apps/questdb/9.4.3/data.yml b/apps/questdb/9.4.3/data.yml new file mode 100644 index 000000000..22b8778d9 --- /dev/null +++ b/apps/questdb/9.4.3/data.yml @@ -0,0 +1,139 @@ +additionalProperties: + formFields: + - default: 9000 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: Web and HTTP API Port + labelZh: Web 与 HTTP API 端口 + label: + en: Web and HTTP API Port + zh: Web 与 HTTP API 端口 + zh-Hant: Web 與 HTTP API 連接埠 + ja: Web および HTTP API ポート + ko: Web 및 HTTP API 포트 + ru: Порт Web и HTTP API + ms: Port Web dan API HTTP + pt-br: Porta Web e da API HTTP + required: true + rule: paramPort + type: number + - default: 8812 + edit: true + envKey: PANEL_APP_PORT_PG + labelEn: PGWire Port + labelZh: PGWire 端口 + label: + en: PGWire Port + zh: PGWire 端口 + zh-Hant: PGWire 連接埠 + ja: PGWire ポート + ko: PGWire 포트 + ru: Порт PGWire + ms: Port PGWire + pt-br: Porta PGWire + required: true + rule: paramPort + type: number + - default: admin + edit: true + envKey: QUESTDB_HTTP_USER + labelEn: HTTP Username + labelZh: HTTP 用户名 + label: + en: HTTP Username + zh: HTTP 用户名 + zh-Hant: HTTP 使用者名稱 + ja: HTTP ユーザー名 + ko: HTTP 사용자 이름 + ru: Имя пользователя HTTP + ms: Nama Pengguna HTTP + pt-br: Usuário HTTP + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: QUESTDB_HTTP_PASSWORD + labelEn: HTTP Password + labelZh: HTTP 密码 + label: + en: HTTP Password + zh: HTTP 密码 + zh-Hant: HTTP 密碼 + ja: HTTP パスワード + ko: HTTP 비밀번호 + ru: Пароль HTTP + ms: Kata Laluan HTTP + pt-br: Senha HTTP + required: true + type: password + - default: admin + edit: true + envKey: QUESTDB_PG_USER + labelEn: PGWire Username + labelZh: PGWire 用户名 + label: + en: PGWire Username + zh: PGWire 用户名 + zh-Hant: PGWire 使用者名稱 + ja: PGWire ユーザー名 + ko: PGWire 사용자 이름 + ru: Имя пользователя PGWire + ms: Nama Pengguna PGWire + pt-br: Usuário PGWire + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: QUESTDB_PG_PASSWORD + labelEn: PGWire Password + labelZh: PGWire 密码 + label: + en: PGWire Password + zh: PGWire 密码 + zh-Hant: PGWire 密碼 + ja: PGWire パスワード + ko: PGWire 비밀번호 + ru: Пароль PGWire + ms: Kata Laluan PGWire + pt-br: Senha PGWire + required: true + type: password + - default: "false" + edit: true + envKey: QUESTDB_TELEMETRY_ENABLED + labelEn: Enable Anonymous Telemetry + labelZh: 启用匿名遥测 + label: + en: Enable Anonymous Telemetry + zh: 启用匿名遥测 + zh-Hant: 啟用匿名遙測 + ja: 匿名テレメトリを有効化 + ko: 익명 원격 측정 활성화 + ru: Включить анонимную телеметрию + ms: Dayakan Telemetri Tanpa Nama + pt-br: Ativar telemetria anônima + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" + - 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: Diretório de dados + required: true + type: text diff --git a/apps/questdb/9.4.3/data/.gitkeep b/apps/questdb/9.4.3/data/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/questdb/9.4.3/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/apps/questdb/9.4.3/docker-compose.yml b/apps/questdb/9.4.3/docker-compose.yml new file mode 100644 index 000000000..e5f0ab805 --- /dev/null +++ b/apps/questdb/9.4.3/docker-compose.yml @@ -0,0 +1,34 @@ +services: + questdb: + image: "questdb/questdb:9.4.3" + container_name: ${CONTAINER_NAME} + init: true + restart: unless-stopped + networks: + - 1panel-network + ports: + - "${PANEL_APP_PORT_HTTP}:9000" + - "${PANEL_APP_PORT_PG}:8812" + environment: + - QDB_HTTP_USER=${QUESTDB_HTTP_USER} + - QDB_HTTP_PASSWORD=${QUESTDB_HTTP_PASSWORD} + - QDB_PG_USER=${QUESTDB_PG_USER} + - QDB_PG_PASSWORD=${QUESTDB_PG_PASSWORD} + - QDB_TELEMETRY_ENABLED=${QUESTDB_TELEMETRY_ENABLED} + - QDB_LINE_TCP_ENABLED=false + - QDB_HTTP_PESSIMISTIC_HEALTH_CHECK_ENABLED=true + - QDB_HTTP_HEALTH_CHECK_AUTHENTICATION_REQUIRED=false + volumes: + - "${APP_DATA_DIR}:/var/lib/questdb" + healthcheck: + test: ["CMD-SHELL", "curl --fail --silent http://127.0.0.1:9003/status >/dev/null"] + interval: 30s + timeout: 5s + start_period: 60s + retries: 5 + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/questdb/9.4.3/scripts/init.sh b/apps/questdb/9.4.3/scripts/init.sh new file mode 100755 index 000000000..ceed52f87 --- /dev/null +++ b/apps/questdb/9.4.3/scripts/init.sh @@ -0,0 +1,189 @@ +#!/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 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +env_has_key() { + local key="$1" + [[ -f "$ENV_FILE" ]] && grep -qE "^${key}=" "$ENV_FILE" +} + +read_env_value() { + local key="$1" + local value="" + + if env_has_key "$key"; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)" + fi + strip_matching_quotes "$value" +} + +read_effective_value() { + local key="$1" + + if [[ -v "$key" ]]; then + strip_matching_quotes "${!key}" + else + read_env_value "$key" + fi +} + +username_is_safe() { + local value="$1" + [[ "$value" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,62}$ ]] +} + +secret_is_safe() { + local value="$1" + [[ "$value" =~ ^[-A-Za-z0-9._~!@#%^\&*+=:,/?]{32,}$ ]] +} + +generate_secret() { + local value="" + + if command -v openssl >/dev/null 2>&1; then + value="$(openssl rand -hex 32)" + elif [[ -r /dev/urandom ]] && command -v od >/dev/null 2>&1; then + value="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')" + fi + [[ "$value" =~ ^[0-9a-f]{64}$ ]] || fail "Unable to generate a secure QuestDB password" + printf '%s\n' "$value" +} + +write_secret_cache() { + local cache_file="$1" + local value="$2" + local temp_file + + umask 077 + temp_file="$(mktemp "${cache_file}.tmp.XXXXXX")" + printf '%s\n' "$value" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$cache_file" +} + +set_env_value() { + local key="$1" + local value="$2" + local env_dir + local temp_file + + [[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" + [[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + env_dir="$(dirname "$ENV_FILE")" + temp_file="$(mktemp "${env_dir}/.questdb-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 --reference="$ENV_FILE" "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +resolve_secret() { + local env_key="$1" + local cache_file="$2" + local value + local cached_value="" + + [[ ! -L "$cache_file" ]] || fail "QuestDB password cache must not be a symbolic link" + if [[ -e "$cache_file" && ! -f "$cache_file" ]]; then + fail "QuestDB password cache must be a regular file" + fi + if [[ -s "$cache_file" ]]; then + cached_value="$(sed -n '1p' "$cache_file")" + secret_is_safe "$cached_value" || fail "Persisted QuestDB password is invalid" + fi + + value="$(read_effective_value "$env_key")" + if [[ -z "$value" || "$value" == "generate" ]]; then + if [[ -n "$cached_value" ]]; then + value="$cached_value" + else + value="$(generate_secret)" + fi + else + secret_is_safe "$value" || fail "${env_key} must contain at least 32 safe characters" + fi + + write_secret_cache "$cache_file" "$value" + set_env_value "$env_key" "$value" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + +app_data_dir_explicit=false +if [[ ${APP_DATA_DIR+x} ]]; then + APP_DATA_DIR_RAW="$APP_DATA_DIR" + app_data_dir_explicit=true +elif env_has_key APP_DATA_DIR; then + APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" + app_data_dir_explicit=true +else + APP_DATA_DIR_RAW="./data" +fi +APP_DATA_DIR_RAW="$(strip_matching_quotes "$APP_DATA_DIR_RAW")" + +if [[ "$app_data_dir_explicit" == "true" && -z "$APP_DATA_DIR_RAW" ]]; then + fail "APP_DATA_DIR must not be empty" +fi + +case "$APP_DATA_DIR_RAW" in + /*) + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; + *) + APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" + case "$APP_DATA_DIR_ABS" in + "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;; + *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;; + esac + ;; +esac + +[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi +mkdir -p -- "$APP_DATA_DIR_ABS" + +http_user="$(read_effective_value QUESTDB_HTTP_USER)" +pg_user="$(read_effective_value QUESTDB_PG_USER)" +username_is_safe "$http_user" || fail "QUESTDB_HTTP_USER contains unsupported characters" +username_is_safe "$pg_user" || fail "QUESTDB_PG_USER contains unsupported characters" + +resolve_secret QUESTDB_HTTP_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_http_password" +resolve_secret QUESTDB_PG_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_pg_password" diff --git a/apps/questdb/9.4.3/scripts/uninstall.sh b/apps/questdb/9.4.3/scripts/uninstall.sh new file mode 100755 index 000000000..5a3f6bd6b --- /dev/null +++ b/apps/questdb/9.4.3/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 --volumes --remove-orphans +else + docker compose down --volumes --remove-orphans +fi diff --git a/apps/questdb/9.4.3/scripts/upgrade.sh b/apps/questdb/9.4.3/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/questdb/9.4.3/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec bash "${SCRIPT_DIR}/init.sh" diff --git a/apps/questdb/README.md b/apps/questdb/README.md new file mode 100644 index 000000000..2a28ec1bb --- /dev/null +++ b/apps/questdb/README.md @@ -0,0 +1,57 @@ +# QuestDB + +## 产品介绍 + +QuestDB 是一个面向实时分析的开源时序数据库,提供高吞吐写入、低延迟 SQL 查询、Web Console、REST API、InfluxDB Line Protocol 和 PostgreSQL Wire Protocol。 + +## 主要功能 + +- 面向时序数据的列式存储、WAL 和分区管理 +- Web Console、REST API 与 SQL 查询 +- 通过 HTTP 接收 InfluxDB Line Protocol 数据 +- 兼容 PostgreSQL Wire Protocol 的查询接口 +- 快照、检查点和持久化数据目录 + +## 访问说明 + +- Web Console、REST API 和 ILP-over-HTTP 共用默认端口 `9000`。 +- PostgreSQL Wire Protocol 默认使用端口 `8812`,数据库名为 `qdb`。 +- 本应用默认启用 HTTP Basic 认证和 PGWire 密码认证,两个接口使用独立用户名与密码。 +- QuestDB 的 ILP TCP 端口 `9009` 默认不提供简单的用户名/密码保护。本应用为减少未认证写入面,默认关闭且不发布该端口;常见写入场景可直接使用已受 HTTP Basic 保护的 ILP-over-HTTP。 +- 内部 `9003` 端口仅用于容器健康检查,不发布到宿主机。健康状态端点不要求业务凭据,其他 HTTP 接口仍受 Basic 认证保护。 + +## 数据与安全 + +- QuestDB 的完整数据根目录持久化到安装表单选择的数据目录,其中包含 `db`、`conf`、`public`、快照和检查点等运行数据。 +- 安装脚本会生成并持久化独立的 HTTP 与 PGWire 密码。数据目录中的 `.questdb_http_password` 和 `.questdb_pg_password` 用于防止 1Panel 升级回放安装参数时轮换密码,请与数据库一起备份。 +- 官方镜像入口以 root 启动,用于检查数据目录属主,然后以 UID/GID `10001:10001` 运行 QuestDB。首次挂载既有数据目录时,上游入口可能调整其已知数据子目录的属主;迁移前应先备份并核对权限。 +- Web Console 和协议端口本身不提供 HTTPS。对外访问时应使用 HTTPS 反向代理、限制端口来源,并按需配置网络层 TLS。 +- 默认关闭匿名遥测,可在安装表单中按需开启。 + +## 升级说明 + +- 升级前停止写入并完整备份数据目录及两个密码缓存文件。 +- 固定版与 `latest` 使用相同的数据路径和配置键,可通过 1Panel 执行跨版本升级。 +- 数据格式迁移可能在启动阶段完成。生产数据应先在副本环境验证目标版本,并在升级过程中等待健康检查通过后再恢复流量。 + +## Introduction + +QuestDB is an open-source time-series database for high-throughput ingestion and low-latency SQL analytics. This package exposes the authenticated Web/REST/ILP-over-HTTP endpoint and PGWire endpoint, while disabling unauthenticated ILP TCP by default. + +Back up the full data directory, `.questdb_http_password` and `.questdb_pg_password` before upgrades. Public deployments should place the Web endpoint behind HTTPS and restrict direct access to the database ports. + +## Features + +- Column-oriented time-series storage, WAL and partition management +- Web Console, REST API and SQL queries +- Authenticated InfluxDB Line Protocol over HTTP +- PostgreSQL Wire Protocol compatibility +- Persistent checkpoints and snapshots + +## 参考资料 + +- Docker 部署: +- 配置选项: +- 安全指南: +- 源码仓库: +- 官方镜像: diff --git a/apps/questdb/data.yml b/apps/questdb/data.yml new file mode 100644 index 000000000..9b77ed434 --- /dev/null +++ b/apps/questdb/data.yml @@ -0,0 +1,31 @@ +name: QuestDB +tags: + - 数据库 +title: 面向实时分析的高性能开源时序数据库 +description: 面向实时分析的高性能开源时序数据库 +additionalProperties: + key: questdb + name: QuestDB + tags: + - Database + shortDescZh: 面向实时分析的高性能开源时序数据库 + shortDescEn: A high-performance open-source time-series database for real-time analytics + description: + en: A high-performance open-source time-series database for real-time analytics + zh: 面向实时分析的高性能开源时序数据库 + zh-Hant: 面向即時分析的高效能開源時序資料庫 + ja: リアルタイム分析向けの高性能オープンソース時系列データベース + ko: 실시간 분석을 위한 고성능 오픈 소스 시계열 데이터베이스 + ru: Высокопроизводительная база данных временных рядов с открытым исходным кодом для аналитики в реальном времени + ms: Pangkalan data siri masa sumber terbuka berprestasi tinggi untuk analitik masa nyata + pt-br: Banco de dados de séries temporais de alto desempenho e código aberto para análises em tempo real + type: website + crossVersionUpdate: true + limit: 0 + recommend: 0 + website: https://questdb.com/ + github: https://github.com/questdb/questdb + document: https://questdb.com/docs/deployment/docker/ + architectures: + - amd64 + - arm64 diff --git a/apps/questdb/latest/.env.sample b/apps/questdb/latest/.env.sample new file mode 100644 index 000000000..18379ca1f --- /dev/null +++ b/apps/questdb/latest/.env.sample @@ -0,0 +1,9 @@ +PANEL_APP_PORT_HTTP=9000 +PANEL_APP_PORT_PG=8812 +QUESTDB_HTTP_USER=admin +QUESTDB_HTTP_PASSWORD=replace-with-at-least-32-safe-characters +QUESTDB_PG_USER=admin +QUESTDB_PG_PASSWORD=replace-with-another-32-safe-characters +QUESTDB_TELEMETRY_ENABLED=false +APP_DATA_DIR=./data +CONTAINER_NAME=questdb diff --git a/apps/questdb/latest/data.yml b/apps/questdb/latest/data.yml new file mode 100644 index 000000000..22b8778d9 --- /dev/null +++ b/apps/questdb/latest/data.yml @@ -0,0 +1,139 @@ +additionalProperties: + formFields: + - default: 9000 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: Web and HTTP API Port + labelZh: Web 与 HTTP API 端口 + label: + en: Web and HTTP API Port + zh: Web 与 HTTP API 端口 + zh-Hant: Web 與 HTTP API 連接埠 + ja: Web および HTTP API ポート + ko: Web 및 HTTP API 포트 + ru: Порт Web и HTTP API + ms: Port Web dan API HTTP + pt-br: Porta Web e da API HTTP + required: true + rule: paramPort + type: number + - default: 8812 + edit: true + envKey: PANEL_APP_PORT_PG + labelEn: PGWire Port + labelZh: PGWire 端口 + label: + en: PGWire Port + zh: PGWire 端口 + zh-Hant: PGWire 連接埠 + ja: PGWire ポート + ko: PGWire 포트 + ru: Порт PGWire + ms: Port PGWire + pt-br: Porta PGWire + required: true + rule: paramPort + type: number + - default: admin + edit: true + envKey: QUESTDB_HTTP_USER + labelEn: HTTP Username + labelZh: HTTP 用户名 + label: + en: HTTP Username + zh: HTTP 用户名 + zh-Hant: HTTP 使用者名稱 + ja: HTTP ユーザー名 + ko: HTTP 사용자 이름 + ru: Имя пользователя HTTP + ms: Nama Pengguna HTTP + pt-br: Usuário HTTP + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: QUESTDB_HTTP_PASSWORD + labelEn: HTTP Password + labelZh: HTTP 密码 + label: + en: HTTP Password + zh: HTTP 密码 + zh-Hant: HTTP 密碼 + ja: HTTP パスワード + ko: HTTP 비밀번호 + ru: Пароль HTTP + ms: Kata Laluan HTTP + pt-br: Senha HTTP + required: true + type: password + - default: admin + edit: true + envKey: QUESTDB_PG_USER + labelEn: PGWire Username + labelZh: PGWire 用户名 + label: + en: PGWire Username + zh: PGWire 用户名 + zh-Hant: PGWire 使用者名稱 + ja: PGWire ユーザー名 + ko: PGWire 사용자 이름 + ru: Имя пользователя PGWire + ms: Nama Pengguna PGWire + pt-br: Usuário PGWire + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: QUESTDB_PG_PASSWORD + labelEn: PGWire Password + labelZh: PGWire 密码 + label: + en: PGWire Password + zh: PGWire 密码 + zh-Hant: PGWire 密碼 + ja: PGWire パスワード + ko: PGWire 비밀번호 + ru: Пароль PGWire + ms: Kata Laluan PGWire + pt-br: Senha PGWire + required: true + type: password + - default: "false" + edit: true + envKey: QUESTDB_TELEMETRY_ENABLED + labelEn: Enable Anonymous Telemetry + labelZh: 启用匿名遥测 + label: + en: Enable Anonymous Telemetry + zh: 启用匿名遥测 + zh-Hant: 啟用匿名遙測 + ja: 匿名テレメトリを有効化 + ko: 익명 원격 측정 활성화 + ru: Включить анонимную телеметрию + ms: Dayakan Telemetri Tanpa Nama + pt-br: Ativar telemetria anônima + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" + - 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: Diretório de dados + required: true + type: text diff --git a/apps/questdb/latest/data/.gitkeep b/apps/questdb/latest/data/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/questdb/latest/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/apps/questdb/latest/docker-compose.yml b/apps/questdb/latest/docker-compose.yml new file mode 100644 index 000000000..0532b6a24 --- /dev/null +++ b/apps/questdb/latest/docker-compose.yml @@ -0,0 +1,34 @@ +services: + questdb: + image: "questdb/questdb:latest" + container_name: ${CONTAINER_NAME} + init: true + restart: unless-stopped + networks: + - 1panel-network + ports: + - "${PANEL_APP_PORT_HTTP}:9000" + - "${PANEL_APP_PORT_PG}:8812" + environment: + - QDB_HTTP_USER=${QUESTDB_HTTP_USER} + - QDB_HTTP_PASSWORD=${QUESTDB_HTTP_PASSWORD} + - QDB_PG_USER=${QUESTDB_PG_USER} + - QDB_PG_PASSWORD=${QUESTDB_PG_PASSWORD} + - QDB_TELEMETRY_ENABLED=${QUESTDB_TELEMETRY_ENABLED} + - QDB_LINE_TCP_ENABLED=false + - QDB_HTTP_PESSIMISTIC_HEALTH_CHECK_ENABLED=true + - QDB_HTTP_HEALTH_CHECK_AUTHENTICATION_REQUIRED=false + volumes: + - "${APP_DATA_DIR}:/var/lib/questdb" + healthcheck: + test: ["CMD-SHELL", "curl --fail --silent http://127.0.0.1:9003/status >/dev/null"] + interval: 30s + timeout: 5s + start_period: 60s + retries: 5 + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/questdb/latest/scripts/init.sh b/apps/questdb/latest/scripts/init.sh new file mode 100755 index 000000000..ceed52f87 --- /dev/null +++ b/apps/questdb/latest/scripts/init.sh @@ -0,0 +1,189 @@ +#!/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 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +env_has_key() { + local key="$1" + [[ -f "$ENV_FILE" ]] && grep -qE "^${key}=" "$ENV_FILE" +} + +read_env_value() { + local key="$1" + local value="" + + if env_has_key "$key"; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)" + fi + strip_matching_quotes "$value" +} + +read_effective_value() { + local key="$1" + + if [[ -v "$key" ]]; then + strip_matching_quotes "${!key}" + else + read_env_value "$key" + fi +} + +username_is_safe() { + local value="$1" + [[ "$value" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,62}$ ]] +} + +secret_is_safe() { + local value="$1" + [[ "$value" =~ ^[-A-Za-z0-9._~!@#%^\&*+=:,/?]{32,}$ ]] +} + +generate_secret() { + local value="" + + if command -v openssl >/dev/null 2>&1; then + value="$(openssl rand -hex 32)" + elif [[ -r /dev/urandom ]] && command -v od >/dev/null 2>&1; then + value="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')" + fi + [[ "$value" =~ ^[0-9a-f]{64}$ ]] || fail "Unable to generate a secure QuestDB password" + printf '%s\n' "$value" +} + +write_secret_cache() { + local cache_file="$1" + local value="$2" + local temp_file + + umask 077 + temp_file="$(mktemp "${cache_file}.tmp.XXXXXX")" + printf '%s\n' "$value" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$cache_file" +} + +set_env_value() { + local key="$1" + local value="$2" + local env_dir + local temp_file + + [[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" + [[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + env_dir="$(dirname "$ENV_FILE")" + temp_file="$(mktemp "${env_dir}/.questdb-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 --reference="$ENV_FILE" "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +resolve_secret() { + local env_key="$1" + local cache_file="$2" + local value + local cached_value="" + + [[ ! -L "$cache_file" ]] || fail "QuestDB password cache must not be a symbolic link" + if [[ -e "$cache_file" && ! -f "$cache_file" ]]; then + fail "QuestDB password cache must be a regular file" + fi + if [[ -s "$cache_file" ]]; then + cached_value="$(sed -n '1p' "$cache_file")" + secret_is_safe "$cached_value" || fail "Persisted QuestDB password is invalid" + fi + + value="$(read_effective_value "$env_key")" + if [[ -z "$value" || "$value" == "generate" ]]; then + if [[ -n "$cached_value" ]]; then + value="$cached_value" + else + value="$(generate_secret)" + fi + else + secret_is_safe "$value" || fail "${env_key} must contain at least 32 safe characters" + fi + + write_secret_cache "$cache_file" "$value" + set_env_value "$env_key" "$value" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + +app_data_dir_explicit=false +if [[ ${APP_DATA_DIR+x} ]]; then + APP_DATA_DIR_RAW="$APP_DATA_DIR" + app_data_dir_explicit=true +elif env_has_key APP_DATA_DIR; then + APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" + app_data_dir_explicit=true +else + APP_DATA_DIR_RAW="./data" +fi +APP_DATA_DIR_RAW="$(strip_matching_quotes "$APP_DATA_DIR_RAW")" + +if [[ "$app_data_dir_explicit" == "true" && -z "$APP_DATA_DIR_RAW" ]]; then + fail "APP_DATA_DIR must not be empty" +fi + +case "$APP_DATA_DIR_RAW" in + /*) + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; + *) + APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" + case "$APP_DATA_DIR_ABS" in + "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;; + *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;; + esac + ;; +esac + +[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi +mkdir -p -- "$APP_DATA_DIR_ABS" + +http_user="$(read_effective_value QUESTDB_HTTP_USER)" +pg_user="$(read_effective_value QUESTDB_PG_USER)" +username_is_safe "$http_user" || fail "QUESTDB_HTTP_USER contains unsupported characters" +username_is_safe "$pg_user" || fail "QUESTDB_PG_USER contains unsupported characters" + +resolve_secret QUESTDB_HTTP_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_http_password" +resolve_secret QUESTDB_PG_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_pg_password" diff --git a/apps/questdb/latest/scripts/uninstall.sh b/apps/questdb/latest/scripts/uninstall.sh new file mode 100755 index 000000000..5a3f6bd6b --- /dev/null +++ b/apps/questdb/latest/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 --volumes --remove-orphans +else + docker compose down --volumes --remove-orphans +fi diff --git a/apps/questdb/latest/scripts/upgrade.sh b/apps/questdb/latest/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/questdb/latest/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec bash "${SCRIPT_DIR}/init.sh" diff --git a/apps/questdb/logo.png b/apps/questdb/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ca83aa19a31cd2a29496cb0521b54f01b7f3dc1e GIT binary patch literal 4453 zcmV-r5t{CaP)cw=oH1;^NOztyY`|53 zkS%J(W{$#JhK?<2u{myxENZbjZigyrkS%M7Dr$%;YRN)x%R_L*L2$!BZ_q_>yghEf zKX1E2bHG1vy+3fQIBlafY^gSF%Sv^`NOsCic$+e8%Tj#COnAyxfWuLJ%3Fi8I&Q^U zgv(%u#$=3^Fl&-7Ym6*vhbn4E&ptc=003ijQchC<0~RMlPGfz8l%=z~qh?t&Dhfl( z+T`=@w!4(>Zs%gjL{bttuHD#<|MdMZqq{~oTa=sjEh!Au@WAX`Oj5l)Ig}=t&nd`ZC+HDpyfCxYcs5K}!!WbirF~-vB!u;#E z$=ubg^){OsL4*K8z|z`)CfqWXH*(d?Yqj!R_I8^DWCm3kAOryU0jN1-EMtsiS*qlO z%xhHnHXy_S(1<_-(;*m9YY=2vk|gQf($ecONk*W^Y+wK(L>4jt2m!;*7|W<5%QD5T zLUZL&CzGWo zQ=KJ=@EXvP^xk%wdAlgA8X~s??d-rN0wehqLKw@kfonlam7JKi%xwq*mmq|uGXMYp zV?d38FqY+49gL1-+0HCU(ppMl^GhqCG6T~dM#8feAVL@+2~$T)uBhY-X(Wy8JMD09 z009(+@~~C0m)pNqr>^YnVx+4ckm$lI(zOEdoq4G)WwiBuVls5A)HQF#Cip zuGsQiK-vJ%Eb0)#nApRR8_U?Mu)#d@Y|UR;wIK(kI-2Bw1#0GKS(0TLb$p@IV7`9K zZfACpM3bGhbKABR8T(q9FfYwMJ6~P5*_oN@05Yn601yDkvIQxVWQ1MmIZfv5dOLzS ztW5>jiVYK5mSy~k_}9Z+O_3Rfg;_S!RcM+TAURh3hk?NY2KsrZe7`%s(!k1tKAMS_-(GB`3_@D$~%l@vZG*sX3>vX zQ5sVeBI0V>7VK65G;%W_A27z1N~Kb%P;6P2G4^3@h+c1~ilQjWe&V8UZlxWet|QQp z*86KKrTkT4XEyir=XKJRTa77IJu z`K?L>=9q-pOYej)LV(Er z{r*b!txy+cs`<4*=1tXNRYml-{}k* zt)2Mh2&(H40_awDMomwf&WfUF7WZL{Z;;7_7SITxm8BUy*cI+symqA(O}!uet6aay z&Kt&o8@e{DzG2}|@ij-hwU}86$;!rFBh^~1$d#BhBrLOnx-|QNqsf6u$vuoCYV`O)dyk>M+aq&#hIP&lpBFEYnrM? zl3KXQEH_Bz0J&MML5|~=KeTplH*fR=k}0uu1_(h2S7zF_7wRl%3a&p=+)%B&LF3VKP~AbG-Pk852tou`QXJl?9vB-S!F8#9 zC&<$N(1aUh(_`jp4YoOgO}zvt^syiH`(ldFT+>W;USwTo7fpZ5wjqSar`=N$4&;-Q zYpKpHEu--bl#u7Q1ec6Me$r|Axk%QRTlP<36`KVm;<8PKP;FtFeM zC{<`~muYBSRn;c@TnIu4kGnK%+{eeK$Ezt1{#6e&UtnEVRb79`tXno9gb;SU)TgJX zr;90%{S{i8N{S`7HB?pC9~YUAv`%Oz_Q&ZmbG1Vsd+0_mYw9d;L)*C5ucQpkH$Bk0 zZfebJe=I;BXhMB<`bpYzikzVNE!=hKpI%b=|~gK?B+`KkoJW{eHjafc`jr=tH0UY<~QV z0ZrTd0CxNRe!ti2^?DY#@H(NX!WO%(e;xpB`lH{qn0vj~0{u~Q*Bi~<-Db1d++Aiu zlc@rD+$TZB?)AFe*IhLPBaL+-_2G_J1QZGPcTrS0^ zXV<#l`!hm4UEX&H9RRI8n%NbAtR*zUXZ^knyWjtNgeRBGzMVM;TC4BQNNWL8X6%%j z#M200p}enzINEG}9egz#>YWX1o*3hk{`t8b`io*z>Yz1f^>Yt)XVS3}SLv-bfDpo{ zdw$-xVSoB{q(;#`6Iyc}1|2kKL7gc9006*%jK-th`EY1Ke-o|4RZ&`M{nm9ck2Pn&hF^64iO}P09lM!>d3Mr$tQhF>Mx6lsRIs?^(nQUTO+p~F~+jg zk!4wu?bJWSI1DH@>r6kZuI}h`)U_PIGE34c+XXR``p2hnz8Z0e77ndXr?p^QTyoyT>+~q~Cxxk~Ron5m^Dsz1EKKJQ z*?X(4^M|n(zl}}p9Q1Le4&5Ywo|u|n#8hVYbt`I-AD{9usoi_Gx7AP8ew>(kiQEqJ zxgquwq5k0?@l?3~C@9;}hA#OlF?FUSF)ke&7WuT0nA%IM+S|GM^Q40g+kW0~i2dc) zNvYMKqo8T1%Oy#EB{8D&^KdvE{t~Zg!3!!lVd*SMk~HZ2IUN4D8I!u& z@-qiLu6I&DPDVYqB?Ukaq@RYvf5xQdsx8%Xgshtfo3qe58^A0{gF)x}*wlRWf&WhE zHAq70a>-xg!C)|u27|%TQRn-AJ&9SERrmb2Z=Xo!knMMUnWcfjel+O(oGZko_MX%; zVlT;6)l7q-^FXqCxt-EZQfgiGH-@Ek66$Kj>t)HwJQxh(zS75}4kk`YUEK8K3@0y* zdty@RAnwsN$thXAw)hOWNb2Wn0<7Cug$$(}H?K;e=w;B(By;^M z_L|SkOz=F176qCsKlGE@CoV2xQ`3Cyf@TaF7@gfH&|LZ9$hCRTc2l^@!+ZeN)%E)r9V_o9qc+1vinpt*A1Z$t5ePC%XcweC|O1)L>Egyw3W zjV)_1pc8A&3%A4f?gY>l^_U^Gp>z^zPJH+q9mpI+m_dmQ&DA~|GgIBzB2GwM+x{?` zpW{JuwO_^omUhI;1cOiFem&Y$RV-+(_RHAM&==&7Jti+di%(q?8Yw^%YCo0TQco_D zP;=!wABK-q#fA2zzVL;fOp;LZ;&-i?Lo=w|mcF={OeT{jNro19vDpfmPf`;=yQvAV zKlJ2DqJwl^{O&em2!%9|;zK*B389;LLYR|Mb9~``qi%o4-PF)rt@+Zs3wmXqOeT}b zzmpCv)T*1$eBGg{s-_+$NKFQHDD!0ULE=#hz98&Bt9xbyRaI3JC~c$mWp+SMLCtY| zp}N^NH3qXpX=8f(bpD%#xabSB3oZy;h65 zBOx@$-+kdhp7dSQ#-AOCK}V>6m> r>VKVf8JBSxmvI@FaT%BK=EVO1?z;0|*$-yG00000NkvXXu0mjfhN6>_ literal 0 HcmV?d00001