From 3364d6c9725f5fcfe0662888c1e0803004752beb Mon Sep 17 00:00:00 2001
From: Valentin Gagarin <valentin.gagarin@tweag.io>
Date: Tue, 18 Mar 2025 18:18:49 +0100
Subject: [PATCH] fix: NixOS deployment code

- simplify the configuration module

  the `package` attribute makes little sense to be user-configurable,
  since it will always need to be the derivation defined in this very
  repository. for debugging one may as well change the original code itself.

- unbreak deployment

  setting `CREDENTIALS_DIRECTORY` disabled the systemd mechanism set up
  in the configuration module.

- remove unneeded configuration for deployment

- unbreak integration tests

  before that missed waiting for the service to create some
  state before running the application-level tests.
---
 infra/machines/fedi201/fedipanel.nix | 14 +-------------
 panel/default.nix                    | 10 +++-------
 panel/nix/configuration.nix          | 28 +++++++++-------------------
 panel/nix/tests.nix                  |  6 ++++--
 panel/src/panel/settings.py          |  2 ++
 5 files changed, 19 insertions(+), 41 deletions(-)

diff --git a/infra/machines/fedi201/fedipanel.nix b/infra/machines/fedi201/fedipanel.nix
index 5312eafb..4f90c473 100644
--- a/infra/machines/fedi201/fedipanel.nix
+++ b/infra/machines/fedi201/fedipanel.nix
@@ -4,15 +4,10 @@
 }:
 let
   name = "panel";
-  panel = (import ../../../panel/default.nix { }).package;
 in
 {
   imports = [
-    ../../../panel/nix/configuration.nix
-  ];
-
-  environment.systemPackages = [
-    panel
+    (import ../../../panel { }).module
   ];
 
   security.acme = {
@@ -22,18 +17,11 @@ in
 
   services.${name} = {
     enable = true;
-    package = panel;
     production = true;
     domain = "demo.fediversity.eu";
-    host = "0.0.0.0";
     secrets = {
       SECRET_KEY = config.age.secrets.panel-secret-key.path;
     };
     port = 8000;
-    settings = {
-      DATABASE_URL = "sqlite:///var/lib/${name}/db.sqlite3";
-      CREDENTIALS_DIRECTORY = "/var/lib/${name}/.credentials";
-      STATIC_ROOT = "/var/lib/${name}/static";
-    };
   };
 }
diff --git a/panel/default.nix b/panel/default.nix
index b0ec435e..c5bc5fe4 100644
--- a/panel/default.nix
+++ b/panel/default.nix
@@ -8,17 +8,13 @@
   },
 }:
 let
