mirror of
https://github.com/Melorise/nPanel.git
synced 2026-11-07 14:49:35 +00:00
221 lines
7.0 KiB
Nix
221 lines
7.0 KiB
Nix
{ self }:
|
||
{ config, lib, pkgs, ... }:
|
||
|
||
let
|
||
inherit (lib) mkEnableOption mkIf mkOption types;
|
||
cfg = config.services.nPanel;
|
||
|
||
packageForSystem = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||
toolPath = lib.makeBinPath [
|
||
pkgs.bash
|
||
pkgs.coreutils
|
||
pkgs.curl
|
||
pkgs.docker
|
||
pkgs.docker-compose
|
||
pkgs.findutils
|
||
pkgs.gnugrep
|
||
pkgs.gnused
|
||
pkgs.gnutar
|
||
pkgs.gzip
|
||
pkgs.iproute2
|
||
pkgs.openssl
|
||
pkgs.procps
|
||
pkgs.systemd
|
||
pkgs.util-linux
|
||
pkgs.which
|
||
];
|
||
|
||
# This file is intentionally mutable. It holds nPanel bootstrap state below
|
||
# stateDir and never enters the Nix store.
|
||
initialise = pkgs.writeShellScript "npanel-initialise" ''
|
||
set -euo pipefail
|
||
|
||
state_dir=${lib.escapeShellArg cfg.stateDir}
|
||
control_file="$state_dir/npanel.conf"
|
||
password_file=${lib.escapeShellArg (if cfg.initialPasswordFile == null then "" else cfg.initialPasswordFile)}
|
||
|
||
install -d -m 0750 "$state_dir"
|
||
if [ ! -s "$control_file" ]; then
|
||
if [ -n "$password_file" ]; then
|
||
password="$(cat "$password_file")"
|
||
if [ -z "$password" ]; then
|
||
echo "nPanel: initialPasswordFile is empty" >&2
|
||
exit 1
|
||
fi
|
||
else
|
||
# `head` deliberately closes the random stream after 32 bytes. Do not
|
||
# let that expected SIGPIPE be promoted by the script-wide pipefail.
|
||
password="$(set +o pipefail; tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)"
|
||
if [ "''${#password}" -ne 32 ]; then
|
||
echo "nPanel: failed to generate an initial password" >&2
|
||
exit 1
|
||
fi
|
||
(umask 077; printf '%s\n' "$password" > "$state_dir/initial-password")
|
||
echo "nPanel created a one-time bootstrap password in $state_dir/initial-password" >&2
|
||
fi
|
||
|
||
umask 077
|
||
cat > "$control_file" <<EOF
|
||
BASE_DIR=/var/lib/npanel
|
||
ORIGINAL_PORT=${toString cfg.port}
|
||
ORIGINAL_VERSION=${cfg.package.version or "unknown"}
|
||
ORIGINAL_USERNAME=${cfg.initialUser}
|
||
ORIGINAL_PASSWORD=$password
|
||
ORIGINAL_ENTRANCE=${cfg.securityEntrance}
|
||
LANGUAGE=${cfg.language}
|
||
PANEL_EDITION=cn
|
||
EOF
|
||
fi
|
||
|
||
# These are declarative NixOS settings. They must be rewritten on every
|
||
# start so a panel-side database edit cannot survive the next activation.
|
||
update_control_value() {
|
||
key="$1"
|
||
value="$2"
|
||
tmp="$(mktemp "$state_dir/.npanel.conf.XXXXXX")"
|
||
if grep -q "^$key=" "$control_file"; then
|
||
sed "s|^$key=.*|$key=$value|" "$control_file" > "$tmp"
|
||
else
|
||
cat "$control_file" > "$tmp"
|
||
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
||
fi
|
||
chmod 0600 "$tmp"
|
||
mv "$tmp" "$control_file"
|
||
}
|
||
|
||
update_control_value ORIGINAL_PORT ${lib.escapeShellArg (toString cfg.port)}
|
||
update_control_value ORIGINAL_ENTRANCE ${lib.escapeShellArg cfg.securityEntrance}
|
||
update_control_value MANAGED_PORT ${lib.escapeShellArg (toString cfg.port)}
|
||
update_control_value MANAGED_ENTRANCE ${lib.escapeShellArg cfg.securityEntrance}
|
||
'';
|
||
|
||
commonServiceConfig = {
|
||
User = "root";
|
||
Group = "root";
|
||
SupplementaryGroups = [ "docker" ];
|
||
WorkingDirectory = "/var/lib/npanel";
|
||
Environment = [ "PATH=${toolPath}" ];
|
||
|
||
# The process sees fixed compatibility paths. Custom locations are bound
|
||
# into that view; default locations need no self-bind mount.
|
||
BindPaths =
|
||
lib.optionals (cfg.stateDir != "/var/lib/npanel") [
|
||
"${cfg.stateDir}:/var/lib/npanel"
|
||
]
|
||
++ lib.optionals (cfg.runtimeDir != "/run/npanel") [
|
||
"${cfg.runtimeDir}:/run/npanel"
|
||
];
|
||
|
||
Restart = "on-failure";
|
||
RestartSec = "5s";
|
||
UMask = "0077";
|
||
PrivateTmp = true;
|
||
ProtectSystem = "full";
|
||
ProtectHome = false;
|
||
PrivateDevices = false;
|
||
NoNewPrivileges = false;
|
||
};
|
||
in
|
||
{
|
||
options.services.nPanel = {
|
||
enable = mkEnableOption "nPanel 单机 Docker 运维面板";
|
||
|
||
package = mkOption {
|
||
type = types.package;
|
||
default = packageForSystem;
|
||
defaultText = lib.literalExpression "self.packages.\${pkgs.system}.default";
|
||
description = "nPanel core 软件包。agent 与前端从该软件包的 passthru 属性取得。";
|
||
};
|
||
|
||
stateDir = mkOption {
|
||
type = types.str;
|
||
default = "/var/lib/npanel";
|
||
description = "nPanel 数据库、Compose 项目、上传、备份和应用运行时数据的可变目录。";
|
||
};
|
||
|
||
runtimeDir = mkOption {
|
||
type = types.str;
|
||
default = "/run/npanel";
|
||
description = "core 与 agent 的本地 Unix socket 等易失运行时数据目录。";
|
||
};
|
||
|
||
port = mkOption {
|
||
type = types.port;
|
||
default = 9999;
|
||
description = "nPanel Web 服务监听端口。此值由 NixOS 持续接管,不能在面板中修改。";
|
||
};
|
||
|
||
securityEntrance = mkOption {
|
||
type = types.strMatching "[A-Za-z0-9]{5,116}";
|
||
example = "npanelAccess";
|
||
description = "nPanel 安全入口路径后缀(不含斜杠)。此值由 NixOS 持续接管,不能在面板中修改。";
|
||
};
|
||
|
||
initialUser = mkOption {
|
||
type = types.str;
|
||
default = "admin";
|
||
description = "首次初始化 nPanel 内部登录系统时使用的管理员名称。";
|
||
};
|
||
|
||
initialPasswordFile = mkOption {
|
||
type = types.nullOr types.str;
|
||
default = null;
|
||
example = "/run/secrets/npanel-initial-password";
|
||
description = "首次初始化密码文件。null 时生成一次性密码并以 0600 保存到 stateDir/initial-password。";
|
||
};
|
||
|
||
language = mkOption {
|
||
type = types.enum [ "zh" "en" ];
|
||
default = "zh";
|
||
description = "首次初始化时的界面语言。";
|
||
};
|
||
};
|
||
|
||
config = mkIf cfg.enable {
|
||
assertions = [
|
||
{
|
||
assertion = config.virtualisation.docker.enable;
|
||
message = "services.nPanel 需要 virtualisation.docker.enable = true;Docker 的声明式配置仍由用户维护。";
|
||
}
|
||
{
|
||
assertion = cfg.stateDir != cfg.runtimeDir;
|
||
message = "services.nPanel.stateDir 与 runtimeDir 必须是不同目录。";
|
||
}
|
||
{
|
||
assertion = cfg.securityEntrance != "";
|
||
message = "services.nPanel.securityEntrance 是必填项,且必须为 5 到 116 位字母或数字。";
|
||
}
|
||
];
|
||
|
||
systemd.tmpfiles.rules = [
|
||
"d ${cfg.stateDir} 0750 root root -"
|
||
"d ${cfg.runtimeDir} 0750 root root -"
|
||
];
|
||
|
||
systemd.services.npanel-agent = {
|
||
description = "nPanel privileged local agent";
|
||
wantedBy = [ "multi-user.target" ];
|
||
wants = [ "network-online.target" ];
|
||
after = [ "network-online.target" "docker.service" "systemd-tmpfiles-setup.service" ];
|
||
requires = [ "docker.service" ];
|
||
|
||
serviceConfig = commonServiceConfig // {
|
||
ExecStartPre = [ "${initialise}" ];
|
||
ExecStart = "${cfg.package.passthru.agent}/bin/npanel-agent";
|
||
};
|
||
};
|
||
|
||
systemd.services.npanel-core = {
|
||
description = "nPanel web core";
|
||
wantedBy = [ "multi-user.target" ];
|
||
after = [ "npanel-agent.service" ];
|
||
requires = [ "npanel-agent.service" ];
|
||
|
||
serviceConfig = commonServiceConfig // {
|
||
ExecStartPre = [ "${initialise}" ];
|
||
ExecStart = "${cfg.package}/bin/npanel-core";
|
||
};
|
||
};
|
||
};
|
||
}
|