diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6910458baf40..58f499c3d1fb 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -980,6 +980,7 @@ ./services/networking/aria2.nix ./services/networking/asterisk.nix ./services/networking/atftpd.nix + ./services/networking/atticd.nix ./services/networking/autossh.nix ./services/networking/avahi-daemon.nix ./services/networking/babeld.nix diff --git a/nixos/modules/services/networking/atticd.nix b/nixos/modules/services/networking/atticd.nix new file mode 100644 index 000000000000..5397bda31a9f --- /dev/null +++ b/nixos/modules/services/networking/atticd.nix @@ -0,0 +1,233 @@ +{ + lib, + pkgs, + config, + ... +}: + +let + inherit (lib) types; + + cfg = config.services.atticd; + + format = pkgs.formats.toml { }; + + checkedConfigFile = + pkgs.runCommand "checked-attic-server.toml" + { + configFile = format.generate "server.toml" cfg.settings; + } + '' + export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${lib.getExe pkgs.openssl} genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)" + export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:" + ${lib.getExe cfg.package} --mode check-config -f $configFile + cat <$configFile >$out + ''; + + atticadmShim = pkgs.writeShellScript "atticadm" '' + if [ -n "$ATTICADM_PWD" ]; then + cd "$ATTICADM_PWD" + if [ "$?" != "0" ]; then + >&2 echo "Warning: Failed to change directory to $ATTICADM_PWD" + fi + fi + + exec ${cfg.package}/bin/atticadm -f ${checkedConfigFile} "$@" + ''; + + atticadmWrapper = pkgs.writeShellScriptBin "atticd-atticadm" '' + exec systemd-run \ + --quiet \ + --pipe \ + --pty \ + --same-dir \ + --wait \ + --collect \ + --service-type=exec \ + --property=EnvironmentFile=${cfg.environmentFile} \ + --property=DynamicUser=yes \ + --property=User=${cfg.user} \ + --property=Environment=ATTICADM_PWD=$(pwd) \ + --working-directory / \ + -- \ + ${atticadmShim} "$@" + ''; + + hasLocalPostgresDB = + let + url = cfg.settings.database.url or ""; + localStrings = [ + "localhost" + "127.0.0.1" + "/run/postgresql" + ]; + hasLocalStrings = lib.any (lib.flip lib.hasInfix url) localStrings; + in + config.services.postgresql.enable && lib.hasPrefix "postgresql://" url && hasLocalStrings; +in +{ + options = { + services.atticd = { + enable = lib.mkEnableOption "the atticd, the Nix Binary Cache server"; + + package = lib.mkPackageOption pkgs "attic-server" { }; + + environmentFile = lib.mkOption { + description = '' + Path to an EnvironmentFile containing required environment + variables: + + - ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64: The base64-encoded RSA PEM PKCS1 of the + RS256 JWT secret. Generate it with `openssl genrsa -traditional 4096 | base64 -w0`. + ''; + type = types.nullOr types.path; + default = null; + }; + + user = lib.mkOption { + description = '' + The group under which attic runs. + ''; + type = types.str; + default = "atticd"; + }; + + group = lib.mkOption { + description = '' + The user under which attic runs. + ''; + type = types.str; + default = "atticd"; + }; + + settings = lib.mkOption { + description = '' + Structured configurations of atticd. + See https://github.com/zhaofengli/attic/blob/main/server/src/config-template.toml + ''; + type = format.type; + default = { }; + }; + + mode = lib.mkOption { + description = '' + Mode in which to run the server. + + 'monolithic' runs all components, and is suitable for single-node deployments. + + 'api-server' runs only the API server, and is suitable for clustering. + + 'garbage-collector' only runs the garbage collector periodically. + + A simple NixOS-based Attic deployment will typically have one 'monolithic' and any number of 'api-server' nodes. + + There are several other supported modes that perform one-off operations, but these are the only ones that make sense to run via the NixOS module. + ''; + type = lib.types.enum [ + "monolithic" + "api-server" + "garbage-collector" + ]; + default = "monolithic"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.environmentFile != null; + message = '' + is not set. + + Run `openssl genrsa -traditional 4096 | base64 -w0` and create a file with the following contents: + + ATTIC_SERVER_TOKEN_RS256_SECRET="output from command" + + Then, set `services.atticd.environmentFile` to the quoted absolute path of the file. + ''; + } + ]; + + services.atticd.settings = { + chunking = lib.mkDefault { + nar-size-threshold = 65536; + min-size = 16384; # 16 KiB + avg-size = 65536; # 64 KiB + max-size = 262144; # 256 KiB + }; + + database.url = lib.mkDefault "sqlite:///var/lib/atticd/server.db?mode=rwc"; + + # "storage" is internally tagged + # if the user sets something the whole thing must be replaced + storage = lib.mkDefault { + type = "local"; + path = "/var/lib/atticd/storage"; + }; + }; + + systemd.services.atticd = { + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ] ++ lib.optionals hasLocalPostgresDB [ "postgresql.service" ]; + requires = lib.optionals hasLocalPostgresDB [ "postgresql.service" ]; + + serviceConfig = { + ExecStart = "${lib.getExe cfg.package} -f ${checkedConfigFile} --mode ${cfg.mode}"; + EnvironmentFile = cfg.environmentFile; + StateDirectory = "atticd"; # for usage with local storage and sqlite + DynamicUser = true; + User = cfg.user; + Group = cfg.group; + Restart = "on-failure"; + RestartSec = 10; + + CapabilityBoundingSet = [ "" ]; + DeviceAllow = ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ReadWritePaths = + let + path = cfg.settings.storage.path; + isDefaultStateDirectory = path == "/var/lib/atticd" || lib.hasPrefix "/var/lib/atticd/" path; + in + lib.optionals (cfg.settings.storage.type or "" == "local" && !isDefaultStateDirectory) [ path ]; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@resources" + "~@privileged" + ]; + UMask = "0077"; + }; + }; + + environment.systemPackages = [ + atticadmWrapper + ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0e2a21803c2e..a3fb29440c2c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -136,6 +136,7 @@ in { artalk = handleTest ./artalk.nix {}; atd = handleTest ./atd.nix {}; atop = handleTest ./atop.nix {}; + atticd = runTest ./atticd.nix; atuin = handleTest ./atuin.nix {}; audiobookshelf = handleTest ./audiobookshelf.nix {}; auth-mysql = handleTest ./auth-mysql.nix {}; diff --git a/nixos/tests/atticd.nix b/nixos/tests/atticd.nix new file mode 100644 index 000000000000..4193d75d2a92 --- /dev/null +++ b/nixos/tests/atticd.nix @@ -0,0 +1,92 @@ +{ lib, pkgs, ... }: + +let + accessKey = "BKIKJAA5BMMU2RHO6IBB"; + secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12"; + + minioCredentialsFile = pkgs.writeText "minio-credentials-full" '' + MINIO_ROOT_USER=${accessKey} + MINIO_ROOT_PASSWORD=${secretKey} + ''; + environmentFile = pkgs.runCommand "atticd-env" { } '' + echo ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${lib.getExe pkgs.openssl} genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)" > $out + ''; +in + +{ + name = "atticd"; + + nodes = { + local = { + services.atticd = { + enable = true; + + inherit environmentFile; + }; + + environment.systemPackages = [ + pkgs.attic-client + ]; + }; + + s3 = { + services.atticd = { + enable = true; + settings = { + storage = { + type = "s3"; + bucket = "attic"; + region = "us-east-1"; + endpoint = "http://127.0.0.1:9000"; + + credentials = { + access_key_id = accessKey; + secret_access_key = secretKey; + }; + }; + }; + + inherit environmentFile; + }; + + services.minio = { + enable = true; + rootCredentialsFile = minioCredentialsFile; + }; + + environment.systemPackages = [ + pkgs.attic-client + pkgs.minio-client + ]; + }; + }; + + testScript = # python + '' + start_all() + + with subtest("local storage push"): + local.wait_for_unit("atticd.service") + token = local.succeed("atticd-atticadm make-token --sub stop --validity 1y --create-cache '*' --pull '*' --push '*' --delete '*' --configure-cache '*' --configure-cache-retention '*'").strip() + + local.succeed(f"attic login local http://localhost:8080 {token}") + local.succeed("attic cache create test-cache") + local.succeed("attic push test-cache ${environmentFile}") + + with subtest("s3 storage push"): + s3.wait_for_unit("atticd.service") + s3.wait_for_unit("minio.service") + s3.wait_for_open_port(9000) + s3.succeed( + "mc config host add minio " + + "http://localhost:9000 " + + "${accessKey} ${secretKey} --api s3v4", + "mc mb minio/attic", + ) + token = s3.succeed("atticd-atticadm make-token --sub stop --validity 1y --create-cache '*' --pull '*' --push '*' --delete '*' --configure-cache '*' --configure-cache-retention '*'").strip() + + s3.succeed(f"attic login s3 http://localhost:8080 {token}") + s3.succeed("attic cache create test-cache") + s3.succeed("attic push test-cache ${environmentFile}") + ''; +} diff --git a/pkgs/by-name/at/attic-client/package.nix b/pkgs/by-name/at/attic-client/package.nix index c45bad9f7102..3433db9802ec 100644 --- a/pkgs/by-name/at/attic-client/package.nix +++ b/pkgs/by-name/at/attic-client/package.nix @@ -2,6 +2,7 @@ , rustPlatform , fetchFromGitHub , nix +, nixosTests , boost , pkg-config , stdenv @@ -53,7 +54,13 @@ rustPlatform.buildRustPackage { fi ''; - passthru.updateScript = ./update.sh; + passthru = { + tests = { + inherit (nixosTests) atticd; + }; + + updateScript = ./update.sh; + }; meta = with lib; { description = "Multi-tenant Nix Binary Cache";