blob: 4e285862971613f06a88f0dbfbbd081615992319 (
plain) (
tree)
|
|
{
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
<link xlink:href="https://git-scm.com/docs/git-clone#_git_urls"/>
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.";
};
}
|