{ config, lib, pkgs, ... }: let cfg = config.services.nix-sync; mkUnit = name: repo: let esa = lib.strings.escapeShellArg; parents = path: let split_path = builtins.split "/" path; filename = builtins.elemAt split_path (builtins.length split_path - 1); in lib.strings.removeSuffix "/" (builtins.replaceStrings [filename] [""] path); optionalPathSeparator = if lib.strings.hasPrefix "/" repo.path then "" else "/"; repoCachePath = cfg.cachePath + optionalPathSeparator + repo.path; execStartScript = pkgs.writeScript "git-sync-exec" '' #! /usr/bin/env dash cd ${esa repoCachePath}; while true; do origin="$(git rev-parse @{u})"; branch="$(git rev-parse @)"; if ! [ "$origin" = "$branch" ]; then git pull; 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 -r ${esa repo.path}; ln -s "$out_path" ${esa repo.path}; rm "$out-paths"; fi sleep ${esa repo.interval}; done ''; execStartPreScript = '' if ! stat ${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 repo.path}; 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 = { ExecStart = execStartScript; Restart = "on-abort"; # User and group User = cfg.user; Group = cfg.group; # Runtime directory and mode RuntimeDirectory = "nginx"; RuntimeDirectoryMode = "0750"; # Cache directory and mode CacheDirectory = "nginx"; CacheDirectoryMode = "0750"; # Logs directory and mode LogsDirectory = "nginx"; LogsDirectoryMode = "0750"; # Proc filesystem ProcSubset = "pid"; 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 repoCachePath}" "-${esa cfg.cachePath}"]; ReadOnlyPaths = ["/nix"]; 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; 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 = "git+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. ''; }; interval = lib.mkOption { type = lib.types.int; default = 500; description = '' The interval, specified in seconds, at which the synchronization will be triggered even without filesystem changes. ''; }; }; }); in { options = { services.nix-sync = { enable = lib.mkEnableOption "git-sync services"; package = lib.mkOption { type = lib.types.package; default = pkgs.git-sync; defaultText = lib.literalExpression "pkgs.git-sync"; description = '' Package containing the git-sync program. ''; }; 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}'."; } ]; # generate the websites directory, so systemd can mount it rw environment.etc."nginx/websites/.keep".text = "keep this directory"; systemd.services = services; 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."; }; } # vim: ts=2