nixos/acme: switch concurrency limit to a runtime-based implementation

The previous implementation caused triggers on many units when adding
or removing certificates because the baked-in lock file assignments
changed.

Changed files
+28 -62
nixos
modules
security
+28 -62
nixos/modules/security/acme/default.nix
···
# Since that service is a oneshot with RemainAfterExit,
# the folder will exist during all renewal services.
lockdir = "/run/acme/";
-
concurrencyLockfiles = map (n: "${toString n}.lock") (lib.range 1 cfg.maxConcurrentRenewals);
-
# Assign elements of `baseList` to each element of `needAssignmentList`, until the latter is exhausted.
-
# returns: [{fst = "element of baseList"; snd = "element of needAssignmentList"}]
-
roundRobinAssign =
-
baseList: needAssignmentList:
-
if baseList == [ ] then [ ] else _rrCycler baseList baseList needAssignmentList;
-
_rrCycler =
-
with builtins;
-
origBaseList: workingBaseList: needAssignmentList:
-
if (workingBaseList == [ ] || needAssignmentList == [ ]) then
-
[ ]
-
else
-
[
-
{
-
fst = head workingBaseList;
-
snd = head needAssignmentList;
-
}
-
]
-
++ _rrCycler origBaseList (
-
if (tail workingBaseList == [ ]) then origBaseList else tail workingBaseList
-
) (tail needAssignmentList);
-
attrsToList = lib.mapAttrsToList (
-
attrname: attrval: {
-
name = attrname;
-
value = attrval;
-
}
-
);
-
# for an AttrSet `funcsAttrs` having functions as values, apply single arguments from
-
# `argsList` to them in a round-robin manner.
-
# Returns an attribute set with the applied functions as values.
-
roundRobinApplyAttrs =
-
funcsAttrs: argsList:
-
lib.listToAttrs (
-
map (x: {
-
inherit (x.snd) name;
-
value = x.snd.value x.fst;
-
}) (roundRobinAssign argsList (attrsToList funcsAttrs))
-
);
+
wrapInFlock =
-
lockfilePath: script:
+
script:
# explainer: https://stackoverflow.com/a/60896531
''
-
exec {LOCKFD}> ${lockfilePath}
-
echo "Waiting to acquire lock ${lockfilePath}"
-
${pkgs.flock}/bin/flock ''${LOCKFD} || exit 1
-
echo "Acquired lock ${lockfilePath}"
+
maxConcurrentRenewals=${toString cfg.maxConcurrentRenewals}
+
+
acquireLock() {
+
echo "Waiting to acquire lock in ${lockdir}"
+
while true; do
+
for i in $(seq 1 $maxConcurrentRenewals); do
+
exec {LOCKFD}> "${lockdir}/$i.lock"
+
if ${pkgs.flock}/bin/flock -n ''${LOCKFD}; then
+
return 0
+
fi
+
exec {LOCKFD}>&-
+
done
+
sleep 1;
+
done
+
}
+
+
if [ "$maxConcurrentRenewals" -gt "0" ]; then
+
acquireLock
+
fi
''
-
+ script
-
+ "\n"
-
+ ''echo "Releasing lock ${lockfilePath}" # only released after process exit'';
+
+ script;
# There are many services required to make cert renewals work.
# They all follow a common structure:
···
};
};
-
baseService = lockfileName: {
+
baseService = {
description = "Ensure certificate for ${cert}";
wantedBy = [ "multi-user.target" ];
···
# Working directory will be /tmp
# minica will output to a folder sharing the name of the first domain
# in the list, which will be ${data.domain}
-
script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") ''
+
script = wrapInFlock ''
set -ex
# Regenerate self-signed certificates (in case the SANs change) until we
···
'';
};
-
orderRenewService = lockfileName: {
+
orderRenewService = {
description = "Order (and renew) ACME certificate for ${cert}";
after = [
"network.target"
···
};
# Working directory will be /tmp
-
script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") ''
+
script = wrapInFlock ''
${lib.optionalString data.enableDebugLogs "set -x"}
set -euo pipefail
···
systemd.services =
let
-
orderRenewServiceFunctions = lib.mapAttrs' (
+
orderRenewServices = lib.mapAttrs' (
cert: conf: lib.nameValuePair "acme-order-renew-${cert}" conf.orderRenewService
) certConfigs;
-
orderRenewServices =
-
if cfg.maxConcurrentRenewals > 0 then
-
roundRobinApplyAttrs orderRenewServiceFunctions concurrencyLockfiles
-
else
-
lib.mapAttrs (_: f: f null) orderRenewServiceFunctions;
-
baseServiceFunctions = lib.mapAttrs' (
+
baseServices = lib.mapAttrs' (
cert: conf: lib.nameValuePair "acme-${cert}" conf.baseService
) certConfigs;
-
baseServices =
-
if cfg.maxConcurrentRenewals > 0 then
-
roundRobinApplyAttrs baseServiceFunctions concurrencyLockfiles
-
else
-
lib.mapAttrs (_: f: f null) baseServiceFunctions;
in
acme-setup = setupService;