{ config, lib, pkgs, ... }: let cfg = config.services.nix-sync; esa = lib.strings.escapeShellArg; mkTimer = name: repo: { description = "Nix sync ${name} timer"; wantedBy = ["timers.target"]; timerConfig = { OnUnitActiveSec = repo.interval; }; wants = ["network-online.target"]; after = ["network-online.target"]; }; parents = path: let split_path = builtins.split "/" path; filename = builtins.elemAt split_path (builtins.length split_path - 1); path_build = lib.strings.removeSuffix "/" (builtins.replaceStrings [filename] [""] path); final_path = if filename == "" then parents path_build else path_build; in final_path; mkUnit = name: repo: let optionalPathSeparator = if lib.strings.hasPrefix "/" repo.path then "" else "/"; /* * `ln` tries to create a symlink in the directory, if the target ends with a '/', * thus remove it. */ repoPath = lib.strings.removeSuffix "/" repo.path; repoCachePath = cfg.cachePath + optionalPathSeparator + repo.path; execStartScript = pkgs.writeScript "nix-sync-exec" '' #! /usr/bin/env dash export XDG_CACHE_HOME="$CACHE_DIRECTORY"; cd ${esa repoCachePath}; git fetch origin="$(git rev-parse @{u})"; branch="$(git rev-parse @)"; if ! [ "$origin" = "$branch" ]; then git pull --rebase; out_paths=$(mktemp); nix build . --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths"; [ "$(wc -l < "$out_paths")" -gt 1 ] && {echo "To many out-paths"; exit 1;} out_path="$(cat "$out_paths")"; rm ${esa repoPath}; ln -s "$out_path" ${esa repoPath}; rm "$out_paths"; fi ''; execStartPreScript = '' export XDG_CACHE_HOME="$CACHE_DIRECTORY"; if ! [ -d ${esa repoCachePath}/.git ]; then mkdir --parents ${esa repoCachePath}; git clone ${esa repo.uri} ${esa repoCachePath}; out_paths=$(mktemp); nix build ${esa repoCachePath} --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths"; [ "$(wc -l < "$out_paths")" -gt 1 ] && {echo "To many out-paths"; exit 1;} out_path="$(cat "$out_paths")"; ln -s "$out_path" ${esa repoPath}; rm "$out_paths"; fi if ! [ -L ${esa repoPath} ]; then cd ${esa repoCachePath}; git pull --rebase; out_paths=$(mktemp); nix build . --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths"; [ "$(wc -l < "$out_paths")" -gt 1 ] && { echo "To many out-paths"; exit 1; } out_path="$(cat "$out_paths")"; if [ -d ${esa repoPath} ]; then rm -d ${esa repoPath}; else mkdir --parents "$(dirname ${esa repoPath})"; fi [ -e ${esa repoPath} ] && rm ${esa repoPath}; ln -s "$out_path" ${esa repoPath}; rm "$out_paths"; fi ''; in { description = "Nix Sync ${name}"; wantedBy = ["default.target"]; after = ["network.target"]; path = with pkgs; [openssh git nix mktemp coreutils dash]; preStart = execStartPreScript; serviceConfig = { TimeoutSec = 0; ExecStart = execStartScript; Restart = "on-abort"; # User and group User = cfg.user; Group = cfg.group; # Runtime directory and mode RuntimeDirectory = "nix-sync"; RuntimeDirectoryMode = "0750"; # Cache directory and mode CacheDirectory = "nix-sync"; CacheDirectoryMode = "0750"; # Logs directory and mode LogsDirectory = "nix-sync"; LogsDirectoryMode = "0750"; # Proc filesystem ProcSubset = "all"; ProtectProc = "invisible"; # New file permissions UMask = "0027"; # 0640 / 0750 # Capabilities AmbientCapabilities = ["CAP_CHOWN"]; CapabilityBoundingSet = ["CAP_CHOWN"]; # Security NoNewPrivileges = true; # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html) ReadWritePaths = ["${esa (parents repo.path)}" "-${esa (parents repoCachePath)}" "-${esa cfg.cachePath}"]; ReadOnlyPaths = ["/nix"]; # TODO: Should be irrelevant, as we have ProtectSystem=Strict <2024-06-01> ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; ProtectHostname = true; ProtectClock = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; ProtectControlGroups = true; RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"]; RestrictNamespaces = true; LockPersonality = true; MemoryDenyWriteExecute = true; RestrictRealtime = true; RestrictSUIDSGID = true; RemoveIPC = true; PrivateMounts = true; # System Call Filtering SystemCallArchitectures = "native"; SystemCallFilter = ["~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"]; }; }; services = lib.mapAttrs' (name: repo: { name = "nix-sync-${name}"; value = mkUnit name repo; }) cfg.repositories; timers = lib.mapAttrs' (name: repo: { name = "nix-sync-${name}"; value = mkTimer name repo; }) cfg.repositories; # generate the websites directory, so systemd can mount it read write generatedDirectories = lib.mapAttrsToList ( _: repo: "d ${esa (parents repo.path)} 0755 ${cfg.user} ${cfg.group}" ) cfg.repositories; repositoryType = lib.types.submodule ({name, ...}: { options = { name = lib.mkOption { internal = true; default = name; type = lib.types.str; description = "The name that should be given to this unit."; }; path = lib.mkOption { type = lib.types.str; description = "The path at which to sync the repository"; }; uri = lib.mkOption { type = lib.types.str; example = "ssh://user@example.com:/~[user]/path/to/repo.git"; description = '' The URI of the remote to be synchronized. This is only used in the event that the directory does not already exist. See for the supported URIs. ''; }; extraSettings = lib.mkOption { type = lib.types.attrsOf lib.types.anything; example = lib.literalExpression '' { locations."/.well-known/openpgpkey/hu/" = { extraConfig = \'\' default_type application/octet-stream; add_header Access-Control-Allow-Origin * always; \'\'; }; } ''; description = '' Extra config to add the the nginx virtual host. ''; }; interval = lib.mkOption { type = lib.types.int; default = 500; description = '' The interval, specified in seconds, at which the synchronization will be triggered. ''; }; }; }); in { options = { services.nix-sync = { enable = lib.mkEnableOption "nix-sync services"; user = lib.mkOption { type = lib.types.str; default = "nix-sync"; description = lib.mdDoc "User account under which nix-sync units runs."; }; group = lib.mkOption { type = lib.types.str; default = "nix-sync"; description = lib.mdDoc "Group account under which nix-sync units runs."; }; cachePath = lib.mkOption { type = lib.types.str; default = "/var/lib/nix-sync"; description = lib.mdDoc '' Where to cache git directories. Should not end with a slash ("/") ''; }; repositories = lib.mkOption { type = with lib.types; attrsOf repositoryType; description = '' The repositories that should be synchronized. ''; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = !lib.strings.hasSuffix "/" cfg.cachePath; message = "Your cachePath ('${cfg.cachePath}') ends with a slash ('/'), please use: '${lib.strings.removeSuffix "/" cfg.cachePath}'."; } ]; systemd = { tmpfiles.rules = generatedDirectories; inherit services timers; }; users.users = if cfg.user == "nix-sync" then { nix-sync = { group = "${cfg.group}"; isSystemUser = true; }; } else lib.warnIf (cfg.user != "nix-sync") "The user (${cfg.user}) is not \"nix-sync\", thus you are responible for generating it."; users.groups = if cfg.group == "nix-sync" then { nix-sync = { members = ["${cfg.user}"]; }; } else lib.warnIf (cfg.group != "nix-sync") "The group (${cfg.group}) is not \"nix-sync\", thus you are responible for generating it."; }; }