From d088405bd7f263e9b4ed3df7978700d50685b50f Mon Sep 17 00:00:00 2001 From: happysalada Date: Sun, 2 Mar 2025 09:25:38 -0500 Subject: [PATCH 1/2] prefect: make ui file writeable prefect: add wrapper for dependencies --- .../make_ui_files_writeable_on_startup.patch | 42 +++++++++++++++++++ pkgs/by-name/pr/prefect/package.nix | 10 +++++ 2 files changed, 52 insertions(+) create mode 100644 pkgs/by-name/pr/prefect/make_ui_files_writeable_on_startup.patch diff --git a/pkgs/by-name/pr/prefect/make_ui_files_writeable_on_startup.patch b/pkgs/by-name/pr/prefect/make_ui_files_writeable_on_startup.patch new file mode 100644 index 000000000000..279c2fa88118 --- /dev/null +++ b/pkgs/by-name/pr/prefect/make_ui_files_writeable_on_startup.patch @@ -0,0 +1,42 @@ +From a97d5f501ff3125d96e6c64dfa498ca1a598a4bd Mon Sep 17 00:00:00 2001 +From: happysalada +Date: Sun, 2 Mar 2025 08:30:36 -0500 +Subject: [PATCH] feat: ensure ui files are writeable On startup prefect copies + over files from the ui into the ui directory. If for any reason the ui files + were not writeable, the whole setup will fail. This PR ensures that the + copied files are writeable. To give a bit more context, I am currently + packaging Prefect for nixos. Nix having a little bit of a strict build + system, makes sure that the built package has only read-only files. this is + to ensure the build is deterministic. I understand that this might appear as + a detail related to nix build system only. I can patch the source when + building the nix package, but I thought I would try to contribute the patch. + No hard feelings if you are not interested in this patch. Thank you for + developping prefect! + +fix formatting +--- + src/prefect/server/api/server.py | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/src/prefect/server/api/server.py b/src/prefect/server/api/server.py +index e5b64d527..ac64616ef 100644 +--- a/src/prefect/server/api/server.py ++++ b/src/prefect/server/api/server.py +@@ -250,8 +250,14 @@ def copy_directory(directory: str, path: str) -> None: + if os.path.exists(destination): + shutil.rmtree(destination) + shutil.copytree(source, destination, symlinks=True) ++ # ensure copied files are writeable ++ for root, dirs, files in os.walk(destination): ++ for f in files: ++ os.chmod(os.path.join(root, f), 0o600) + else: + shutil.copy2(source, destination) ++ # Ensure copied file is writeable ++ os.chmod(destination, 0o600) + + + async def custom_internal_exception_handler( +-- +2.48.1 + diff --git a/pkgs/by-name/pr/prefect/package.nix b/pkgs/by-name/pr/prefect/package.nix index 52c734533609..bc2fb162487a 100644 --- a/pkgs/by-name/pr/prefect/package.nix +++ b/pkgs/by-name/pr/prefect/package.nix @@ -18,6 +18,10 @@ python3Packages.buildPythonApplication rec { hash = "sha256-4kwGrKvDihBi6Gcvcf6ophNI6GGd+M4qR0nnu/AUK1Q="; }; + patches = [ + ./make_ui_files_writeable_on_startup.patch + ]; + pythonRelaxDeps = [ "websockets" ]; @@ -147,6 +151,12 @@ python3Packages.buildPythonApplication rec { ]; }; + makeWrapperArgs = [ + # Add the installed directories to the python path so the worker can find them + "--prefix PYTHONPATH : ${python3Packages.makePythonPath dependencies}" + "--prefix PYTHONPATH : $out/${python3Packages.python.sitePackages}" + ]; + # Tests are not included in the pypi source doCheck = false; # nativeCheckInputs = ( From ef12e14cb706c780ed55b177576588c4a757fd81 Mon Sep 17 00:00:00 2001 From: happysalada Date: Mon, 3 Mar 2025 20:40:59 -0500 Subject: [PATCH 2/2] nixos/prefect: init module prefect: add dburl to worker prefect: use same state directory prefect: fix worker environment prefect: create user prefect: use datadir for sqlite url prefect: make datadir writable prefect: don't protect home prefect fix sqlite url prefect: fix state directory prefect: user should not be systemuser prefect: set to normal user add prefect to systempackages try user with same name prefect use prefect_home do not set database url revert to dynamic user prefect: add tests prefect: fix port to string --- nixos/modules/module-list.nix | 1 + nixos/modules/services/scheduling/prefect.nix | 232 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/prefect.nix | 27 ++ pkgs/by-name/pr/prefect/package.nix | 5 + 5 files changed, 266 insertions(+) create mode 100644 nixos/modules/services/scheduling/prefect.nix create mode 100644 nixos/tests/prefect.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0a563ab736d9..a9129b430d8f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1361,6 +1361,7 @@ ./services/scheduling/atd.nix ./services/scheduling/cron.nix ./services/scheduling/fcron.nix + ./services/scheduling/prefect.nix ./services/scheduling/scx.nix ./services/search/elasticsearch-curator.nix ./services/search/elasticsearch.nix diff --git a/nixos/modules/services/scheduling/prefect.nix b/nixos/modules/services/scheduling/prefect.nix new file mode 100644 index 000000000000..409279edd692 --- /dev/null +++ b/nixos/modules/services/scheduling/prefect.nix @@ -0,0 +1,232 @@ +{ + lib, + pkgs, + config, + ... +}: + +let + cfg = config.services.prefect; + inherit (lib.types) + bool + str + enum + path + attrsOf + nullOr + submodule + port + ; + +in +{ + options.services.prefect = { + enable = lib.mkOption { + type = bool; + default = false; + description = "enable prefect server and worker services"; + }; + + package = lib.mkPackageOption pkgs "prefect" { }; + + host = lib.mkOption { + type = str; + default = "127.0.0.1"; + example = "0.0.0.0"; + description = "Prefect server host"; + }; + + port = lib.mkOption { + type = port; + default = 4200; + description = "Prefect server port"; + }; + + dataDir = lib.mkOption { + type = path; + default = "/var/lib/prefect-server"; + description = '' + Specify the directory for Prefect. + ''; + }; + + database = lib.mkOption { + type = enum [ + "sqlite" + "postgres" + ]; + default = "sqlite"; + description = "which database to use for prefect server: sqlite or postgres"; + }; + + databaseHost = lib.mkOption { + type = str; + default = "localhost"; + description = "database host for postgres only"; + }; + + databasePort = lib.mkOption { + type = str; + default = "5432"; + description = "database port for postgres only"; + }; + + databaseName = lib.mkOption { + type = str; + default = "prefect"; + description = "database name for postgres only"; + }; + + databaseUser = lib.mkOption { + type = str; + default = "postgres"; + description = "database user for postgres only"; + }; + + databasePasswordFile = lib.mkOption { + type = nullOr str; + default = null; + description = '' + path to a file containing e.g.: + DBPASSWORD=supersecret + + stored outside the nix store, read by systemd as EnvironmentFile. + ''; + }; + + # now define workerPools as an attribute set of submodules, + # each key is the pool name, and the submodule has an installPolicy + workerPools = lib.mkOption { + type = attrsOf (submodule { + options = { + installPolicy = lib.mkOption { + type = enum [ + "always" + "if-not-present" + "never" + "prompt" + ]; + default = "always"; + description = "install policy for the worker (always, if-not-present, never, prompt)"; + }; + }; + }); + default = { }; + description = '' + define a set of worker pools with submodule config. example: + workerPools.my-pool = { + installPolicy = "never"; + }; + ''; + }; + + baseUrl = lib.mkOption { + type = nullOr str; + default = null; + description = "external url when served by a reverse proxy, e.g. https://example.com/prefect"; + }; + }; + + config = lib.mkIf cfg.enable { + # define systemd.services as the server plus any worker definitions + systemd.services = + { + "prefect-server" = { + description = "prefect server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + DynamicUser = true; + StateDirectory = "prefect-server"; + # TODO all my efforts to setup the database url + # have failed with some unable to open file + Environment = [ + "PREFECT_HOME=%S/prefect-server" + "PREFECT_UI_STATIC_DIRECTORY=%S/prefect-server" + "PREFECT_SERVER_ANALYTICS_ENABLED=off" + "PREFECT_UI_API_URL=${cfg.baseUrl}/api" + "PREFECT_UI_URL=${cfg.baseUrl}" + ]; + EnvironmentFile = + if cfg.database == "postgres" && cfg.databasePasswordFile != null then + [ cfg.databasePasswordFile ] + else + [ ]; + + # ReadWritePaths = [ cfg.dataDir ]; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + MemoryDenyWriteExecute = true; + LockPersonality = true; + CapabilityBoundingSet = [ ]; + AmbientCapabilities = [ ]; + RestrictSUIDSGID = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + MemoryAccounting = true; + CPUAccounting = true; + + ExecStart = "${pkgs.prefect}/bin/prefect server start --host ${cfg.host} --port ${toString cfg.port}"; + Restart = "always"; + WorkingDirectory = cfg.dataDir; + }; + }; + } + // lib.concatMapAttrs (poolName: poolCfg: { + # return a partial attr set with one key: "prefect-worker-..." + "prefect-worker-${poolName}" = { + description = "prefect worker for pool '${poolName}'"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + environment.systemPackages = cfg.package; + + serviceConfig = { + DynamicUser = true; + StateDirectory = "prefect-worker-${poolName}"; + Environment = [ + "PREFECT_HOME=%S/prefect-worker-${poolName}" + "PREFECT_API_URL=${cfg.baseUrl}/api" + ]; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + MemoryDenyWriteExecute = true; + LockPersonality = true; + CapabilityBoundingSet = [ ]; + AmbientCapabilities = [ ]; + RestrictSUIDSGID = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + MemoryAccounting = true; + CPUAccounting = true; + ExecStart = '' + ${pkgs.prefect}/bin/prefect worker start \ + --pool ${poolName} \ + --type process \ + --install-policy ${poolCfg.installPolicy} + ''; + Restart = "always"; + }; + }; + }) cfg.workerPools; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 52d7e8fb618f..8105fd31c877 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -950,6 +950,7 @@ in { pppd = handleTest ./pppd.nix {}; predictable-interface-names = handleTest ./predictable-interface-names.nix {}; pretalx = runTest ./web-apps/pretalx.nix; + prefect = runTest ./prefect.nix; pretix = runTest ./web-apps/pretix.nix; printing-socket = handleTest ./printing.nix { socket = true; listenTcp = true; }; printing-service = handleTest ./printing.nix { socket = false; listenTcp = true; }; diff --git a/nixos/tests/prefect.nix b/nixos/tests/prefect.nix new file mode 100644 index 000000000000..0e2f1a36c687 --- /dev/null +++ b/nixos/tests/prefect.nix @@ -0,0 +1,27 @@ +{ lib, ... }: +let + mainPort = "4200"; +in +{ + name = "prefect"; + + nodes = { + machine = + { ... }: + { + services.prefect = { + enable = true; + }; + }; + }; + + testScript = '' + machine.start() + machine.wait_for_unit("prefect-server.service") + machine.wait_for_open_port("${mainPort}") + ''; + + meta = with lib.maintainers; { + maintainers = [ happysalada ]; + }; +} diff --git a/pkgs/by-name/pr/prefect/package.nix b/pkgs/by-name/pr/prefect/package.nix index bc2fb162487a..78627cc832c3 100644 --- a/pkgs/by-name/pr/prefect/package.nix +++ b/pkgs/by-name/pr/prefect/package.nix @@ -2,6 +2,7 @@ lib, python3Packages, fetchPypi, + nixosTests, }: python3Packages.buildPythonApplication rec { @@ -157,6 +158,10 @@ python3Packages.buildPythonApplication rec { "--prefix PYTHONPATH : $out/${python3Packages.python.sitePackages}" ]; + passthru.tests = { + inherit (nixosTests) prefect; + }; + # Tests are not included in the pypi source doCheck = false; # nativeCheckInputs = (