From c158b826f0443eaa5a139b959b46806195f6ddf1 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:16:33 +0800 Subject: [PATCH] feat: add Flagsmith app --- apps/flagsmith/2.257.0/.env.sample | 13 ++ apps/flagsmith/2.257.0/data.yml | 208 ++++++++++++++++++++ apps/flagsmith/2.257.0/data/.gitkeep | 1 + apps/flagsmith/2.257.0/docker-compose.yml | 104 ++++++++++ apps/flagsmith/2.257.0/scripts/init.sh | 67 +++++++ apps/flagsmith/2.257.0/scripts/uninstall.sh | 8 + apps/flagsmith/2.257.0/scripts/upgrade.sh | 5 + apps/flagsmith/README.md | 52 +++++ apps/flagsmith/data.yml | 33 ++++ apps/flagsmith/latest/.env.sample | 13 ++ apps/flagsmith/latest/data.yml | 208 ++++++++++++++++++++ apps/flagsmith/latest/data/.gitkeep | 1 + apps/flagsmith/latest/docker-compose.yml | 104 ++++++++++ apps/flagsmith/latest/scripts/init.sh | 67 +++++++ apps/flagsmith/latest/scripts/uninstall.sh | 8 + apps/flagsmith/latest/scripts/upgrade.sh | 5 + apps/flagsmith/logo.png | Bin 0 -> 6805 bytes 17 files changed, 897 insertions(+) create mode 100644 apps/flagsmith/2.257.0/.env.sample create mode 100644 apps/flagsmith/2.257.0/data.yml create mode 100644 apps/flagsmith/2.257.0/data/.gitkeep create mode 100644 apps/flagsmith/2.257.0/docker-compose.yml create mode 100755 apps/flagsmith/2.257.0/scripts/init.sh create mode 100755 apps/flagsmith/2.257.0/scripts/uninstall.sh create mode 100755 apps/flagsmith/2.257.0/scripts/upgrade.sh create mode 100644 apps/flagsmith/README.md create mode 100644 apps/flagsmith/data.yml create mode 100644 apps/flagsmith/latest/.env.sample create mode 100644 apps/flagsmith/latest/data.yml create mode 100644 apps/flagsmith/latest/data/.gitkeep create mode 100644 apps/flagsmith/latest/docker-compose.yml create mode 100755 apps/flagsmith/latest/scripts/init.sh create mode 100755 apps/flagsmith/latest/scripts/uninstall.sh create mode 100755 apps/flagsmith/latest/scripts/upgrade.sh create mode 100644 apps/flagsmith/logo.png diff --git a/apps/flagsmith/2.257.0/.env.sample b/apps/flagsmith/2.257.0/.env.sample new file mode 100644 index 000000000..451b1f4d9 --- /dev/null +++ b/apps/flagsmith/2.257.0/.env.sample @@ -0,0 +1,13 @@ +CONTAINER_NAME="flagsmith" +PANEL_APP_BIND_ADDRESS="127.0.0.1" +PANEL_APP_PORT_HTTP="8000" +PANEL_DB_TYPE="postgresql" +PANEL_DB_HOST="postgresql" +PANEL_DB_PORT="5432" +PANEL_DB_NAME="flagsmith" +PANEL_DB_USER="flagsmith" +PANEL_DB_USER_PASSWORD="change-me" +DJANGO_SECRET_KEY="generate" +FLAGSMITH_DOMAIN="localhost:8000" +FLAGSMITH_ALLOWED_HOSTS="*" +PREVENT_SIGNUP="false" diff --git a/apps/flagsmith/2.257.0/data.yml b/apps/flagsmith/2.257.0/data.yml new file mode 100644 index 000000000..ab06122ed --- /dev/null +++ b/apps/flagsmith/2.257.0/data.yml @@ -0,0 +1,208 @@ +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: 8000 + 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: postgresql + envKey: PANEL_DB_TYPE + labelEn: Database Runtime + labelZh: 数据库运行环境 + label: + en: Database Runtime + zh: 数据库运行环境 + zh-Hant: 資料庫執行環境 + ja: データベース実行環境 + ko: 데이터베이스 런타임 + ru: Среда базы данных + ms: Masa jalan pangkalan data + pt-br: Ambiente de banco de dados + required: true + type: apps + values: + - label: PostgreSQL + value: postgresql + child: + default: "" + envKey: PANEL_DB_HOST + labelEn: PostgreSQL Service + labelZh: PostgreSQL 服务 + label: + en: PostgreSQL Service + zh: PostgreSQL 服务 + zh-Hant: PostgreSQL 服務 + ja: PostgreSQL サービス + ko: PostgreSQL 서비스 + ru: Сервис PostgreSQL + ms: Perkhidmatan PostgreSQL + pt-br: Servico PostgreSQL + required: true + type: service + - default: 5432 + edit: true + envKey: PANEL_DB_PORT + labelEn: Database Port + labelZh: 数据库端口 + label: + en: Database Port + zh: 数据库端口 + zh-Hant: 資料庫連接埠 + ja: データベースポート + ko: 데이터베이스 포트 + ru: Порт базы данных + ms: Port pangkalan data + pt-br: Porta do banco de dados + required: true + rule: paramPort + type: number + - default: flagsmith + edit: true + envKey: PANEL_DB_NAME + 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: flagsmith + edit: true + envKey: PANEL_DB_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: "" + edit: true + envKey: PANEL_DB_USER_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 + rule: paramComplexity + type: password + - default: generate + edit: true + envKey: DJANGO_SECRET_KEY + labelEn: Application Secret + labelZh: 应用密钥 + label: + en: Application Secret + zh: 应用密钥 + zh-Hant: 應用程式密鑰 + ja: アプリケーションシークレット + ko: 애플리케이션 비밀 키 + ru: Секрет приложения + ms: Rahsia aplikasi + pt-br: Segredo do aplicativo + required: true + type: password + - default: localhost:8000 + edit: true + envKey: FLAGSMITH_DOMAIN + labelEn: Public Host + labelZh: 公开访问主机 + label: + en: Public Host + zh: 公开访问主机 + zh-Hant: 公開存取主機 + ja: 公開ホスト + ko: 공개 호스트 + ru: Публичный хост + ms: Hos awam + pt-br: Host publico + required: true + type: text + - default: "*" + edit: true + envKey: FLAGSMITH_ALLOWED_HOSTS + labelEn: Allowed Hosts + labelZh: 允许的主机 + label: + en: Allowed Hosts + zh: 允许的主机 + zh-Hant: 允許的主機 + ja: 許可するホスト + ko: 허용된 호스트 + ru: Разрешенные хосты + ms: Hos yang dibenarkan + pt-br: Hosts permitidos + required: true + type: text + - default: "false" + edit: true + envKey: PREVENT_SIGNUP + labelEn: Prevent New Signups + labelZh: 禁止新用户注册 + label: + en: Prevent New Signups + zh: 禁止新用户注册 + zh-Hant: 禁止新使用者註冊 + ja: 新規登録を禁止 + ko: 신규 가입 방지 + ru: Запретить новые регистрации + ms: Halang pendaftaran baharu + pt-br: Impedir novos cadastros + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" diff --git a/apps/flagsmith/2.257.0/data/.gitkeep b/apps/flagsmith/2.257.0/data/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/flagsmith/2.257.0/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/apps/flagsmith/2.257.0/docker-compose.yml b/apps/flagsmith/2.257.0/docker-compose.yml new file mode 100644 index 000000000..3b8c38dd3 --- /dev/null +++ b/apps/flagsmith/2.257.0/docker-compose.yml @@ -0,0 +1,104 @@ +services: + flagsmith-migrate: + image: "flagsmith/flagsmith:2.257.0@sha256:e3b2cfca0dd268cc7af320becfd3a3ed928a58ae424c5c69b00a600bdf2a36b3" + container_name: ${CONTAINER_NAME}-migrate + restart: unless-stopped + entrypoint: ["/bin/sh", "-c"] + command: + - flagsmith migrate && touch /tmp/flagsmith-migration-complete && exec sleep infinity + networks: + - 1panel-network + environment: + DJANGO_DB_HOST: ${PANEL_DB_HOST} + DJANGO_DB_PORT: ${PANEL_DB_PORT} + DJANGO_DB_NAME: ${PANEL_DB_NAME} + DJANGO_DB_USER: ${PANEL_DB_USER} + DJANGO_DB_PASSWORD: ${PANEL_DB_USER_PASSWORD} + USE_POSTGRES_FOR_ANALYTICS: "true" + ENVIRONMENT: production + DJANGO_ALLOWED_HOSTS: ${FLAGSMITH_ALLOWED_HOSTS} + FLAGSMITH_DOMAIN: ${FLAGSMITH_DOMAIN} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} + PREVENT_SIGNUP: ${PREVENT_SIGNUP} + TASK_RUN_METHOD: TASK_PROCESSOR + PROMETHEUS_ENABLED: "true" + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/flagsmith-migration-complete"] + interval: 5s + timeout: 2s + start_period: 20s + retries: 60 + security_opt: + - no-new-privileges:true + labels: + createdBy: "Apps" + + flagsmith: + image: "flagsmith/flagsmith:2.257.0@sha256:e3b2cfca0dd268cc7af320becfd3a3ed928a58ae424c5c69b00a600bdf2a36b3" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + command: ["serve"] + depends_on: + flagsmith-migrate: + condition: service_healthy + networks: + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8000" + environment: + DJANGO_DB_HOST: ${PANEL_DB_HOST} + DJANGO_DB_PORT: ${PANEL_DB_PORT} + DJANGO_DB_NAME: ${PANEL_DB_NAME} + DJANGO_DB_USER: ${PANEL_DB_USER} + DJANGO_DB_PASSWORD: ${PANEL_DB_USER_PASSWORD} + USE_POSTGRES_FOR_ANALYTICS: "true" + ENVIRONMENT: production + DJANGO_ALLOWED_HOSTS: ${FLAGSMITH_ALLOWED_HOSTS} + FLAGSMITH_DOMAIN: ${FLAGSMITH_DOMAIN} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} + PREVENT_SIGNUP: ${PREVENT_SIGNUP} + TASK_RUN_METHOD: TASK_PROCESSOR + PROMETHEUS_ENABLED: "true" + healthcheck: + test: ["CMD", "flagsmith", "healthcheck", "tcp"] + interval: 10s + timeout: 5s + start_period: 60s + retries: 6 + security_opt: + - no-new-privileges:true + labels: + createdBy: "Apps" + + flagsmith-task-processor: + image: "flagsmith/flagsmith:2.257.0@sha256:e3b2cfca0dd268cc7af320becfd3a3ed928a58ae424c5c69b00a600bdf2a36b3" + container_name: ${CONTAINER_NAME}-task-processor + restart: unless-stopped + command: ["run-task-processor"] + depends_on: + flagsmith-migrate: + condition: service_healthy + networks: + - 1panel-network + environment: + DJANGO_DB_HOST: ${PANEL_DB_HOST} + DJANGO_DB_PORT: ${PANEL_DB_PORT} + DJANGO_DB_NAME: ${PANEL_DB_NAME} + DJANGO_DB_USER: ${PANEL_DB_USER} + DJANGO_DB_PASSWORD: ${PANEL_DB_USER_PASSWORD} + USE_POSTGRES_FOR_ANALYTICS: "true" + ENVIRONMENT: production + DJANGO_ALLOWED_HOSTS: ${FLAGSMITH_ALLOWED_HOSTS} + FLAGSMITH_DOMAIN: ${FLAGSMITH_DOMAIN} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} + PREVENT_SIGNUP: ${PREVENT_SIGNUP} + TASK_RUN_METHOD: TASK_PROCESSOR + PROMETHEUS_ENABLED: "true" + security_opt: + - no-new-privileges:true + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/flagsmith/2.257.0/scripts/init.sh b/apps/flagsmith/2.257.0/scripts/init.sh new file mode 100755 index 000000000..3f3cb2225 --- /dev/null +++ b/apps/flagsmith/2.257.0/scripts/init.sh @@ -0,0 +1,67 @@ +#!/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" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ROOT_DIR}/.flagsmith-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" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + +domain="$(read_env_value FLAGSMITH_DOMAIN)" +[[ "$domain" =~ ^[A-Za-z0-9._:-]+$ ]] || fail "FLAGSMITH_DOMAIN must be a host with an optional port and no scheme or path" + +allowed_hosts="$(read_env_value FLAGSMITH_ALLOWED_HOSTS)" +[[ "$allowed_hosts" =~ ^[A-Za-z0-9.*_:-]+(,[A-Za-z0-9.*_:-]+)*$ ]] || fail "FLAGSMITH_ALLOWED_HOSTS must be a comma-separated host list" + +prevent_signup="$(read_env_value PREVENT_SIGNUP)" +[[ "$prevent_signup" == "true" || "$prevent_signup" == "false" ]] || fail "PREVENT_SIGNUP must be true or false" + +secret="$(read_env_value DJANGO_SECRET_KEY)" +if [[ -z "$secret" || "$secret" == "generate" ]]; then + command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate DJANGO_SECRET_KEY" + secret="$(openssl rand -hex 32)" +fi +[[ ${#secret} -ge 50 ]] || fail "DJANGO_SECRET_KEY must contain at least 50 characters" +[[ "$secret" =~ ^[A-Za-z0-9._~!@%+=:,/-]+$ ]] || fail "DJANGO_SECRET_KEY contains unsupported dotenv characters" + +chmod 600 "$ENV_FILE" +set_env_value DJANGO_SECRET_KEY "$secret" diff --git a/apps/flagsmith/2.257.0/scripts/uninstall.sh b/apps/flagsmith/2.257.0/scripts/uninstall.sh new file mode 100755 index 000000000..5c199242b --- /dev/null +++ b/apps/flagsmith/2.257.0/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/flagsmith/2.257.0/scripts/upgrade.sh b/apps/flagsmith/2.257.0/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/flagsmith/2.257.0/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/flagsmith/README.md b/apps/flagsmith/README.md new file mode 100644 index 000000000..842a3a04f --- /dev/null +++ b/apps/flagsmith/README.md @@ -0,0 +1,52 @@ +# Flagsmith + +## 产品介绍 + +Flagsmith 是开源的功能标志与远程配置平台,可集中管理功能开关、环境、身份和分段规则,并通过 SDK 或 API 向应用提供配置。 + +## 主要功能 + +- 为多个项目和环境管理功能标志 +- 按身份、特征和分段规则逐步发布功能 +- 通过客户端、服务端 SDK 和 REST API 获取标志 +- 记录标志分析数据并运行后台任务 + +## 访问说明 + +- 安装前需在 1Panel 中准备一个正在运行的 PostgreSQL 服务,并在表单中选择它。1Panel 会创建并关联 Flagsmith 使用的数据库和用户。 +- 安装后访问 `http://<服务器 IP>:8000`,实际端口以 `PANEL_APP_PORT_HTTP` 为准,然后创建首个账户。 +- `FLAGSMITH_DOMAIN` 填写浏览器实际访问的主机和可选端口,不包含 `http://` 或路径。例如 `flags.example.com` 或 `192.0.2.10:8000`。 +- 公网部署应通过 HTTPS 反向代理访问,并将 `FLAGSMITH_ALLOWED_HOSTS` 限制为实际域名。 + +## 拓扑和数据 + +本包保留官方 Compose 的完整应用拓扑:独立数据库迁移门禁、Web/API 主服务和独立任务处理器。三个角色使用同一官方镜像,迁移成功后门禁容器进入空闲状态,另外两个服务才会启动。迁移容器保持运行是为了兼容 1Panel 将成功退出的一次性容器仍标记为异常的状态模型;它不对外提供服务或重复执行任务。Redis 仅用于可选的高级缓存或事件摄取配置,不是官方基础 Compose 的必需依赖。 + +业务数据保存在所选 PostgreSQL 服务中。升级或卸载前请备份该数据库;卸载应用不会删除外部 PostgreSQL 服务本身。1Panel 当前可能将关联 PostgreSQL 用户创建为超级用户,因此共享实例上的关联用户不是强隔离边界。面向不受信任用户或高价值数据时,应使用专用 PostgreSQL 实例。 + +## 安全说明 + +- `DJANGO_SECRET_KEY` 在首次安装时生成并保存在应用 `.env` 中,升级时保持不变。不要提交或共享安装后的 `.env`。 +- 默认允许首个及后续用户注册。完成管理员初始化后,可将 `PREVENT_SIGNUP` 改为 `true`。 +- Google OAuth、SAML、SMTP、Webhook 和其他外部集成均为可选功能。启用时应使用最小权限凭据,并在升级前重新检查镜像安全公告。 +- 应用镜像以非 root 用户运行,只有 Web/API 主服务向主机发布端口;迁移和任务处理器不发布主机端口。 + +## Introduction + +Flagsmith is an open-source feature flag and remote configuration platform. This package preserves the official migration gate, Web/API service, and task-processor topology while using a 1Panel-managed PostgreSQL service. The migration container stays idle after a successful migration because 1Panel otherwise treats a successfully exited one-shot Compose container as unhealthy. + +## Features + +- Manage feature flags across projects and environments +- Target identities and segments for gradual releases +- Serve configuration through client SDKs, server SDKs, and REST APIs +- Store flag analytics and process background tasks + +Choose a running PostgreSQL service during installation. Back up the linked database before upgrades or uninstalling. A 1Panel-linked PostgreSQL role may currently be a superuser, so use a dedicated PostgreSQL instance where stronger isolation is required. Set `FLAGSMITH_DOMAIN` to the public host with an optional port, and use an HTTPS reverse proxy for Internet-facing deployments. + +## References + +- Website: +- Source: +- Documentation: +- License: BSD-3-Clause diff --git a/apps/flagsmith/data.yml b/apps/flagsmith/data.yml new file mode 100644 index 000000000..ae41e635a --- /dev/null +++ b/apps/flagsmith/data.yml @@ -0,0 +1,33 @@ +name: Flagsmith +tags: + - 开发工具 + - 项目管理 +title: 开源功能标志和远程配置平台 +description: 开源功能标志和远程配置平台 +additionalProperties: + key: flagsmith + name: Flagsmith + tags: + - DevTool + - Tool + shortDescZh: 开源功能标志和远程配置平台 + shortDescEn: Open-source feature flag and remote configuration platform + description: + en: Open-source feature flag and remote configuration platform + zh: 开源功能标志和远程配置平台 + zh-Hant: 開源功能旗標與遠端設定平台 + ja: オープンソースの機能フラグおよびリモート設定プラットフォーム + ko: 오픈 소스 기능 플래그 및 원격 구성 플랫폼 + ru: Платформа с открытым исходным кодом для флагов функций и удаленной конфигурации + ms: Platform sumber terbuka untuk bendera ciri dan konfigurasi jauh + pt-br: Plataforma de codigo aberto para sinalizadores de recursos e configuracao remota + type: tool + crossVersionUpdate: true + limit: 0 + recommend: 0 + website: https://www.flagsmith.com/ + github: https://github.com/Flagsmith/flagsmith + document: https://docs.flagsmith.com/deployment-self-hosting/hosting-guides/docker + architectures: + - amd64 + - arm64 diff --git a/apps/flagsmith/latest/.env.sample b/apps/flagsmith/latest/.env.sample new file mode 100644 index 000000000..451b1f4d9 --- /dev/null +++ b/apps/flagsmith/latest/.env.sample @@ -0,0 +1,13 @@ +CONTAINER_NAME="flagsmith" +PANEL_APP_BIND_ADDRESS="127.0.0.1" +PANEL_APP_PORT_HTTP="8000" +PANEL_DB_TYPE="postgresql" +PANEL_DB_HOST="postgresql" +PANEL_DB_PORT="5432" +PANEL_DB_NAME="flagsmith" +PANEL_DB_USER="flagsmith" +PANEL_DB_USER_PASSWORD="change-me" +DJANGO_SECRET_KEY="generate" +FLAGSMITH_DOMAIN="localhost:8000" +FLAGSMITH_ALLOWED_HOSTS="*" +PREVENT_SIGNUP="false" diff --git a/apps/flagsmith/latest/data.yml b/apps/flagsmith/latest/data.yml new file mode 100644 index 000000000..ab06122ed --- /dev/null +++ b/apps/flagsmith/latest/data.yml @@ -0,0 +1,208 @@ +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: 8000 + 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: postgresql + envKey: PANEL_DB_TYPE + labelEn: Database Runtime + labelZh: 数据库运行环境 + label: + en: Database Runtime + zh: 数据库运行环境 + zh-Hant: 資料庫執行環境 + ja: データベース実行環境 + ko: 데이터베이스 런타임 + ru: Среда базы данных + ms: Masa jalan pangkalan data + pt-br: Ambiente de banco de dados + required: true + type: apps + values: + - label: PostgreSQL + value: postgresql + child: + default: "" + envKey: PANEL_DB_HOST + labelEn: PostgreSQL Service + labelZh: PostgreSQL 服务 + label: + en: PostgreSQL Service + zh: PostgreSQL 服务 + zh-Hant: PostgreSQL 服務 + ja: PostgreSQL サービス + ko: PostgreSQL 서비스 + ru: Сервис PostgreSQL + ms: Perkhidmatan PostgreSQL + pt-br: Servico PostgreSQL + required: true + type: service + - default: 5432 + edit: true + envKey: PANEL_DB_PORT + labelEn: Database Port + labelZh: 数据库端口 + label: + en: Database Port + zh: 数据库端口 + zh-Hant: 資料庫連接埠 + ja: データベースポート + ko: 데이터베이스 포트 + ru: Порт базы данных + ms: Port pangkalan data + pt-br: Porta do banco de dados + required: true + rule: paramPort + type: number + - default: flagsmith + edit: true + envKey: PANEL_DB_NAME + 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: flagsmith + edit: true + envKey: PANEL_DB_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: "" + edit: true + envKey: PANEL_DB_USER_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 + rule: paramComplexity + type: password + - default: generate + edit: true + envKey: DJANGO_SECRET_KEY + labelEn: Application Secret + labelZh: 应用密钥 + label: + en: Application Secret + zh: 应用密钥 + zh-Hant: 應用程式密鑰 + ja: アプリケーションシークレット + ko: 애플리케이션 비밀 키 + ru: Секрет приложения + ms: Rahsia aplikasi + pt-br: Segredo do aplicativo + required: true + type: password + - default: localhost:8000 + edit: true + envKey: FLAGSMITH_DOMAIN + labelEn: Public Host + labelZh: 公开访问主机 + label: + en: Public Host + zh: 公开访问主机 + zh-Hant: 公開存取主機 + ja: 公開ホスト + ko: 공개 호스트 + ru: Публичный хост + ms: Hos awam + pt-br: Host publico + required: true + type: text + - default: "*" + edit: true + envKey: FLAGSMITH_ALLOWED_HOSTS + labelEn: Allowed Hosts + labelZh: 允许的主机 + label: + en: Allowed Hosts + zh: 允许的主机 + zh-Hant: 允許的主機 + ja: 許可するホスト + ko: 허용된 호스트 + ru: Разрешенные хосты + ms: Hos yang dibenarkan + pt-br: Hosts permitidos + required: true + type: text + - default: "false" + edit: true + envKey: PREVENT_SIGNUP + labelEn: Prevent New Signups + labelZh: 禁止新用户注册 + label: + en: Prevent New Signups + zh: 禁止新用户注册 + zh-Hant: 禁止新使用者註冊 + ja: 新規登録を禁止 + ko: 신규 가입 방지 + ru: Запретить новые регистрации + ms: Halang pendaftaran baharu + pt-br: Impedir novos cadastros + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" diff --git a/apps/flagsmith/latest/data/.gitkeep b/apps/flagsmith/latest/data/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/flagsmith/latest/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/apps/flagsmith/latest/docker-compose.yml b/apps/flagsmith/latest/docker-compose.yml new file mode 100644 index 000000000..de86bfbd0 --- /dev/null +++ b/apps/flagsmith/latest/docker-compose.yml @@ -0,0 +1,104 @@ +services: + flagsmith-migrate: + image: "flagsmith/flagsmith:latest@sha256:e3b2cfca0dd268cc7af320becfd3a3ed928a58ae424c5c69b00a600bdf2a36b3" + container_name: ${CONTAINER_NAME}-migrate + restart: unless-stopped + entrypoint: ["/bin/sh", "-c"] + command: + - flagsmith migrate && touch /tmp/flagsmith-migration-complete && exec sleep infinity + networks: + - 1panel-network + environment: + DJANGO_DB_HOST: ${PANEL_DB_HOST} + DJANGO_DB_PORT: ${PANEL_DB_PORT} + DJANGO_DB_NAME: ${PANEL_DB_NAME} + DJANGO_DB_USER: ${PANEL_DB_USER} + DJANGO_DB_PASSWORD: ${PANEL_DB_USER_PASSWORD} + USE_POSTGRES_FOR_ANALYTICS: "true" + ENVIRONMENT: production + DJANGO_ALLOWED_HOSTS: ${FLAGSMITH_ALLOWED_HOSTS} + FLAGSMITH_DOMAIN: ${FLAGSMITH_DOMAIN} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} + PREVENT_SIGNUP: ${PREVENT_SIGNUP} + TASK_RUN_METHOD: TASK_PROCESSOR + PROMETHEUS_ENABLED: "true" + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/flagsmith-migration-complete"] + interval: 5s + timeout: 2s + start_period: 20s + retries: 60 + security_opt: + - no-new-privileges:true + labels: + createdBy: "Apps" + + flagsmith: + image: "flagsmith/flagsmith:latest@sha256:e3b2cfca0dd268cc7af320becfd3a3ed928a58ae424c5c69b00a600bdf2a36b3" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + command: ["serve"] + depends_on: + flagsmith-migrate: + condition: service_healthy + networks: + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8000" + environment: + DJANGO_DB_HOST: ${PANEL_DB_HOST} + DJANGO_DB_PORT: ${PANEL_DB_PORT} + DJANGO_DB_NAME: ${PANEL_DB_NAME} + DJANGO_DB_USER: ${PANEL_DB_USER} + DJANGO_DB_PASSWORD: ${PANEL_DB_USER_PASSWORD} + USE_POSTGRES_FOR_ANALYTICS: "true" + ENVIRONMENT: production + DJANGO_ALLOWED_HOSTS: ${FLAGSMITH_ALLOWED_HOSTS} + FLAGSMITH_DOMAIN: ${FLAGSMITH_DOMAIN} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} + PREVENT_SIGNUP: ${PREVENT_SIGNUP} + TASK_RUN_METHOD: TASK_PROCESSOR + PROMETHEUS_ENABLED: "true" + healthcheck: + test: ["CMD", "flagsmith", "healthcheck", "tcp"] + interval: 10s + timeout: 5s + start_period: 60s + retries: 6 + security_opt: + - no-new-privileges:true + labels: + createdBy: "Apps" + + flagsmith-task-processor: + image: "flagsmith/flagsmith:latest@sha256:e3b2cfca0dd268cc7af320becfd3a3ed928a58ae424c5c69b00a600bdf2a36b3" + container_name: ${CONTAINER_NAME}-task-processor + restart: unless-stopped + command: ["run-task-processor"] + depends_on: + flagsmith-migrate: + condition: service_healthy + networks: + - 1panel-network + environment: + DJANGO_DB_HOST: ${PANEL_DB_HOST} + DJANGO_DB_PORT: ${PANEL_DB_PORT} + DJANGO_DB_NAME: ${PANEL_DB_NAME} + DJANGO_DB_USER: ${PANEL_DB_USER} + DJANGO_DB_PASSWORD: ${PANEL_DB_USER_PASSWORD} + USE_POSTGRES_FOR_ANALYTICS: "true" + ENVIRONMENT: production + DJANGO_ALLOWED_HOSTS: ${FLAGSMITH_ALLOWED_HOSTS} + FLAGSMITH_DOMAIN: ${FLAGSMITH_DOMAIN} + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} + PREVENT_SIGNUP: ${PREVENT_SIGNUP} + TASK_RUN_METHOD: TASK_PROCESSOR + PROMETHEUS_ENABLED: "true" + security_opt: + - no-new-privileges:true + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/flagsmith/latest/scripts/init.sh b/apps/flagsmith/latest/scripts/init.sh new file mode 100755 index 000000000..3f3cb2225 --- /dev/null +++ b/apps/flagsmith/latest/scripts/init.sh @@ -0,0 +1,67 @@ +#!/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" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ROOT_DIR}/.flagsmith-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" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + +domain="$(read_env_value FLAGSMITH_DOMAIN)" +[[ "$domain" =~ ^[A-Za-z0-9._:-]+$ ]] || fail "FLAGSMITH_DOMAIN must be a host with an optional port and no scheme or path" + +allowed_hosts="$(read_env_value FLAGSMITH_ALLOWED_HOSTS)" +[[ "$allowed_hosts" =~ ^[A-Za-z0-9.*_:-]+(,[A-Za-z0-9.*_:-]+)*$ ]] || fail "FLAGSMITH_ALLOWED_HOSTS must be a comma-separated host list" + +prevent_signup="$(read_env_value PREVENT_SIGNUP)" +[[ "$prevent_signup" == "true" || "$prevent_signup" == "false" ]] || fail "PREVENT_SIGNUP must be true or false" + +secret="$(read_env_value DJANGO_SECRET_KEY)" +if [[ -z "$secret" || "$secret" == "generate" ]]; then + command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate DJANGO_SECRET_KEY" + secret="$(openssl rand -hex 32)" +fi +[[ ${#secret} -ge 50 ]] || fail "DJANGO_SECRET_KEY must contain at least 50 characters" +[[ "$secret" =~ ^[A-Za-z0-9._~!@%+=:,/-]+$ ]] || fail "DJANGO_SECRET_KEY contains unsupported dotenv characters" + +chmod 600 "$ENV_FILE" +set_env_value DJANGO_SECRET_KEY "$secret" diff --git a/apps/flagsmith/latest/scripts/uninstall.sh b/apps/flagsmith/latest/scripts/uninstall.sh new file mode 100755 index 000000000..5c199242b --- /dev/null +++ b/apps/flagsmith/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 --remove-orphans +else + docker compose down --remove-orphans +fi diff --git a/apps/flagsmith/latest/scripts/upgrade.sh b/apps/flagsmith/latest/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/flagsmith/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/flagsmith/logo.png b/apps/flagsmith/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4749009862848e097801ffc9014a2a94965e8384 GIT binary patch literal 6805 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