-  package = pkgs.callPackage ./nix/package.nix { };
-
-  pkgs' = pkgs.extend (_final: _prev: { panel = package; });
-
   manage = pkgs.writeScriptBin "manage" ''
     exec ${pkgs.lib.getExe pkgs.python3} ${toString ./src/manage.py} $@
   '';
 in
 {
   shell = pkgs.mkShellNoCC {
-    inputsFrom = [ package ];
+    inputsFrom = [ (pkgs.callPackage ./nix/package.nix { }) ];
     packages = [
       pkgs.npins
       manage
@@ -36,8 +32,8 @@ in
     '';
   };
 
-  tests = pkgs'.callPackage ./nix/tests.nix { };
-  inherit package;
+  module = import ./nix/configuration.nix;
+  tests = pkgs.callPackage ./nix/tests.nix { };
 
   # re-export inputs so they can be overridden granularly
   # (they can't be accessed from the outside any other way)
diff --git a/panel/nix/configuration.nix b/panel/nix/configuration.nix
index e261fd95..21ac9d7c 100644
--- a/panel/nix/configuration.nix
+++ b/panel/nix/configuration.nix
@@ -12,7 +12,6 @@ let
     mkEnableOption
     mkIf
     mkOption
-    mkPackageOption
     optionalString
     types
     ;
@@ -22,23 +21,15 @@ let
   name = "panel";
 
   cfg = config.services.${name};
+  package = pkgs.callPackage ./package.nix { };
 
   database-url = "sqlite:////var/lib/${name}/db.sqlite3";
 
   python-environment = pkgs.python3.withPackages (
-    ps:
-    with ps;
-    [
+    ps: with ps; [
+      package
       uvicorn
-      cfg.package
-      dj-database-url
-      django-compressor
-      django-debug-toolbar
-      django-libsass
-      django_4
-      setuptools
     ]
-    ++ cfg.package.propagatedBuildInputs
   );
 
   configFile = pkgs.concatText "configuration.py" [
@@ -48,7 +39,7 @@ let
 
   manage-service = writeShellApplication {
     name = "manage";
-    text = ''exec ${cfg.package}/bin/manage.py "$@"'';
+    text = ''exec ${package}/bin/manage.py "$@"'';
   };
 
   manage-admin = writeShellApplication {
@@ -83,8 +74,6 @@ in
 {
   options.services.${name} = {
     enable = mkEnableOption "Service configuration for `${name}`";
-    # NOTE: this requires that the package is present in `pkgs`
-    package = mkPackageOption pkgs name { };
     production = mkOption {
       type = types.bool;
       default = true;
@@ -145,6 +134,8 @@ in
   };
 
   config = mkIf cfg.enable {
+    nixpkgs.overlays = [ (import ./overlay.nix) ];
+
     environment.systemPackages = [ manage-admin ];
 
     services = {
@@ -181,16 +172,15 @@ in
       preStart = ''
         # Auto-migrate on first run or if the package has changed
         versionFile="/var/lib/${name}/package-version"
-        if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${package} ]]; then
           manage migrate --no-input
           manage collectstatic --no-input --clear
           manage compress --force
-          echo ${cfg.package} > "$versionFile"
+          echo ${package} > "$versionFile"
         fi
       '';
       script = ''
-        export PYTHONPATH=$PYTHONPATH:${cfg.package}/lib/python3.12/site-packages
-        ${python-environment}/bin/python -m uvicorn ${name}.asgi:application --host ${cfg.host} --port ${toString cfg.port}
+        uvicorn ${name}.asgi:application --host ${cfg.host} --port ${toString cfg.port}
       '';
       serviceConfig = {
         Restart = "always";
diff --git a/panel/nix/tests.nix b/panel/nix/tests.nix
index a8259384..ea5ebd94 100644
--- a/panel/nix/tests.nix
+++ b/panel/nix/tests.nix
@@ -3,6 +3,8 @@ let
   # TODO: specify project/service name globally
   name = "panel";
   defaults = {
+    # XXX: we have to duplicate this here despite it being defined in the service module, otherwise the test framework will error out
+    nixpkgs.overlays = lib.mkForce [ (import ./overlay.nix) ];
     services.${name} = {
       enable = true;
       production = false;
@@ -26,6 +28,7 @@ lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; }))
     # run all application-level tests managed by Django
     # https://docs.djangoproject.com/en/5.0/topics/testing/overview/
     testScript = ''
+      server.wait_for_unit("${name}.service")
       server.succeed("manage test ${name}")
     '';
   };
@@ -34,7 +37,6 @@ lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; }))
     nodes.server = _: { imports = [ ./configuration.nix ]; };
     # check that the admin interface is served
     testScript = ''
-      server.wait_for_unit("multi-user.target")
       server.wait_for_unit("${name}.service")
       server.wait_for_open_port(8000)
       server.succeed("curl --fail -L -H 'Host: example.org' http://localhost/admin")
@@ -45,11 +47,11 @@ lib.mapAttrs (name: test: pkgs.testers.runNixOSTest (test // { inherit name; }))
     inherit defaults;
     nodes.server = _: { imports = [ ./configuration.nix ]; };
     extraPythonPackages = ps: with ps; [ beautifulsoup4 ];
+    # type checking on `beautifulsoup4` will error out
     skipTypeCheck = true;
     # check that stylesheets are pre-processed and served
     testScript = ''
       from bs4 import BeautifulSoup
-      server.wait_for_unit("multi-user.target")
       server.wait_for_unit("${name}.service")
       server.wait_for_open_port(8000)
       stdout = server.succeed("curl --fail -H 'Host: example.org' http://localhost")
diff --git a/panel/src/panel/settings.py b/panel/src/panel/settings.py
index fc32a36b..4209f80a 100644
--- a/panel/src/panel/settings.py
+++ b/panel/src/panel/settings.py
@@ -26,6 +26,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
 # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
 
 def get_secret(name: str, encoding: str = "utf-8") -> str:
+    # In the NixOS deployment, this variable is set by `systemd` via `LoadCredential`
+    # https://systemd.io/CREDENTIALS/
     credentials_dir = env.get("CREDENTIALS_DIRECTORY")
 
     if credentials_dir is None: