From 9532ba3962bc46d0c76bcba3dc44b39baf04b28f Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Sat, 27 Dec 2025 21:16:56 -0800 Subject: [PATCH 01/18] nixos/ncps: Fix deprecated flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ncps package has updated its flags and deprecated some in favor of others. Here's the full list: - `--cache-data-path` → `--cache-storage-local` - `--upstream-cache` → `--cache-upstream-url` - `--upstream-public-key` → `--cache-upstream-public-key` --- nixos/modules/services/networking/ncps.nix | 102 ++++++++++++--------- nixos/tests/all-tests.nix | 4 +- nixos/tests/ncps.nix | 14 +-- 3 files changed, 70 insertions(+), 50 deletions(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index 83161c051b5d..ecffa8625d55 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -35,7 +35,7 @@ let serveFlags = lib.concatStringsSep " " ( [ "--cache-hostname='${cfg.cache.hostName}'" - "--cache-data-path='${cfg.cache.dataPath}'" + "--cache-storage-local='${cfg.cache.storage.local}'" "--cache-database-url='${cfg.cache.databaseURL}'" "--cache-temp-path='${cfg.cache.tempPath}'" "--server-addr='${cfg.server.addr}'" @@ -49,8 +49,8 @@ let ]) ++ (lib.optional (cfg.cache.secretKeyPath != null) "--cache-secret-key-path='%d/secretKey'") ++ (lib.optional (!cfg.cache.signNarinfo) "--cache-sign-narinfo='false'") - ++ (lib.forEach cfg.upstream.caches (url: "--upstream-cache='${url}'")) - ++ (lib.forEach cfg.upstream.publicKeys (pk: "--upstream-public-key='${pk}'")) + ++ (lib.forEach cfg.cache.upstream.publicKeys (pk: "--cache-upstream-public-key='${pk}'")) + ++ (lib.forEach cfg.cache.upstream.urls (url: "--cache-upstream-url='${url}'")) ++ (lib.optional (cfg.netrcFile != null) "--netrc-file='${cfg.netrcFile}'") ); @@ -60,6 +60,23 @@ let dbDir = dirOf dbPath; in { + imports = [ + (lib.mkRenamedOptionModule + [ "services" "ncps" "cache" "dataPath" ] + [ "services" "ncps" "cache" "storage" "local" ] + ) + + (lib.mkRenamedOptionModule + [ "services" "ncps" "upstream" "caches" ] + [ "services" "ncps" "cache" "upstream" "urls" ] + ) + + (lib.mkRenamedOptionModule + [ "services" "ncps" "upstream" "publicKeys" ] + [ "services" "ncps" "cache" "upstream" "publicKeys" ] + ) + ]; + options = { services.ncps = { enable = lib.mkEnableOption "ncps: Nix binary cache proxy service implemented in Go"; @@ -113,17 +130,9 @@ in ''; }; - dataPath = lib.mkOption { - type = lib.types.str; - default = "/var/lib/ncps"; - description = '' - The local directory for storing configuration and cached store paths - ''; - }; - databaseURL = lib.mkOption { type = lib.types.str; - default = "sqlite:${cfg.cache.dataPath}/db/db.sqlite"; + default = "sqlite:${cfg.cache.storage.local}/db/db.sqlite"; defaultText = "sqlite:/var/lib/ncps/db/db.sqlite"; description = '' The URL of the database (currently only SQLite is supported) @@ -174,6 +183,16 @@ in ''; }; + storage = { + local = lib.mkOption { + type = lib.types.str; + default = "/var/lib/ncps"; + description = '' + The local directory for storing configuration and cached store paths + ''; + }; + }; + tempPath = lib.mkOption { type = lib.types.str; default = "/tmp"; @@ -190,6 +209,28 @@ in Whether to sign narInfo files or passthru as-is from upstream ''; }; + + upstream = { + publicKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; + description = '' + A list of public keys of upstream caches in the format + `host[-[0-9]*]:public-key`. This flag is used to verify the + signatures of store paths downloaded from upstream caches. + ''; + }; + + urls = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = [ "https://cache.nixos.org" ]; + description = '' + A list of URLs of upstream binary caches. + ''; + }; + }; + }; server = { @@ -202,27 +243,6 @@ in }; }; - upstream = { - caches = lib.mkOption { - type = lib.types.listOf lib.types.str; - example = [ "https://cache.nixos.org" ]; - description = '' - A list of URLs of upstream binary caches. - ''; - }; - - publicKeys = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; - description = '' - A list of public keys of upstream caches in the format - `host[-[0-9]*]:public-key`. This flag is used to verify the - signatures of store paths downloaded from upstream caches. - ''; - }; - }; - netrcFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -256,10 +276,10 @@ in UMask = "0066"; }; script = - (lib.optionalString (cfg.cache.dataPath != "/var/lib/ncps") '' - if ! test -d ${cfg.cache.dataPath}; then - mkdir -p ${cfg.cache.dataPath} - chown ncps:ncps ${cfg.cache.dataPath} + (lib.optionalString (cfg.cache.storage.local != "/var/lib/ncps") '' + if ! test -d ${cfg.cache.storage.local}; then + mkdir -p ${cfg.cache.storage.local} + chown ncps:ncps ${cfg.cache.storage.local} fi '') + (lib.optionalString isSqlite '' @@ -304,10 +324,10 @@ in }) # ensure permissions on required directories - (lib.mkIf (cfg.cache.dataPath != "/var/lib/ncps") { - ReadWritePaths = [ cfg.cache.dataPath ]; + (lib.mkIf (cfg.cache.storage.local != "/var/lib/ncps") { + ReadWritePaths = [ cfg.cache.storage.local ]; }) - (lib.mkIf (cfg.cache.dataPath == "/var/lib/ncps") { + (lib.mkIf (cfg.cache.storage.local == "/var/lib/ncps") { StateDirectory = "ncps"; StateDirectoryMode = "0700"; }) @@ -357,7 +377,7 @@ in ]; unitConfig.RequiresMountsFor = lib.concatStringsSep " " ( - [ "${cfg.cache.dataPath}" ] ++ lib.optional isSqlite dbDir + [ "${cfg.cache.storage.local}" ] ++ lib.optional isSqlite dbDir ); }; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 3f93c0827657..c577a2d7a000 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1029,9 +1029,9 @@ in nbd = runTest ./nbd.nix; ncdns = runTest ./ncdns.nix; ncps = runTest ./ncps.nix; - ncps-custom-cache-datapath = runTest { + ncps-custom-storage-local = runTest { imports = [ ./ncps.nix ]; - defaults.services.ncps.cache.dataPath = "/path/to/ncps"; + defaults.services.ncps.cache.storage.local = "/path/to/ncps"; }; ndppd = runTest ./ndppd.nix; nebula-lighthouse-service = runTest ./nebula-lighthouse-service.nix; diff --git a/nixos/tests/ncps.nix b/nixos/tests/ncps.nix index 76f23ccfe802..e46f7eb3d24b 100644 --- a/nixos/tests/ncps.nix +++ b/nixos/tests/ncps.nix @@ -30,13 +30,13 @@ secretKeyPath = toString ( pkgs.writeText "ncps-cache-key" "ncps:dcrGsrku0KvltFhrR5lVIMqyloAdo0y8vYZOeIFUSLJS2IToL7dPHSSCk/fi+PJf8EorpBn8PU7MNhfvZoI8mA==" ); - }; - upstream = { - caches = [ "http://harmonia:5000" ]; - publicKeys = [ - "cache.example.com-1:eIGQXcGQpc00x6/XFcyacLEUmC07u4RAEHt5Y8vdglo=" - ]; + upstream = { + urls = [ "http://harmonia:5000" ]; + publicKeys = [ + "cache.example.com-1:eIGQXcGQpc00x6/XFcyacLEUmC07u4RAEHt5Y8vdglo=" + ]; + }; }; }; @@ -65,7 +65,7 @@ narinfoNameChars = lib.strings.stringToCharacters narinfoName; narinfoPath = lib.concatStringsSep "/" [ - nodes.ncps.services.ncps.cache.dataPath + nodes.ncps.services.ncps.cache.storage.local "store/narinfo" (lib.lists.elemAt narinfoNameChars 0) ((lib.lists.elemAt narinfoNameChars 0) + (lib.lists.elemAt narinfoNameChars 1)) From 74faa5384d90bcb007c9d58148f98f18c249e289 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Tue, 30 Dec 2025 00:02:35 -0800 Subject: [PATCH 02/18] nixos/ncps: Add cache upstream timeout flags --- nixos/modules/services/networking/ncps.nix | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index ecffa8625d55..75ec0d6a1582 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -49,6 +49,12 @@ let ]) ++ (lib.optional (cfg.cache.secretKeyPath != null) "--cache-secret-key-path='%d/secretKey'") ++ (lib.optional (!cfg.cache.signNarinfo) "--cache-sign-narinfo='false'") + ++ (lib.optional ( + cfg.cache.upstream.dialerTimeout != null + ) "--cache-upstream-dialer-timeout='${cfg.cache.upstream.dialerTimeout}'") + ++ (lib.optional ( + cfg.cache.upstream.responseHeaderTimeout != null + ) "--cache-upstream-response-header-timeout='${cfg.cache.upstream.responseHeaderTimeout}'") ++ (lib.forEach cfg.cache.upstream.publicKeys (pk: "--cache-upstream-public-key='${pk}'")) ++ (lib.forEach cfg.cache.upstream.urls (url: "--cache-upstream-url='${url}'")) ++ (lib.optional (cfg.netrcFile != null) "--netrc-file='${cfg.netrcFile}'") @@ -211,6 +217,22 @@ in }; upstream = { + dialerTimeout = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Timeout for establishing TCP connections to upstream caches (e.g., 3s, 5s, 10s). + ''; + }; + + responseHeaderTimeout = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Timeout for waiting for upstream server's response headers (e.g., 3s, 5s, 10s). + ''; + }; + publicKeys = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; From 1f8b510d783cb372c25c194293cdd4afe76cf60d Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Tue, 30 Dec 2025 01:05:26 -0800 Subject: [PATCH 03/18] nixos/ncps: Add support for configuration s3 --- nixos/modules/services/networking/ncps.nix | 116 +++++++++++++++++++-- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index 75ec0d6a1582..a22c815c094e 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -17,6 +17,15 @@ let "panic" ]; + ncpsWrapper = pkgs.writeShellScript "ncps-wrapper" '' + ${lib.optionalString (cfg.cache.storage.s3 != null) '' + export CACHE_STORAGE_S3_ACCESS_KEY_ID="$(cat "$CREDENTIALS_DIRECTORY/s3AccessKeyId")" + export CACHE_STORAGE_S3_SECRET_ACCESS_KEY="$(cat "$CREDENTIALS_DIRECTORY/s3SecretAccessKey")" + ''} + + exec ${lib.getExe cfg.package} "$@" + ''; + globalFlags = lib.concatStringsSep " " ( [ "--log-level='${cfg.logLevel}'" ] ++ (lib.optionals cfg.openTelemetry.enable ( @@ -35,11 +44,23 @@ let serveFlags = lib.concatStringsSep " " ( [ "--cache-hostname='${cfg.cache.hostName}'" - "--cache-storage-local='${cfg.cache.storage.local}'" "--cache-database-url='${cfg.cache.databaseURL}'" "--cache-temp-path='${cfg.cache.tempPath}'" "--server-addr='${cfg.server.addr}'" ] + ++ (lib.optional ( + cfg.cache.storage.s3 == null + ) "--cache-storage-local='${cfg.cache.storage.local}'") + ++ (lib.optionals (cfg.cache.storage.s3 != null) ( + [ + "--cache-storage-s3-bucket='${cfg.cache.storage.s3.bucket}'" + "--cache-storage-s3-endpoint='${cfg.cache.storage.s3.endpoint}'" + ] + ++ (lib.optional cfg.cache.storage.s3.forcePathStyle "--cache-storage-s3-force-path-style") + ++ (lib.optional ( + cfg.cache.storage.s3.region != null + ) "--cache-storage-s3-region='${cfg.cache.storage.s3.region}'") + )) ++ (lib.optional cfg.cache.allowDeleteVerb "--cache-allow-delete-verb") ++ (lib.optional cfg.cache.allowPutVerb "--cache-allow-put-verb") ++ (lib.optional (cfg.cache.maxSize != null) "--cache-max-size='${cfg.cache.maxSize}'") @@ -194,7 +215,66 @@ in type = lib.types.str; default = "/var/lib/ncps"; description = '' - The local directory for storing configuration and cached store paths + The local directory for storing configuration and cached store + paths. This is ignored if services.ncps.cache.storage.s3 is not + null. + ''; + }; + + s3 = lib.mkOption { + type = lib.types.nullOr ( + lib.types.submodule { + options = { + bucket = lib.mkOption { + type = lib.types.str; + description = '' + The name of the S3 bucket. + ''; + }; + + endpoint = lib.mkOption { + type = lib.types.str; + description = '' + S3-compatible endpoint URL with scheme. + ''; + example = "https://s3.amazonaws.com"; + }; + + forcePathStyle = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Force path-style S3 addressing (bucket/key vs key.bucket). + ''; + }; + + region = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + The S3 region. + ''; + }; + + accessKeyIdPath = lib.mkOption { + type = lib.types.str; + description = '' + The path to a file containing only the access-key-id. + ''; + }; + + secretAccessKeyPath = lib.mkOption { + type = lib.types.str; + description = '' + The path to a file containing only the secret-access-key. + ''; + }; + }; + } + ); + default = null; + description = '' + Use S3 for storage instead of local storage. ''; }; }; @@ -228,8 +308,9 @@ in responseHeaderTimeout = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; + example = "5s"; description = '' - Timeout for waiting for upstream server's response headers (e.g., 3s, 5s, 10s). + Timeout for waiting for upstream server's response headers. ''; }; @@ -298,7 +379,7 @@ in UMask = "0066"; }; script = - (lib.optionalString (cfg.cache.storage.local != "/var/lib/ncps") '' + (lib.optionalString (cfg.cache.storage.s3 == null && cfg.cache.storage.local != "/var/lib/ncps") '' if ! test -d ${cfg.cache.storage.local}; then mkdir -p ${cfg.cache.storage.local} chown ncps:ncps ${cfg.cache.storage.local} @@ -333,26 +414,40 @@ in serviceConfig = lib.mkMerge [ { - ExecStart = "${lib.getExe cfg.package} ${globalFlags} serve ${serveFlags}"; + ExecStart = "${ncpsWrapper} ${globalFlags} serve ${serveFlags}"; User = "ncps"; Group = "ncps"; Restart = "on-failure"; RuntimeDirectory = "ncps"; } - # credentials + # credentials for cache.secretKeyPath (lib.mkIf (cfg.cache.secretKeyPath != null) { - LoadCredential = "secretKey:${cfg.cache.secretKeyPath}"; + LoadCredential = lib.singleton "secretKey:${cfg.cache.secretKeyPath}"; + }) + + # credentials for cache.storage.s3 accessKeyIdPath and secretAccessKeyPath + (lib.mkIf (cfg.cache.storage.s3 != null) { + LoadCredential = [ + "s3AccessKeyId:${cfg.cache.storage.s3.accessKeyIdPath}" + "s3SecretAccessKey:${cfg.cache.storage.s3.secretAccessKeyPath}" + ]; }) # ensure permissions on required directories - (lib.mkIf (cfg.cache.storage.local != "/var/lib/ncps") { + (lib.mkIf (cfg.cache.storage.s3 == null && cfg.cache.storage.local != "/var/lib/ncps") { ReadWritePaths = [ cfg.cache.storage.local ]; }) - (lib.mkIf (cfg.cache.storage.local == "/var/lib/ncps") { + (lib.mkIf (cfg.cache.storage.s3 == null && cfg.cache.storage.local == "/var/lib/ncps") { StateDirectory = "ncps"; StateDirectoryMode = "0700"; }) + (lib.mkIf (cfg.cache.storage.s3 != null && isSqlite && lib.strings.hasPrefix "/var/lib/ncps" dbDir) + { + StateDirectory = "ncps"; + StateDirectoryMode = "0700"; + } + ) (lib.mkIf (isSqlite && !lib.strings.hasPrefix "/var/lib/ncps" dbDir) { ReadWritePaths = [ dbDir ]; }) @@ -399,7 +494,8 @@ in ]; unitConfig.RequiresMountsFor = lib.concatStringsSep " " ( - [ "${cfg.cache.storage.local}" ] ++ lib.optional isSqlite dbDir + (lib.optional (cfg.cache.storage.s3 == null) "${cfg.cache.storage.local}") + ++ (lib.optional isSqlite dbDir) ); }; }; From 4e089a6eab0371db7e6b8eef1f8ad603e2b537e6 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Tue, 30 Dec 2025 01:06:53 -0800 Subject: [PATCH 04/18] nixos/tests/ncps: Add kalbasit as a maintainer --- nixos/tests/ncps.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nixos/tests/ncps.nix b/nixos/tests/ncps.nix index e46f7eb3d24b..6f9ade50aa24 100644 --- a/nixos/tests/ncps.nix +++ b/nixos/tests/ncps.nix @@ -6,6 +6,9 @@ { name = "ncps"; + meta = with lib.maintainers; { + maintainers = [ kalbasit ]; + }; nodes = { harmonia = { From 4e08de39acf92096264eaef4878771b1f1983b43 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Tue, 30 Dec 2025 14:08:31 -0800 Subject: [PATCH 05/18] nixos/tests/ncps: Add a test for S3 support via minio --- nixos/tests/all-tests.nix | 1 + nixos/tests/ncps-storage-s3.nix | 137 ++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 nixos/tests/ncps-storage-s3.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index c577a2d7a000..51aa5ac033c5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1033,6 +1033,7 @@ in imports = [ ./ncps.nix ]; defaults.services.ncps.cache.storage.local = "/path/to/ncps"; }; + ncps-storage-s3 = runTest ./ncps-storage-s3.nix; ndppd = runTest ./ndppd.nix; nebula-lighthouse-service = runTest ./nebula-lighthouse-service.nix; nebula.connectivity = runTest ./nebula/connectivity.nix; diff --git a/nixos/tests/ncps-storage-s3.nix b/nixos/tests/ncps-storage-s3.nix new file mode 100644 index 000000000000..890158dd8c4a --- /dev/null +++ b/nixos/tests/ncps-storage-s3.nix @@ -0,0 +1,137 @@ +{ + lib, + pkgs, + ... +}: +let + bucket = "ncps"; + region = "us-west-1"; + accessKey = "test-access-key"; + secretKey = "test-secret-key"; + + initMinio = pkgs.writeShellScriptBin "init-minio.sh" '' + set -euo pipefail + + mc alias set local "http://127.0.0.1:9000" minioadmin minioadmin + mc mb local/${bucket} + mc admin user svcacct add --access-key ${accessKey} --secret-key ${secretKey} local minioadmin + ''; +in +{ + name = "ncps-storage-s3"; + meta = with lib.maintainers; { + maintainers = [ kalbasit ]; + }; + + nodes = { + harmonia = { + services.harmonia = { + enable = true; + signKeyPaths = [ + (pkgs.writeText "cache-key" "cache.example.com-1:9FhO0w+7HjZrhvmzT1VlAZw4OSAlFGTgC24Seg3tmPl4gZBdwZClzTTHr9cVzJpwsRSYLTu7hEAQe3ljy92CWg==") + ]; + settings.priority = 35; + }; + + networking.firewall.allowedTCPPorts = [ 5000 ]; + system.extraDependencies = [ pkgs.emptyFile ]; + }; + + minio = { + services.minio = { + inherit region; + + enable = true; + }; + + networking.firewall.allowedTCPPorts = [ 9000 ]; + environment.systemPackages = [ + pkgs.minio-client + initMinio + ]; + }; + + ncps = { + services.ncps = { + enable = true; + + cache = { + hostName = "ncps"; + + secretKeyPath = builtins.toString ( + pkgs.writeText "ncps-cache-key" "ncps:dcrGsrku0KvltFhrR5lVIMqyloAdo0y8vYZOeIFUSLJS2IToL7dPHSSCk/fi+PJf8EorpBn8PU7MNhfvZoI8mA==" + ); + + storage.s3 = { + inherit bucket region; + + endpoint = "http://minio:9000"; + + accessKeyIdPath = builtins.toFile "minio-access-key" accessKey; + secretAccessKeyPath = builtins.toFile "minio-secret-key" secretKey; + }; + + upstream = { + urls = [ "http://harmonia:5000" ]; + publicKeys = [ + "cache.example.com-1:eIGQXcGQpc00x6/XFcyacLEUmC07u4RAEHt5Y8vdglo=" + ]; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8501 ]; + }; + + client = { + nix.settings = { + substituters = lib.mkForce [ "http://ncps:8501" ]; + trusted-public-keys = lib.mkForce [ + "ncps:UtiE6C+3Tx0kgpP34vjyX/BKK6QZ/D1OzDYX72aCPJg=" + ]; + }; + }; + }; + + testScript = + { nodes, ... }: + let + narinfoName = + (lib.strings.removePrefix "/nix/store/" ( + lib.strings.removeSuffix "-empty-file" pkgs.emptyFile.outPath + )) + + ".narinfo"; + + narinfoNameChars = lib.strings.stringToCharacters narinfoName; + + narinfoPath = lib.concatStringsSep "/" [ + (builtins.head nodes.minio.services.minio.dataDir) + bucket + "store/narinfo" + (lib.lists.elemAt narinfoNameChars 0) + ((lib.lists.elemAt narinfoNameChars 0) + (lib.lists.elemAt narinfoNameChars 1)) + narinfoName + "xl.meta" + ]; + in + '' + minio.start() + + minio.wait_for_unit("minio.service") + + minio.wait_until_succeeds("init-minio.sh") + + start_all() + + harmonia.wait_for_unit("harmonia.service") + + ncps.wait_for_unit("ncps.service") + + client.wait_until_succeeds("curl -f http://ncps:8501/ | grep '\"hostname\":\"${toString nodes.ncps.services.ncps.cache.hostName}\"' >&2") + + client.succeed("cat /etc/nix/nix.conf >&2") + client.succeed("nix-store --realise ${pkgs.emptyFile}") + + minio.succeed("cat ${narinfoPath} >&2") + ''; +} From 3b1e6d3a07b3a984e78cb29266e3325bf3b53aeb Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Tue, 30 Dec 2025 14:12:22 -0800 Subject: [PATCH 06/18] nixos/tests/ncps: Add aciceri as a maintainer --- nixos/tests/ncps-storage-s3.nix | 5 ++++- nixos/tests/ncps.nix | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/nixos/tests/ncps-storage-s3.nix b/nixos/tests/ncps-storage-s3.nix index 890158dd8c4a..57e23c4e1c9a 100644 --- a/nixos/tests/ncps-storage-s3.nix +++ b/nixos/tests/ncps-storage-s3.nix @@ -20,7 +20,10 @@ in { name = "ncps-storage-s3"; meta = with lib.maintainers; { - maintainers = [ kalbasit ]; + maintainers = [ + aciceri + kalbasit + ]; }; nodes = { diff --git a/nixos/tests/ncps.nix b/nixos/tests/ncps.nix index 6f9ade50aa24..0daef543fbff 100644 --- a/nixos/tests/ncps.nix +++ b/nixos/tests/ncps.nix @@ -7,7 +7,10 @@ { name = "ncps"; meta = with lib.maintainers; { - maintainers = [ kalbasit ]; + maintainers = [ + aciceri + kalbasit + ]; }; nodes = { From 10d5182447877709d354316a5eefde8eaaa188a4 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Fri, 2 Jan 2026 00:45:50 -0800 Subject: [PATCH 07/18] nixos/tests/ncps: rename client01 to client --- nixos/tests/ncps.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nixos/tests/ncps.nix b/nixos/tests/ncps.nix index 0daef543fbff..8de7bb053be1 100644 --- a/nixos/tests/ncps.nix +++ b/nixos/tests/ncps.nix @@ -49,7 +49,7 @@ networking.firewall.allowedTCPPorts = [ 8501 ]; }; - client01 = { + client = { nix.settings = { substituters = lib.mkForce [ "http://ncps:8501" ]; trusted-public-keys = lib.mkForce [ @@ -85,10 +85,10 @@ ncps.wait_for_unit("ncps.service") - client01.wait_until_succeeds("curl -f http://ncps:8501/ | grep '\"hostname\":\"${toString nodes.ncps.services.ncps.cache.hostName}\"' >&2") + client.wait_until_succeeds("curl -f http://ncps:8501/ | grep '\"hostname\":\"${toString nodes.ncps.services.ncps.cache.hostName}\"' >&2") - client01.succeed("cat /etc/nix/nix.conf >&2") - client01.succeed("nix-store --realise ${pkgs.emptyFile}") + client.succeed("cat /etc/nix/nix.conf >&2") + client.succeed("nix-store --realise ${pkgs.emptyFile}") ncps.succeed("cat ${narinfoPath} >&2") ''; From de1b9a1308c8716ac13de04c4769a1f3916af177 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 16:55:29 -0800 Subject: [PATCH 08/18] nixos/ncps: types.str -> types.path Change all types.str that are supposed to hold an absolute path to types.str. --- nixos/modules/services/networking/ncps.nix | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index a22c815c094e..7282dcffb938 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -202,7 +202,7 @@ in }; secretKeyPath = lib.mkOption { - type = lib.types.nullOr lib.types.str; + type = lib.types.nullOr lib.types.path; default = null; description = '' The path to load the secretKey for signing narinfos. Leave this @@ -212,7 +212,7 @@ in storage = { local = lib.mkOption { - type = lib.types.str; + type = lib.types.path; default = "/var/lib/ncps"; description = '' The local directory for storing configuration and cached store @@ -257,14 +257,14 @@ in }; accessKeyIdPath = lib.mkOption { - type = lib.types.str; + type = lib.types.path; description = '' The path to a file containing only the access-key-id. ''; }; secretAccessKeyPath = lib.mkOption { - type = lib.types.str; + type = lib.types.path; description = '' The path to a file containing only the secret-access-key. ''; @@ -280,7 +280,7 @@ in }; tempPath = lib.mkOption { - type = lib.types.str; + type = lib.types.path; default = "/tmp"; description = '' The path to the temporary directory that is used by the cache to download NAR files @@ -347,7 +347,7 @@ in }; netrcFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; + type = lib.types.nullOr lib.types.path; default = null; example = "/etc/nix/netrc"; description = '' From 80d0b43ba8e028020948fa4d25ecd820c9e16406 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 16:58:22 -0800 Subject: [PATCH 09/18] nixos/ncps: Fix the location of SQLite migrations The location of the migration files has changed in ncps v0.6.0. --- nixos/modules/services/networking/ncps.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index 7282dcffb938..bd26b09cbfb5 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -409,7 +409,7 @@ in wantedBy = [ "multi-user.target" ]; preStart = '' - ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations --url=${cfg.cache.databaseURL} up + ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations/sqlite --url=${cfg.cache.databaseURL} up ''; serviceConfig = lib.mkMerge [ From dbf35e94e5ab1301c70c2fbbefb51a11fcdf968f Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 17:07:45 -0800 Subject: [PATCH 10/18] nixos/ncps: Use systemd.tmpfiles.settings.ncps for declarative directories --- nixos/modules/services/networking/ncps.nix | 45 ++++++++-------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index bd26b09cbfb5..88cb1c29f757 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -372,34 +372,23 @@ in }; users.groups.ncps = { }; - systemd.services.ncps-create-directories = { - description = "Created required directories by ncps"; - serviceConfig = { - Type = "oneshot"; - UMask = "0066"; - }; - script = - (lib.optionalString (cfg.cache.storage.s3 == null && cfg.cache.storage.local != "/var/lib/ncps") '' - if ! test -d ${cfg.cache.storage.local}; then - mkdir -p ${cfg.cache.storage.local} - chown ncps:ncps ${cfg.cache.storage.local} - fi - '') - + (lib.optionalString isSqlite '' - if ! test -d ${dbDir}; then - mkdir -p ${dbDir} - chown ncps:ncps ${dbDir} - fi - '') - + (lib.optionalString (cfg.cache.tempPath != "/tmp") '' - if ! test -d ${cfg.cache.tempPath}; then - mkdir -p ${cfg.cache.tempPath} - chown ncps:ncps ${cfg.cache.tempPath} - fi - ''); - wantedBy = [ "ncps.service" ]; - before = [ "ncps.service" ]; - }; + systemd.tmpfiles.settings.ncps = + let + perms = { + group = "ncps"; + mode = "0700"; + user = "ncps"; + }; + in + lib.mkMerge [ + (lib.mkIf (cfg.cache.storage.s3 == null && cfg.cache.storage.local != "/var/lib/ncps") { + "${cfg.cache.storage.local}".d = perms; + }) + + (lib.mkIf (isSqlite) { "${dbDir}".d = perms; }) + + (lib.mkIf (cfg.cache.tempPath != "/tmp") { "${cfg.cache.tempPath}".d = perms; }) + ]; systemd.services.ncps = { description = "ncps binary cache proxy service"; From 977f5baed383f47f7b832db5827c73f9e05b6a76 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 17:15:10 -0800 Subject: [PATCH 11/18] nixos/tests/ncps: Add a test for custom SQLite path --- nixos/tests/all-tests.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 51aa5ac033c5..a17ceeb311e5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1029,6 +1029,10 @@ in nbd = runTest ./nbd.nix; ncdns = runTest ./ncdns.nix; ncps = runTest ./ncps.nix; + ncps-custom-sqlite-directory = runTest { + imports = [ ./ncps.nix ]; + defaults.services.ncps.cache.databaseURL = "sqlite:/path/to/ncps/db.sqlite"; + }; ncps-custom-storage-local = runTest { imports = [ ./ncps.nix ]; defaults.services.ncps.cache.storage.local = "/path/to/ncps"; From 164022841f4f50387986f45ae58fb98e36e3ecb2 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 17:23:31 -0800 Subject: [PATCH 12/18] nixos/ncps: Expose the analytics options of ncps --- nixos/modules/services/networking/ncps.nix | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index 88cb1c29f757..a05c542db11a 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -36,9 +36,8 @@ let cfg.openTelemetry.grpcURL != null ) "--otel-grpc-url='${cfg.openTelemetry.grpcURL}'") )) - ++ (lib.optionals cfg.prometheus.enable [ - "--prometheus-enabled" - ]) + ++ (lib.optional cfg.prometheus.enable "--prometheus-enabled") + ++ (lib.optional (!cfg.analytics.reporting.enable) "--analytics-reporting-enabled=false") ); serveFlags = lib.concatStringsSep " " ( @@ -108,6 +107,14 @@ in services.ncps = { enable = lib.mkEnableOption "ncps: Nix binary cache proxy service implemented in Go"; + analytics.reporting.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Enable reporting anonymous usage statistics (DB type, Lock type, Total Size) to the project maintainers. + ''; + }; + package = lib.mkPackageOption pkgs "ncps" { }; dbmatePackage = lib.mkPackageOption pkgs "dbmate" { }; From b6b1f72435962f533b0ae2693467e7def07b18a4 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 17:26:21 -0800 Subject: [PATCH 13/18] nixos/tests/ncps: Disable analytics on ncps tests --- nixos/tests/ncps-storage-s3.nix | 2 ++ nixos/tests/ncps.nix | 2 ++ 2 files changed, 4 insertions(+) diff --git a/nixos/tests/ncps-storage-s3.nix b/nixos/tests/ncps-storage-s3.nix index 57e23c4e1c9a..c75557114a1b 100644 --- a/nixos/tests/ncps-storage-s3.nix +++ b/nixos/tests/ncps-storage-s3.nix @@ -58,6 +58,8 @@ in services.ncps = { enable = true; + analytics.reporting.enable = false; + cache = { hostName = "ncps"; diff --git a/nixos/tests/ncps.nix b/nixos/tests/ncps.nix index 8de7bb053be1..c2528e55b136 100644 --- a/nixos/tests/ncps.nix +++ b/nixos/tests/ncps.nix @@ -31,6 +31,8 @@ services.ncps = { enable = true; + analytics.reporting.enable = false; + cache = { hostName = "ncps"; secretKeyPath = toString ( From fc8e297430062ad72108c58f9cb626c6f6233cfb Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 17:51:24 -0800 Subject: [PATCH 14/18] nixos/ncps: Add all options for Redis --- nixos/modules/services/networking/ncps.nix | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index a05c542db11a..05c76ce1e9ab 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -23,6 +23,10 @@ let export CACHE_STORAGE_S3_SECRET_ACCESS_KEY="$(cat "$CREDENTIALS_DIRECTORY/s3SecretAccessKey")" ''} + ${lib.optionalString (cfg.cache.redis != null && cfg.cache.redis.passwordFile != null) '' + export CACHE_REDIS_PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/redisPassword")" + ''} + exec ${lib.getExe cfg.package} "$@" ''; @@ -47,6 +51,20 @@ let "--cache-temp-path='${cfg.cache.tempPath}'" "--server-addr='${cfg.server.addr}'" ] + ++ (lib.optionals (cfg.cache.redis != null) ( + [ + "--cache-redis-addrs='${builtins.concatStringsSep "," cfg.cache.redis.addresses}'" + "--cache-redis-db='${builtins.toString cfg.cache.redis.database}'" + "--cache-redis-pool-size='${builtins.toString cfg.cache.redis.poolSize}'" + ] + ++ (lib.optional ( + cfg.cache.redis.username != null + ) "--cache-redis-username='${cfg.cache.redis.username}'") + ++ (lib.optional ( + cfg.cache.redis.password != null + ) "--cache-redis-password='${cfg.cache.redis.password}'") + ++ (lib.optional cfg.cache.redis.useTLS "--cache-redis-use-tls") + )) ++ (lib.optional ( cfg.cache.storage.s3 == null ) "--cache-storage-local='${cfg.cache.storage.local}'") @@ -208,6 +226,79 @@ in ''; }; + redis = lib.mkOption { + type = lib.types.nullOr ( + lib.types.submodule { + options = { + addresses = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = '' + ["redis0:6379" "redis1:6379"] + ''; + description = '' + A list of host:port for the Redis servers that are part of a cluster. + To use a single Redis instance, just set this to its single address. + ''; + }; + + database = lib.mkOption { + type = lib.types.int; + default = 0; + description = '' + Redis database number (0-15) + ''; + }; + + username = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Redis username for authentication (for Redis ACL). + ''; + }; + + password = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Redis password for authentication (for Redis ACL). + ''; + }; + + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + File containing the redis password for authentication (for Redis ACL). + ''; + }; + + poolSize = lib.mkOption { + type = lib.types.int; + default = 10; + description = '' + Redis connection pool size. + ''; + }; + + useTLS = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Use TLS for Redis connection. + ''; + }; + }; + } + ); + + default = null; + + description = '' + Configure Redis. + ''; + }; + secretKeyPath = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; @@ -371,6 +462,11 @@ in assertion = cfg.cache.lru.schedule == null || cfg.cache.maxSize != null; message = "You must specify config.ncps.cache.lru.schedule when config.ncps.cache.maxSize is set"; } + { + assertion = + cfg.cache.redis == null || cfg.cache.redis.password == null || cfg.cache.redis.passwordFile == null; + message = "You cannot specify both config.ncps.cache.redis.password and config.ncps.cache.redis.passwordFile"; + } ]; users.users.ncps = { @@ -430,6 +526,11 @@ in ]; }) + # credentials for Redis + (lib.mkIf (cfg.cache.redis != null && cfg.cache.redis.passwordFile != null) { + LoadCredential = lib.singleton "redisPassword:${cfg.cache.redis.passwordFile}"; + }) + # ensure permissions on required directories (lib.mkIf (cfg.cache.storage.s3 == null && cfg.cache.storage.local != "/var/lib/ncps") { ReadWritePaths = [ cfg.cache.storage.local ]; From 99c4bb7c9334f89dbd8f798bfc6e17c02622f403 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Wed, 7 Jan 2026 22:05:28 -0800 Subject: [PATCH 15/18] nixos/ncps: Use dbmate-wrapper from ncps --- nixos/modules/services/networking/ncps.nix | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index 05c76ce1e9ab..a3ef2c0c8be3 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -119,6 +119,12 @@ in [ "services" "ncps" "upstream" "publicKeys" ] [ "services" "ncps" "cache" "upstream" "publicKeys" ] ) + + (lib.mkRemovedOptionModule [ + "services" + "ncps" + "dbmatePackage" + ] "dbmate is now wrapped within ncps package, you need to override ncps to change dbmate package") ]; options = { @@ -135,8 +141,6 @@ in package = lib.mkPackageOption pkgs "ncps" { }; - dbmatePackage = lib.mkPackageOption pkgs "dbmate" { }; - openTelemetry = { enable = lib.mkEnableOption "Enable OpenTelemetry logs, metrics, and tracing"; @@ -501,7 +505,7 @@ in wantedBy = [ "multi-user.target" ]; preStart = '' - ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations/sqlite --url=${cfg.cache.databaseURL} up + ${cfg.package}/bin/dbmate-ncps --url="${cfg.cache.databaseURL}" up ''; serviceConfig = lib.mkMerge [ From 137d1e1830d8955ad04c4fdeb4288bf288d55933 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 8 Jan 2026 19:11:24 -0800 Subject: [PATCH 16/18] nixos/tests/ncps: Implement an HA test --- nixos/tests/all-tests.nix | 2 +- nixos/tests/ncps-ha.nix | 210 ++++++++++++++++++++++++++++++++ nixos/tests/ncps-storage-s3.nix | 142 --------------------- 3 files changed, 211 insertions(+), 143 deletions(-) create mode 100644 nixos/tests/ncps-ha.nix delete mode 100644 nixos/tests/ncps-storage-s3.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a17ceeb311e5..3c0547d4578c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1037,7 +1037,7 @@ in imports = [ ./ncps.nix ]; defaults.services.ncps.cache.storage.local = "/path/to/ncps"; }; - ncps-storage-s3 = runTest ./ncps-storage-s3.nix; + ncps-ha = runTest ./ncps-ha.nix; ndppd = runTest ./ndppd.nix; nebula-lighthouse-service = runTest ./nebula-lighthouse-service.nix; nebula.connectivity = runTest ./nebula/connectivity.nix; diff --git a/nixos/tests/ncps-ha.nix b/nixos/tests/ncps-ha.nix new file mode 100644 index 000000000000..c39962c0536c --- /dev/null +++ b/nixos/tests/ncps-ha.nix @@ -0,0 +1,210 @@ +{ + lib, + pkgs, + ... +}: +let + # s3 creds + bucket = "ncps"; + region = "us-west-1"; + accessKey = builtins.toFile "minio-access-key" "easy-key"; + secretKey = builtins.toFile "minio-secret-key" "easy-secret"; + + # pg creds + postgresPassword = "easypwd"; + + # redis creds + redisPassword = "easypwd"; + + initMinio = pkgs.writeShellScriptBin "init-minio.sh" '' + set -euo pipefail + + mc alias set local "http://127.0.0.1:9000" minioadmin minioadmin + mc mb local/${bucket} + mc admin user svcacct add --access-key "$(cat ${accessKey})" --secret-key "$(cat ${secretKey})" local minioadmin + ''; + + ncpsAttrs = hostname: { + services.ncps = { + enable = true; + + analytics.reporting.enable = false; + + cache = { + hostName = hostname; + + databaseURL = "postgres://ncps:${lib.escapeURL postgresPassword}@postgres:5432/ncps?sslmode=disable"; + + secretKeyPath = builtins.toString ( + pkgs.writeText "ncps-cache-key" "ncps:dcrGsrku0KvltFhrR5lVIMqyloAdo0y8vYZOeIFUSLJS2IToL7dPHSSCk/fi+PJf8EorpBn8PU7MNhfvZoI8mA==" + ); + + redis = { + addresses = [ "redis:6379" ]; + passwordFile = builtins.toFile "redis-password" redisPassword; + }; + + storage.s3 = { + inherit bucket region; + + endpoint = "http://minio:9000"; + + accessKeyIdPath = accessKey; + secretAccessKeyPath = secretKey; + }; + + upstream = { + urls = [ "http://harmonia:5000" ]; + publicKeys = [ + "cache.example.com-1:eIGQXcGQpc00x6/XFcyacLEUmC07u4RAEHt5Y8vdglo=" + ]; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8501 ]; + }; +in +{ + name = "ncps-storage-s3"; + meta = with lib.maintainers; { + maintainers = [ + aciceri + kalbasit + ]; + }; + + nodes = { + client0 = { + nix.settings = { + substituters = lib.mkForce [ "http://ncps0:8501" ]; + trusted-public-keys = lib.mkForce [ + "ncps:UtiE6C+3Tx0kgpP34vjyX/BKK6QZ/D1OzDYX72aCPJg=" + ]; + }; + }; + + client1 = { + nix.settings = { + substituters = lib.mkForce [ "http://ncps1:8501" ]; + trusted-public-keys = lib.mkForce [ + "ncps:UtiE6C+3Tx0kgpP34vjyX/BKK6QZ/D1OzDYX72aCPJg=" + ]; + }; + }; + + harmonia = { + services.harmonia = { + enable = true; + signKeyPaths = [ + (pkgs.writeText "cache-key" "cache.example.com-1:9FhO0w+7HjZrhvmzT1VlAZw4OSAlFGTgC24Seg3tmPl4gZBdwZClzTTHr9cVzJpwsRSYLTu7hEAQe3ljy92CWg==") + ]; + settings.priority = 35; + }; + + networking.firewall.allowedTCPPorts = [ 5000 ]; + system.extraDependencies = [ pkgs.emptyFile ]; + }; + + minio = { + services.minio = { + inherit region; + + enable = true; + }; + + networking.firewall.allowedTCPPorts = [ 9000 ]; + environment.systemPackages = [ + pkgs.minio-client + initMinio + ]; + }; + + ncps0 = ncpsAttrs "ncps0"; + ncps1 = ncpsAttrs "ncps1"; + + postgres = { + services.postgresql = { + enable = true; + enableTCPIP = true; + authentication = '' + host all all all scram-sha-256 + ''; + initialScript = pkgs.writeText "init-postgres.sql" '' + CREATE DATABASE "ncps" WITH ENCODING = 'UTF8'; + CREATE ROLE "ncps" WITH LOGIN PASSWORD '${ + builtins.replaceStrings [ "'" ] [ "''" ] postgresPassword + }'; + ALTER DATABASE "ncps" OWNER TO "ncps"; + ''; + }; + + networking.firewall.allowedTCPPorts = [ 5432 ]; + }; + + redis = { + services.redis.servers.ncps = { + enable = true; + openFirewall = true; + port = 6379; + requirePass = redisPassword; + bind = null; + }; + }; + }; + + testScript = + { nodes, ... }: + let + narinfoName = + (lib.strings.removePrefix "/nix/store/" ( + lib.strings.removeSuffix "-empty-file" pkgs.emptyFile.outPath + )) + + ".narinfo"; + + narinfoNameChars = lib.strings.stringToCharacters narinfoName; + + narinfoPath = lib.concatStringsSep "/" [ + (builtins.head nodes.minio.services.minio.dataDir) + bucket + "store/narinfo" + (lib.lists.elemAt narinfoNameChars 0) + ((lib.lists.elemAt narinfoNameChars 0) + (lib.lists.elemAt narinfoNameChars 1)) + narinfoName + "xl.meta" + ]; + in + '' + harmonia.start() + minio.start() + postgres.start() + redis.start() + + minio.wait_for_unit("minio.service") + + minio.wait_until_succeeds("init-minio.sh") + + postgres.wait_for_unit("postgresql.service") + redis.wait_for_unit("redis-ncps.service") + + redis.wait_until_succeeds("redis-cli -h redis -p 6379 -a '${redisPassword}' ping") + + start_all() + + harmonia.wait_for_unit("harmonia.service") + + ncps0.wait_for_unit("ncps.service") + ncps1.wait_for_unit("ncps.service") + + client0.wait_until_succeeds("curl -f http://ncps0:8501/ | grep '\"hostname\":\"${toString nodes.ncps0.services.ncps.cache.hostName}\"' >&2") + client1.wait_until_succeeds("curl -f http://ncps1:8501/ | grep '\"hostname\":\"${toString nodes.ncps1.services.ncps.cache.hostName}\"' >&2") + + client0.succeed("cat /etc/nix/nix.conf >&2") + client0.succeed("nix-store --realise ${pkgs.emptyFile}") + + client1.succeed("cat /etc/nix/nix.conf >&2") + client1.succeed("nix-store --realise ${pkgs.emptyFile}") + + minio.succeed("cat ${narinfoPath} >&2") + ''; +} diff --git a/nixos/tests/ncps-storage-s3.nix b/nixos/tests/ncps-storage-s3.nix deleted file mode 100644 index c75557114a1b..000000000000 --- a/nixos/tests/ncps-storage-s3.nix +++ /dev/null @@ -1,142 +0,0 @@ -{ - lib, - pkgs, - ... -}: -let - bucket = "ncps"; - region = "us-west-1"; - accessKey = "test-access-key"; - secretKey = "test-secret-key"; - - initMinio = pkgs.writeShellScriptBin "init-minio.sh" '' - set -euo pipefail - - mc alias set local "http://127.0.0.1:9000" minioadmin minioadmin - mc mb local/${bucket} - mc admin user svcacct add --access-key ${accessKey} --secret-key ${secretKey} local minioadmin - ''; -in -{ - name = "ncps-storage-s3"; - meta = with lib.maintainers; { - maintainers = [ - aciceri - kalbasit - ]; - }; - - nodes = { - harmonia = { - services.harmonia = { - enable = true; - signKeyPaths = [ - (pkgs.writeText "cache-key" "cache.example.com-1:9FhO0w+7HjZrhvmzT1VlAZw4OSAlFGTgC24Seg3tmPl4gZBdwZClzTTHr9cVzJpwsRSYLTu7hEAQe3ljy92CWg==") - ]; - settings.priority = 35; - }; - - networking.firewall.allowedTCPPorts = [ 5000 ]; - system.extraDependencies = [ pkgs.emptyFile ]; - }; - - minio = { - services.minio = { - inherit region; - - enable = true; - }; - - networking.firewall.allowedTCPPorts = [ 9000 ]; - environment.systemPackages = [ - pkgs.minio-client - initMinio - ]; - }; - - ncps = { - services.ncps = { - enable = true; - - analytics.reporting.enable = false; - - cache = { - hostName = "ncps"; - - secretKeyPath = builtins.toString ( - pkgs.writeText "ncps-cache-key" "ncps:dcrGsrku0KvltFhrR5lVIMqyloAdo0y8vYZOeIFUSLJS2IToL7dPHSSCk/fi+PJf8EorpBn8PU7MNhfvZoI8mA==" - ); - - storage.s3 = { - inherit bucket region; - - endpoint = "http://minio:9000"; - - accessKeyIdPath = builtins.toFile "minio-access-key" accessKey; - secretAccessKeyPath = builtins.toFile "minio-secret-key" secretKey; - }; - - upstream = { - urls = [ "http://harmonia:5000" ]; - publicKeys = [ - "cache.example.com-1:eIGQXcGQpc00x6/XFcyacLEUmC07u4RAEHt5Y8vdglo=" - ]; - }; - }; - }; - - networking.firewall.allowedTCPPorts = [ 8501 ]; - }; - - client = { - nix.settings = { - substituters = lib.mkForce [ "http://ncps:8501" ]; - trusted-public-keys = lib.mkForce [ - "ncps:UtiE6C+3Tx0kgpP34vjyX/BKK6QZ/D1OzDYX72aCPJg=" - ]; - }; - }; - }; - - testScript = - { nodes, ... }: - let - narinfoName = - (lib.strings.removePrefix "/nix/store/" ( - lib.strings.removeSuffix "-empty-file" pkgs.emptyFile.outPath - )) - + ".narinfo"; - - narinfoNameChars = lib.strings.stringToCharacters narinfoName; - - narinfoPath = lib.concatStringsSep "/" [ - (builtins.head nodes.minio.services.minio.dataDir) - bucket - "store/narinfo" - (lib.lists.elemAt narinfoNameChars 0) - ((lib.lists.elemAt narinfoNameChars 0) + (lib.lists.elemAt narinfoNameChars 1)) - narinfoName - "xl.meta" - ]; - in - '' - minio.start() - - minio.wait_for_unit("minio.service") - - minio.wait_until_succeeds("init-minio.sh") - - start_all() - - harmonia.wait_for_unit("harmonia.service") - - ncps.wait_for_unit("ncps.service") - - client.wait_until_succeeds("curl -f http://ncps:8501/ | grep '\"hostname\":\"${toString nodes.ncps.services.ncps.cache.hostName}\"' >&2") - - client.succeed("cat /etc/nix/nix.conf >&2") - client.succeed("nix-store --realise ${pkgs.emptyFile}") - - minio.succeed("cat ${narinfoPath} >&2") - ''; -} From c3deadcb06e7d344e09dffc5921991765f22ba69 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Thu, 8 Jan 2026 19:43:54 -0800 Subject: [PATCH 17/18] nixos/ncps: Add a databaseURLFile for secure configuration --- nixos/modules/services/networking/ncps.nix | 41 ++++++++++++++++++---- nixos/tests/ncps-ha.nix | 8 ++++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index a3ef2c0c8be3..45733a964170 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -27,6 +27,10 @@ let export CACHE_REDIS_PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/redisPassword")" ''} + ${lib.optionalString (cfg.cache.databaseURLFile != null) '' + export CACHE_DATABASE_URL="$(cat "$CREDENTIALS_DIRECTORY/databaseURL")" + ''} + exec ${lib.getExe cfg.package} "$@" ''; @@ -47,10 +51,10 @@ let serveFlags = lib.concatStringsSep " " ( [ "--cache-hostname='${cfg.cache.hostName}'" - "--cache-database-url='${cfg.cache.databaseURL}'" "--cache-temp-path='${cfg.cache.tempPath}'" "--server-addr='${cfg.server.addr}'" ] + ++ (lib.optional (cfg.cache.databaseURL != null) "--cache-database-url='${cfg.cache.databaseURL}'") ++ (lib.optionals (cfg.cache.redis != null) ( [ "--cache-redis-addrs='${builtins.concatStringsSep "," cfg.cache.redis.addresses}'" @@ -98,10 +102,10 @@ let ++ (lib.optional (cfg.netrcFile != null) "--netrc-file='${cfg.netrcFile}'") ); - isSqlite = lib.strings.hasPrefix "sqlite:" cfg.cache.databaseURL; + isSqlite = cfg.cache.databaseURL != null && lib.strings.hasPrefix "sqlite:" cfg.cache.databaseURL; - dbPath = lib.removePrefix "sqlite:" cfg.cache.databaseURL; - dbDir = dirOf dbPath; + dbPath = if isSqlite then lib.removePrefix "sqlite:" cfg.cache.databaseURL else null; + dbDir = if isSqlite then dirOf dbPath else null; in { imports = [ @@ -187,7 +191,7 @@ in }; databaseURL = lib.mkOption { - type = lib.types.str; + type = lib.types.nullOr lib.types.str; default = "sqlite:${cfg.cache.storage.local}/db/db.sqlite"; defaultText = "sqlite:/var/lib/ncps/db/db.sqlite"; description = '' @@ -195,6 +199,14 @@ in ''; }; + databaseURLFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + File containing the URL of the database. + ''; + }; + lru = { schedule = lib.mkOption { type = lib.types.nullOr lib.types.str; @@ -462,6 +474,10 @@ in config = lib.mkIf cfg.enable { assertions = [ + { + assertion = lib.xor (cfg.cache.databaseURL != null) (cfg.cache.databaseURLFile != null); + message = "You must specify exactly one of config.ncps.cache.databaseURL or config.ncps.cache.databaseURLFile"; + } { assertion = cfg.cache.lru.schedule == null || cfg.cache.maxSize != null; message = "You must specify config.ncps.cache.lru.schedule when config.ncps.cache.maxSize is set"; @@ -492,7 +508,7 @@ in "${cfg.cache.storage.local}".d = perms; }) - (lib.mkIf (isSqlite) { "${dbDir}".d = perms; }) + (lib.mkIf isSqlite { "${dbDir}".d = perms; }) (lib.mkIf (cfg.cache.tempPath != "/tmp") { "${cfg.cache.tempPath}".d = perms; }) ]; @@ -505,7 +521,14 @@ in wantedBy = [ "multi-user.target" ]; preStart = '' - ${cfg.package}/bin/dbmate-ncps --url="${cfg.cache.databaseURL}" up + ${lib.optionalString (cfg.cache.databaseURLFile != null) '' + export DATABASE_URL="$(cat "$CREDENTIALS_DIRECTORY/databaseURL")" + ''} + ${lib.optionalString (cfg.cache.databaseURL != null) '' + export DATABASE_URL="${cfg.cache.databaseURL}" + ''} + echo ${cfg.package}/bin/dbmate-ncps up + ${cfg.package}/bin/dbmate-ncps up ''; serviceConfig = lib.mkMerge [ @@ -535,6 +558,10 @@ in LoadCredential = lib.singleton "redisPassword:${cfg.cache.redis.passwordFile}"; }) + (lib.mkIf (cfg.cache.databaseURLFile != null) { + LoadCredential = lib.singleton "databaseURL:${cfg.cache.databaseURLFile}"; + }) + # ensure permissions on required directories (lib.mkIf (cfg.cache.storage.s3 == null && cfg.cache.storage.local != "/var/lib/ncps") { ReadWritePaths = [ cfg.cache.storage.local ]; diff --git a/nixos/tests/ncps-ha.nix b/nixos/tests/ncps-ha.nix index c39962c0536c..e4e8759b9f7c 100644 --- a/nixos/tests/ncps-ha.nix +++ b/nixos/tests/ncps-ha.nix @@ -120,7 +120,13 @@ in ]; }; - ncps0 = ncpsAttrs "ncps0"; + ncps0 = lib.mkMerge [ + (ncpsAttrs "ncps0") + { + services.ncps.cache.databaseURL = lib.mkForce null; + services.ncps.cache.databaseURLFile = builtins.toFile "db-url" "postgres://ncps:${lib.escapeURL postgresPassword}@postgres:5432/ncps?sslmode=disable"; + } + ]; ncps1 = ncpsAttrs "ncps1"; postgres = { From 5ab49ddaa444f4e7bae1700542393e033ddbf9f0 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Sat, 17 Jan 2026 00:36:01 -0800 Subject: [PATCH 18/18] nixos/ncps: Add a new option for --cache-lock-backend --- nixos/modules/services/networking/ncps.nix | 20 ++++++++++++++++++++ nixos/tests/ncps-ha.nix | 2 ++ 2 files changed, 22 insertions(+) diff --git a/nixos/modules/services/networking/ncps.nix b/nixos/modules/services/networking/ncps.nix index 45733a964170..16737d63626d 100644 --- a/nixos/modules/services/networking/ncps.nix +++ b/nixos/modules/services/networking/ncps.nix @@ -51,6 +51,7 @@ let serveFlags = lib.concatStringsSep " " ( [ "--cache-hostname='${cfg.cache.hostName}'" + "--cache-lock-backend='${cfg.cache.lock.backend}'" "--cache-temp-path='${cfg.cache.tempPath}'" "--server-addr='${cfg.server.addr}'" ] @@ -232,6 +233,17 @@ in }; }; + lock.backend = lib.mkOption { + type = lib.types.enum [ + "local" + "redis" + ]; + default = "local"; + description = '' + Lock backend to use: 'local' (single instance), 'redis' (distributed). + ''; + }; + maxSize = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -487,6 +499,14 @@ in cfg.cache.redis == null || cfg.cache.redis.password == null || cfg.cache.redis.passwordFile == null; message = "You cannot specify both config.ncps.cache.redis.password and config.ncps.cache.redis.passwordFile"; } + { + assertion = cfg.cache.lock.backend == "redis" -> cfg.cache.redis != null; + message = "You must specify config.ncps.cache.redis when config.ncps.cache.lock.backend is set to 'redis'"; + } + { + assertion = cfg.cache.redis != null -> cfg.cache.lock.backend == "redis"; + message = "You must set config.ncps.cache.lock.backend to 'redis' when config.ncps.cache.redis is set"; + } ]; users.users.ncps = { diff --git a/nixos/tests/ncps-ha.nix b/nixos/tests/ncps-ha.nix index e4e8759b9f7c..4bf5c6832ab8 100644 --- a/nixos/tests/ncps-ha.nix +++ b/nixos/tests/ncps-ha.nix @@ -35,6 +35,8 @@ let databaseURL = "postgres://ncps:${lib.escapeURL postgresPassword}@postgres:5432/ncps?sslmode=disable"; + lock.backend = "redis"; + secretKeyPath = builtins.toString ( pkgs.writeText "ncps-cache-key" "ncps:dcrGsrku0KvltFhrR5lVIMqyloAdo0y8vYZOeIFUSLJS2IToL7dPHSSCk/fi+PJf8EorpBn8PU7MNhfvZoI8mA==" );