{
config,
lib,
pkgs,
...
}: let
cfg = config.services.nix-sync;
mkUnit = name: repo: let
esa = lib.strings.escapeShellArg;
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 = ["/etc/nginx/websites" "-${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