From 377856140322015fb613da86872ddcc853d8fea8 Mon Sep 17 00:00:00 2001 From: Oskar Manhart <52569953+oskardotglobal@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:55:20 +0200 Subject: [PATCH 1/4] pelican-wings: init at 1.0.0-beta29 --- pkgs/by-name/pe/pelican-wings/package.nix | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 pkgs/by-name/pe/pelican-wings/package.nix diff --git a/pkgs/by-name/pe/pelican-wings/package.nix b/pkgs/by-name/pe/pelican-wings/package.nix new file mode 100644 index 0000000000000..fd98e930cddd5 --- /dev/null +++ b/pkgs/by-name/pe/pelican-wings/package.nix @@ -0,0 +1,67 @@ +{ + lib, + buildGoModule, + fetchFromGitHub, + testers, + runCommand, + nix-update-script, +}: +buildGoModule (finalAttrs: { + pname = "wings"; + version = "1.0.0-beta29"; + + src = fetchFromGitHub { + owner = "pelican"; + repo = "wings"; + tag = "v${finalAttrs.version}"; + hash = "sha256-rKrv3VfP9cHSJq4fPXBV5wzdTQP4slE88wqGFbGyr3A="; + }; + + vendorHash = "sha256-TCTlA+yvfxi0RH0etWJl7B6fbrKVuWZFRFvf7ejrfnA="; + + env.CGO_ENABLED = "0"; + + ldflags = [ + "-s" + "-w" + "-X github.com/pelican/wings/system.Version=${finalAttrs.version}" + ]; + + __structuredAttrs = true; + strictDeps = true; + + passthru = { + tests = { + version = testers.testVersion { + package = finalAttrs.finalPackage; + command = "wings version"; + version = "v${finalAttrs.version}"; + }; + help = runCommand "wings-help-test" { } '' + ${lib.getExe finalAttrs.finalPackage} --help + touch $out + ''; + }; + + updateScript = nix-update-script { + extraArgs = [ + "--version-regex" + "^v(.*-beta.*)$" + "--version=unstable" + ]; + }; + }; + + meta = { + mainProgram = "wings"; + maintainers = [ lib.maintainers.oskardotglobal ]; + homepage = "https://github.com/pelican/wings"; + changelog = "https://github.com/pelican/wings/releases/tag/${finalAttrs.version}"; + description = "Pelican's server control plane"; + license = lib.licenses.mit; + platforms = [ + "x86_64-linux" + "aarch64-linux" + ]; + }; +}) From 738bb4f9158c258a5f2169622443419df46816a6 Mon Sep 17 00:00:00 2001 From: Oskar Manhart <52569953+oskardotglobal@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:57:49 +0200 Subject: [PATCH 2/4] nixos/pelican-wings: init Co-authored-by: NullCube --- nixos/doc/manual/redirects.json | 9 ++ nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/pelican-wings.md | 99 +++++++++++++ nixos/modules/services/misc/pelican-wings.nix | 136 ++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 nixos/modules/services/misc/pelican-wings.md create mode 100644 nixos/modules/services/misc/pelican-wings.nix diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 6e8a587f02c41..4e1af62672b48 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -244,6 +244,15 @@ "module-virtualisation-xen-introduction": [ "index.html#module-virtualisation-xen-introduction" ], + "module-pelican-wings": [ + "index.html#module-pelican-wings" + ], + "module-pelican-wings-setup": [ + "index.html#module-pelican-wings-setup" + ], + "module-pelican-wings-traefik": [ + "index.html#module-pelican-wings-traefik" + ], "sec-boot-problems-scripted-stage-1": [ "index.html#sec-boot-problems-scripted-stage-1" ], diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 953343df682ed..8a2dc783b867c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -952,6 +952,7 @@ ./services/misc/packagekit.nix ./services/misc/paisa.nix ./services/misc/paperless.nix + ./services/misc/pelican-wings.nix ./services/misc/persistent-evdev.nix ./services/misc/pghero.nix ./services/misc/pinchflat.nix diff --git a/nixos/modules/services/misc/pelican-wings.md b/nixos/modules/services/misc/pelican-wings.md new file mode 100644 index 0000000000000..39a0434ed5612 --- /dev/null +++ b/nixos/modules/services/misc/pelican-wings.md @@ -0,0 +1,99 @@ +# Pelican Wings {#module-pelican-wings} + +Wings is the server backend for the Pelican Panel, responsible for running the actual game servers as Docker containers. +The panel is required to use it (and vice versa). To set up the panel, see the docs for [](#module-pelican-panel) + +If you use SSL for the panel, it is also required to set up SSL encryption for the Wings endpoint. +This is possible directly using Wings itself, but a reverse proxy is recommended. + +Read the [upstream docs](https://pelican.dev/docs/wings/install) before continuing. + +## Setting up a node {#module-pelican-wings-setup} + +To set up a new node, go to . This will give you +a YAML configuration file, which you only need the `uuid`, `token` and `token_id` attributes of. + +```nix +# configuration.nix +{ pkgs, ... }: +{ + services.pelican-wings = { + enable = true; + openFirewall = true; + + configuration.remote = "http://panel.example.com"; + # Secrets saved like this will be world-readable in the store: + secretConfigurationFile = pkgs.writeText "wings-secret.yml" '' + uuid: my-node-uuid + token_id: supersecret + token: long-and-super-secret + ''; + }; +} +``` + +The `secretConfigurationFile` option has to be set to the absolute path of a file that exists. Please use a [proper secret management scheme](https://wiki.nixos.org/wiki/Comparison_of_secret_managing_schemes) to provide it. + +## Using traefik as a reverse proxy {#module-pelican-wings-traefik} + +The module has support for using traefik as a reverse proxy. See the docs for [](#opt-services.traefik.enable). +For example, this could look like this: + +```nix +# configuration.nix +{ pkgs, ... }: +{ + services.pelican-wings = { + enable = true; + + enableTraefik = true; + openFirewall = true; + domain = "server1.example.com"; + + configuration.remote = "https://panel.example.com"; + secretConfigurationFile = pkgs.writeText "wings-secret.yml" '' + uuid: my-node-uuid + token_id: supersecret + token: long-and-super-secret + ''; + }; + + services.traefik = { + enable = true; + + staticConfigOptions = { + entryPoints = { + web = { + address = ":80"; + http.redirections.entryPoint = { + to = "websecure"; + scheme = "https"; + }; + }; + + websecure = { + address = ":443"; + }; + }; + + certificatesResolvers.letsencrypt.acme = { + email = "alice@example.com"; + storage = "/var/lib/traefik/acme.json"; + tlsChallenge = { }; + }; + }; + }; + + networking.firewall = { + allowedTCPPorts = [ + 80 + 443 + ]; + allowedUDPPorts = [ + 80 + 443 + ]; + }; +} +``` + diff --git a/nixos/modules/services/misc/pelican-wings.nix b/nixos/modules/services/misc/pelican-wings.nix new file mode 100644 index 0000000000000..713bbfab181ba --- /dev/null +++ b/nixos/modules/services/misc/pelican-wings.nix @@ -0,0 +1,136 @@ +{ + lib, + config, + pkgs, + ... +}: +let + cfg = config.services.pelican-wings; + inherit (lib) + mkEnableOption + mkPackageOption + mkOption + types + mkIf + mkDefault + optionalAttrs + recursiveUpdate + maintainers + getExe + ; +in +{ + options.services.pelican-wings = { + enable = mkEnableOption "Pelican Wings daemon"; + enableTraefik = mkEnableOption "Traefik reverse proxy configuration for Wings"; + + package = mkPackageOption pkgs "pelican-wings" { }; + + openFirewall = mkEnableOption "firewall rules for Wings. Will open both the SFTP port 2022 and the apiPort. Disable if you're using a reverse proxy and open 2022 manually"; + domain = mkOption { + type = types.str; + description = "the domain to run Wings on. Must be reachable from the panel and from all clients accessing the panel"; + }; + + configuration = mkOption { + type = types.attrs; + default = { }; + description = "Base Wings configuration written to the Nix store at eval time. Merged with secretConfigurationFile at runtime, with secrets taking precedence."; + }; + + secretConfigurationFile = mkOption { + type = types.path; + description = "Path to a YAML file containing secret configuration values. Merged on top of configuration at runtime."; + }; + + apiPort = mkOption { + type = types.port; + default = 8080; + description = "Port Wings listens on."; + }; + }; + + config = mkIf cfg.enable { + virtualisation.docker.enable = mkDefault true; + + systemd.services.wings = + let + autoConfig = + recursiveUpdate + { + system.sftp.bind_port = 2022; + api.port = cfg.apiPort; + } + ( + optionalAttrs cfg.enableTraefik { + api = { + host = "0.0.0.0"; + ssl.enabled = false; + }; + } + ); + storeConfig = (pkgs.formats.yaml { }).generate "wings-config.yml" ( + recursiveUpdate autoConfig cfg.configuration + ); + in + { + description = "Pelican Wings Daemon"; + after = [ "docker.service" ]; + requires = [ "docker.service" ]; + wantedBy = [ "multi-user.target" ]; + + path = with pkgs; [ + shadow + yq-go + ]; + + serviceConfig = { + User = "root"; + LimitNOFILE = 4096; + ExecStartPre = pkgs.writeShellScript "wings-pre" '' + yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' \ + ${storeConfig} ${cfg.secretConfigurationFile} > /etc/pelican/config.yml + ''; + ExecStart = "${getExe cfg.package} --config /etc/pelican/config.yml"; + Restart = "on-failure"; + RestartSec = "5s"; + }; + + unitConfig = { + StartLimitIntervalSec = 180; + StartLimitBurst = 30; + }; + }; + + services.traefik.dynamicConfigOptions = mkIf cfg.enableTraefik { + http = { + routers.wings = { + entryPoints = [ "websecure" ]; + rule = "Host(`${cfg.domain}`)"; + service = "wings"; + tls.certResolver = "letsencrypt"; + }; + + services.wings.loadBalancer = { + passHostHeader = true; + servers = [ { url = "http://127.0.0.1:${toString cfg.apiPort}"; } ]; + }; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ + 2022 + cfg.apiPort + ]; + }; + }; + + meta = { + maintainers = with maintainers; [ + oskardotglobal + nullcube + ]; + doc = ./pelican-wings.md; + }; +} From 915d75a97da64f5148ed4be5f6e84eacc79817de Mon Sep 17 00:00:00 2001 From: Oskar Manhart <52569953+oskardotglobal@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:58:19 +0200 Subject: [PATCH 3/4] pelican-panel: init at 1.0.0-beta38 --- pkgs/by-name/pe/pelican-panel/package.nix | 100 ++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 pkgs/by-name/pe/pelican-panel/package.nix diff --git a/pkgs/by-name/pe/pelican-panel/package.nix b/pkgs/by-name/pe/pelican-panel/package.nix new file mode 100644 index 0000000000000..7e217bea18385 --- /dev/null +++ b/pkgs/by-name/pe/pelican-panel/package.nix @@ -0,0 +1,100 @@ +{ + nixosTests, + fetchFromGitHub, + fetchYarnDeps, + lib, + nodejs, + php85, + php85Packages, + stdenvNoCC, + yarnConfigHook, + nix-update-script, + dataDir ? "/var/lib/pelican-panel", +}: +stdenvNoCC.mkDerivation (finalAttrs: { + name = "${finalAttrs.pname}-${finalAttrs.version}"; + + pname = "pelican-panel"; + version = "1.0.0-beta38"; + + src = fetchFromGitHub { + owner = "pelican"; + repo = "panel"; + tag = "v${finalAttrs.version}"; + hash = "sha256-fjY2nHYpSMKBGn+VHb5hsuKcerSy/efp0WenL5yIStQ="; + }; + + buildInputs = [ php85 ]; + + nativeBuildInputs = [ + nodejs + php85.composerHooks2.composerInstallHook + php85Packages.composer + yarnConfigHook + ]; + + composerVendor = php85.mkComposerVendor { + inherit (finalAttrs) + name + pname + src + version + ; + strictDeps = true; + vendorHash = "sha256-tMrWSrN6SldljK69k4SDvehNnOxPrJXfCSk761IAREg="; + }; + + offlineCache = fetchYarnDeps { + yarnLock = "${finalAttrs.src}/yarn.lock"; + hash = "sha256-GppIwYUMM7RHO7KR/PLDVK4pRm86I/MvPuHH/wg6Q9U="; + }; + + installPhase = '' + runHook preInstall + + yarn run build + + cp -r public/build $out/share/php/pelican-panel/public + + chmod -R u+w $out/share + mv $out/share/php/pelican-panel/* $out/ + + rm -rf $out/share $out/plugins $out/storage $out/bootstrap/cache + ln -s ${dataDir}/.env $out/.env + ln -s ${dataDir}/plugins $out/plugins + ln -s ${dataDir}/bootstrap/cache $out/bootstrap/cache + ln -s ${dataDir}/storage $out/storage + + runHook postInstall + ''; + + strictDeps = true; + __structuredAttrs = true; + + passthru = { + php = php85; + updateScript = nix-update-script { + extraArgs = [ + "--version-regex" + "^v(.*-beta.*)$" + "--version=unstable" + ]; + }; + + tests = { + inherit (nixosTests) pelican-panel; + }; + }; + + meta = { + description = "Free game server control panel offering high flying security"; + changelog = "https://github.com/pelican/panel/releases/tag/v${finalAttrs.version}"; + homepage = "https://pelican.dev"; + license = lib.licenses.agpl3Only; + maintainers = with lib.maintainers; [ oskardotglobal ]; + platforms = [ + "x86_64-linux" + "aarch64-linux" + ]; + }; +}) From 48d788afe33f0dd4618b3610ae97c40c4e45098d Mon Sep 17 00:00:00 2001 From: Oskar Manhart <52569953+oskardotglobal@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:02:48 +0200 Subject: [PATCH 4/4] nixos/pelican-panel: init Co-authored-by: NullCube --- nixos/doc/manual/redirects.json | 12 + .../manual/release-notes/rl-2611.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/pelican-panel.md | 104 ++++++ nixos/modules/services/misc/pelican-panel.nix | 348 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/pelican-panel.nix | 38 ++ 7 files changed, 506 insertions(+) create mode 100644 nixos/modules/services/misc/pelican-panel.md create mode 100644 nixos/modules/services/misc/pelican-panel.nix create mode 100644 nixos/tests/pelican-panel.nix diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 4e1af62672b48..32c144c6081ab 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -253,6 +253,18 @@ "module-pelican-wings-traefik": [ "index.html#module-pelican-wings-traefik" ], + "module-pelican-panel": [ + "index.html#module-pelican-panel" + ], + "module-pelican-panel-traefik": [ + "index.html#module-pelican-panel-traefik" + ], + "module-pelican-panel-basic-usage": [ + "index.html#module-pelican-panel-basic-usage" + ], + "module-pelican-panel-environment": [ + "index.html#module-pelican-panel-environment" + ], "sec-boot-problems-scripted-stage-1": [ "index.html#sec-boot-problems-scripted-stage-1" ], diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index e84c3bfbbd634..6297dcab07cab 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -60,6 +60,8 @@ - [Koito](https://koito.io/), a modern, themeable scrobbler that you can use with any program that scrobbles to a custom ListenBrainz URL. Available as [services.koito](#opt-services.koito.enable). +- [Pelican Panel](https://pelican.dev), a game server management panel. Available as [services.pelican-panel](#opt-services.pelican-panel.enable) and [services.pelican-wings](#opt-services.pelican-wings.enable) (for the control plane). + - [Zapret2](https://github.com/bol-van/zapret2), an extensible DPI bypass program. Available as [services.zapret2](#opt-services.zapret2.enable). - [FlapAlerted](https://github.com/Kioubit/FlapAlerted), detects BGP flapping events and provides statistics based on BGP update messages. Available as [services.flap-alerted](#opt-services.flap-alerted.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 8a2dc783b867c..808cff37198ed 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -952,6 +952,7 @@ ./services/misc/packagekit.nix ./services/misc/paisa.nix ./services/misc/paperless.nix + ./services/misc/pelican-panel.nix ./services/misc/pelican-wings.nix ./services/misc/persistent-evdev.nix ./services/misc/pghero.nix diff --git a/nixos/modules/services/misc/pelican-panel.md b/nixos/modules/services/misc/pelican-panel.md new file mode 100644 index 0000000000000..7f9d97811f3fa --- /dev/null +++ b/nixos/modules/services/misc/pelican-panel.md @@ -0,0 +1,104 @@ +# Pelican Panel {#module-pelican-panel} + +[Pelican Panel](https://pelican.dev/) is a game server panel forked from [Pterodactyl](https://pterodactyl.io/) which manages Docker containers on multiple machines, using their control plane Wings. +Wings is required to use it and vice versa. To set up Wings, see the docs for [](#opt-services.traefik.enable). + +Read the [official docs](https://pelican.dev/docs/panel/getting-started) before continuing. + +::: {.note} +Pelican Panel plugins are currently not supported on NixOS. +::: + +## Basic Usage {#module-pelican-panel-basic-usage} + +```nix +# configuration.nix +{ pkgs, ... }: +{ + pelican-panel = { + enable = true; + openFirewall = true; + + # Secrets saved like this will be world-readable in the store: + secretEnvironmentFile = pkgs.writeText "secrets.env" '' + APP_KEY="my-super-secret-app-key" + ''; + }; +} +``` + +The web installer is disabled by default. This means that you have to create the first admin user yourself. Run + +```sh +pelican-artisan p:user:make --admin +``` + +The `secretEnvironmentFile` option has to be set to the absolute path of a file that exists. Please use a [proper secret management scheme](https://wiki.nixos.org/wiki/Comparison_of_secret_managing_schemes) to provide it. + +By default, the panel runs using Caddy on an insecure port ([](#opt-services.pelican-panel.port)). Caddy will not listen on ports 80 and 443. +This is so you can use any reverse proxy, even another webserver like nginx. + +## Environment {#module-pelican-panel-environment} + +All changes to `.env` will be stored in `/var/lib/pelican`, but it is recommended to configure everything through Nix where possible; +make changes with [](#opt-services.pelican-panel.environment) or [](#opt-services.pelican-panel.secretEnvironmentFile). + +## Using traefik as a reverse proxy {#module-pelican-panel-traefik} + +The module has support for using traefik as a reverse proxy. See the docs for [](#opt-services.traefik.enable) on how to enable it. +For example, a full configuration could look like this: + +```nix +# configuration.nix +{ pkgs, ... }: +{ + services = { + pelican-wings = { + # See `services.pelican-wings` + # ... + + configuration.remote = "https://panel.example.com"; + }; + + pelican-panel = { + enable = true; + + enableTraefik = true; + domain = "https://panel.example.com"; + + secretEnvironmentFile = pkgs.writeText "secrets.env" '' + APP_KEY="my-super-secret-app-key" + ''; + }; + + traefik = { + enable = true; + + staticConfigOptions = { + entryPoints = { + web = { + address = ":80"; + + # Redirect http to https + http.redirections.entryPoint = { + to = "websecure"; + scheme = "https"; + }; + }; + + websecure = { + address = ":443"; + }; + }; + + certificatesResolvers.letsencrypt.acme = { + email = "alice@example.com"; + storage = "/var/lib/traefik/acme.json"; + tlsChallenge = { }; + }; + }; + }; + }; +} +``` + diff --git a/nixos/modules/services/misc/pelican-panel.nix b/nixos/modules/services/misc/pelican-panel.nix new file mode 100644 index 0000000000000..557ac529937a6 --- /dev/null +++ b/nixos/modules/services/misc/pelican-panel.nix @@ -0,0 +1,348 @@ +{ + lib, + config, + pkgs, + ... +}: +let + cfg = config.services.pelican-panel; + + inherit (cfg) package; + inherit (package.passthru) php; + + inherit (lib) + mkOption + mkEnableOption + mkPackageOption + types + maintainers + generators + optional + mkIf + optionalAttrs + pipe + mkDefault + isString + replaceStrings + match + ; + + artisan = ( + pkgs.writeShellScriptBin "pelican-artisan" '' + cd /var/lib/pelican/www + exec ${pkgs.sudo}/bin/sudo -u pelican ${php}/bin/php artisan "$@" + '' + ); + + autoConfig = { + APP_DEBUG = false; + APP_ENV = "production"; + APP_INSTALLED = true; + APP_URL = cfg.domain; + + DB_DATABASE = "panel"; + DB_USERNAME = "pelican"; + DB_CONNECTION = "mariadb"; + DB_HOST = "localhost"; + DB_SOCKET = "/run/mysqld/mysqld.sock"; + + REDIS_HOST = "localhost"; + CACHE_STORE = "redis"; + QUEUE_DRIVER = "redis"; + SESSION_DRIVER = "redis"; + + MAIL_DRIVER = "log"; + } + // optionalAttrs cfg.enableTraefik { + TRUSTED_PROXIES = "127.0.0.1"; + }; + + mergedConfig = autoConfig // cfg.environment; + + storeEnv = + let + mkValueString = + v: + if isString v then + ''"${v}"'' + else if v == false then + "false" + else if v == true then + "true" + else + toString v; + mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } "="; + in + pipe mergedConfig [ + (generators.toKeyValue { inherit mkKeyValue; }) + (pkgs.writeText "pelican-base.env") + ]; + + setupScript = pkgs.writeShellScript "pelican-setup" '' + set -euo pipefail + + STATE="/var/lib/pelican" + WWW="$STATE/www" + STORE="${package}" + + # Create persistent state directories + mkdir -p \ + $STATE/storage/{app/public,framework/{cache/data,sessions,views},logs/install} \ + $STATE/plugins \ + $STATE/bootstrap-cache + + # Rebuild the working webroot when the panel package changes + if [ "$(cat $WWW/.nix-store-path 2>/dev/null || true)" != "$STORE" ]; then + rm -rf "$WWW" + cp -r "$STORE" "$WWW" + chmod -R u+w "$WWW" + echo "$STORE" > "$WWW/.nix-store-path" + fi + + sort -u -t '=' -k 1,1 ${cfg.secretEnvironmentFile} ${storeEnv} > "$STATE/.env" + chmod 640 "$STATE/.env" + + # Writable overlay symlinks + ln -sfn "$STATE/storage" "$WWW/storage" + ln -sfn "$STATE/bootstrap-cache" "$WWW/bootstrap/cache" + ln -sfn "$STATE/plugins" "$WWW/plugins" + ln -sfn "$STATE/storage/app/public" "$WWW/public/storage" + ln -sfn "$STATE/.env" "$WWW/.env" + + chmod -R o+rX "$WWW/public" + chown -R pelican:pelican "$STATE" + + ${artisan}/bin/pelican-artisan migrate --force + ${artisan}/bin/pelican-artisan filament:optimize + ''; +in +{ + options.services.pelican-panel = { + enable = mkEnableOption "Pelican Panel"; + enableTraefik = mkEnableOption "Traefik reverse proxy configuration for Pelican"; + + package = mkPackageOption pkgs "pelican-panel" { }; + + openFirewall = mkEnableOption "firewall rules for Pelican"; + + domain = mkOption { + type = with types; nullOr str; + description = "The URL where the users access the panel. This must be reachable from the wings nodes."; + example = "https://panel.example.com"; + }; + + environment = mkOption { + type = types.attrsOf ( + types.oneOf [ + types.str + types.int + types.bool + ] + ); + default = { }; + description = "Base Pelican Panel configuration written to the Nix store at eval time as a .env file. Merged with secretEnvironmentFile at runtime, with secrets taking precedence."; + }; + + secretEnvironmentFile = mkOption { + type = types.path; + description = "Path to a .env file containing secret configuration values. Merged before configuration at runtime so its values take precedence."; + }; + + port = mkOption { + type = types.port; + default = 8081; + description = "The port caddy listens on"; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = + !(cfg.enableTraefik && (cfg.domain == null || match "^.*(:[[:digit:]]+)$" cfg.domain != null)); + message = "`services.pelican-panel.domain` cannot be null or contain a port while `services.pelican-panel.enableTraefik` is enabled"; + } + { + assertion = (cfg.domain == null || match "https?://.*" cfg.domain != null); + message = "`services.pelican-panel.domain` must be `null` or a valid URL"; + } + ]; + + users = { + users.pelican = { + isSystemUser = true; + group = "pelican"; + home = "/var/lib/pelican"; + }; + groups.pelican = { }; + }; + + services = { + mysql = { + enable = true; + package = pkgs.mariadb; + ensureDatabases = [ mergedConfig.DB_DATABASE ]; + ensureUsers = [ + { + name = mergedConfig.DB_USERNAME; + ensurePermissions."${mergedConfig.DB_DATABASE}.*" = "ALL PRIVILEGES"; + } + ]; + }; + + redis.servers.pelican = { + enable = true; + bind = "127.0.0.1"; + port = 6379; + }; + + phpfpm.pools.pelican = { + phpPackage = php; + user = "pelican"; + group = "pelican"; + + settings = { + "listen" = "127.0.0.1:9000"; + "pm" = "dynamic"; + "pm.max_children" = 10; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 5; + "chdir" = "/var/lib/pelican/www"; + "catch_workers_output" = "yes"; + "decorate_workers_output" = "no"; + "env[LOG_CHANNEL]" = "stderr"; + }; + + phpOptions = '' + opcache.enable=1 + opcache.memory_consumption=128 + opcache.interned_strings_buffer=8 + opcache.max_accelerated_files=10000 + opcache.validate_timestamps=0 + ''; + }; + + caddy = { + enable = mkDefault true; + + globalConfig = mkDefault '' + auto_https off + admin off + ''; + + virtualHosts.":${toString cfg.port}".extraConfig = '' + root * /var/lib/pelican/www/public + encode gzip + file_server + php_fastcgi 127.0.0.1:9000 + ''; + }; + + traefik.dynamicConfigOptions = mkIf cfg.enableTraefik { + http = { + routers.pelican = { + entryPoints = [ "websecure" ]; + rule = "Host(`${replaceStrings [ "https://" "http://" ] [ "" "" ] cfg.domain}`)"; + service = "pelican"; + tls.certResolver = "letsencrypt"; + }; + + services.pelican.loadBalancer = { + passHostHeader = true; + servers = [ { url = "http://127.0.0.1:${toString cfg.port}"; } ]; + }; + }; + }; + }; + + systemd = { + services = { + phpfpm-pelican = { + after = [ "pelican-setup.service" ]; + requires = [ "pelican-setup.service" ]; + }; + + caddy = { + after = [ "pelican-setup.service" ]; + requires = [ "pelican-setup.service" ]; + }; + + pelican-setup = { + description = "Pelican Panel Setup"; + wantedBy = [ "multi-user.target" ]; + after = [ + "mysql.service" + "redis-pelican.service" + ]; + requires = [ + "mysql.service" + "redis-pelican.service" + ]; + before = [ + "phpfpm-pelican.service" + "caddy.service" + "pelican-queue.service" + ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = setupScript; + }; + }; + + pelican-queue = { + description = "Pelican Panel Queue Worker"; + after = [ "pelican-setup.service" ]; + requires = [ "pelican-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + User = "pelican"; + Group = "pelican"; + WorkingDirectory = "/var/lib/pelican/www"; + ExecStart = "${php}/bin/php artisan queue:work --tries=3"; + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + + pelican-scheduler = { + description = "Pelican Panel Scheduler"; + after = [ "pelican-setup.service" ]; + + serviceConfig = { + Type = "oneshot"; + User = "pelican"; + Group = "pelican"; + WorkingDirectory = "/var/lib/pelican/www"; + ExecStart = "${php}/bin/php artisan schedule:run"; + }; + }; + }; + + timers.pelican-scheduler = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* *:*:12"; + Persistent = true; + }; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = optional (!cfg.enableTraefik) cfg.port; + }; + + environment.systemPackages = [ artisan ]; + }; + + meta = { + maintainers = with maintainers; [ + oskardotglobal + nullcube + ]; + doc = ./pelican-panel.md; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 7bb6c1c1b5e83..0d5b901a3c5bf 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1347,6 +1347,7 @@ in peerflix = runTest ./peerflix.nix; peering-manager = runTest ./web-apps/peering-manager.nix; peertube = handleTestOn [ "x86_64-linux" ] ./web-apps/peertube.nix { }; + pelican-panel = runTest ./pelican-panel.nix; perses = runTest ./perses.nix; pgadmin4 = runTest ./pgadmin4.nix; pgbackrest = import ./pgbackrest { inherit runTest; }; diff --git a/nixos/tests/pelican-panel.nix b/nixos/tests/pelican-panel.nix new file mode 100644 index 0000000000000..b825c78cd799b --- /dev/null +++ b/nixos/tests/pelican-panel.nix @@ -0,0 +1,38 @@ +{ lib, ... }: +{ + name = "pelican-panel"; + meta.maintainers = [ lib.maintainers.oskardotglobal ]; + + nodes.machine = + { pkgs, ... }: + { + virtualisation.diskSize = 1024 * 10; + + services.pelican-panel = { + enable = true; + port = 8080; + secretEnvironmentFile = pkgs.writeText "pelican-test-secrets.env" '' + APP_KEY="base64:QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=" + ''; + }; + }; + + testScript = '' + with subtest("all required services reach active state"): + machine.start() + machine.wait_for_unit("mysql.service") + machine.wait_for_unit("redis-pelican.service") + machine.wait_for_unit("pelican-setup.service") + machine.wait_for_unit("phpfpm-pelican.service") + machine.wait_for_unit("caddy.service") + machine.wait_for_unit("pelican-queue.service") + + with subtest("web server is reachable on configured port"): + machine.wait_for_open_port(8080) + + with subtest("login page is served by PHP-FPM"): + machine.succeed( + "curl http://localhost:8080/login | grep 'Pelican'" + ) + ''; +}