summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMalte Voos <malte@malvo.org>2022-05-12 17:39:22 +0200
committerMalte Voos <malte@malvo.org>2022-05-12 17:39:22 +0200
commit785f9e04b123b8fd3b5a6fd0901f4ac5177fd752 (patch)
tree6cdbe75d8f334d48b1daf49e80acfc9f11c2db24
downloads6-rc.nix-785f9e04b123b8fd3b5a6fd0901f4ac5177fd752.tar.gz
s6-rc.nix-785f9e04b123b8fd3b5a6fd0901f4ac5177fd752.zip
nixos module
-rw-r--r--flake.nix5
-rw-r--r--module.nix371
2 files changed, 376 insertions, 0 deletions
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..8c4dc5c
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,5 @@
+{
+ outputs = { ... }: {
+ nixosModule = import ./module.nix;
+ };
+}
diff --git a/module.nix b/module.nix
new file mode 100644
index 0000000..bd607c8
--- /dev/null
+++ b/module.nix
@@ -0,0 +1,371 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+ writeExeclineScript = name: text: pkgs.writeScript name ''
+ #!${pkgs.execline}/bin/execlineb -P
+ ${text}
+ '';
+
+ cfg = config.s6;
+
+ # -------- configuration for s6-rc -------- #
+
+ loggerNames = flatten (mapAttrsToList (_: srv: optional (srv.logger != null) srv.logger) cfg.longruns);
+
+ loggingRelations = genAttrs loggerNames
+ (loggerName: filter
+ (serviceName: cfg.longruns.${serviceName}.logger == loggerName)
+ (attrNames cfg.longruns));
+
+ loggerLongruns = mapAttrs'
+ (loggerName: producers:
+ nameValuePair "${loggerName}-log" {
+ run = ''
+ s6-setuidgid ${cfg.logUser}
+ exec -c
+ s6-log -d 3 -- ${cfg.loggingScript loggerName}
+ '';
+ notificationFd = 3;
+ consumerFor = producers;
+ pipelineName = "${loggerName}-pipeline";
+ flagEssential = false;
+ timeoutUp = null;
+ timeoutDown = null;
+ dependencies = null;
+ finish = null;
+ timeoutKill = null;
+ timeoutFinish = null;
+ maxDeathTally = null;
+ downSignal = null;
+ producerFor = null;
+ logger = null;
+ })
+ loggingRelations;
+
+ allLongruns = cfg.longruns // loggerLongruns;
+
+ makeCommonConfig = srv: ''
+ mkdir -p $out
+ '' + optionalString srv.flagEssential ''
+ touch $out/flagEssential
+ '';
+
+ makeBundleConfig = srv: makeCommonConfig srv + ''
+ echo bundle > $out/type
+ mkdir $out/contents.d
+ '' + concatMapStringsSep "\n"
+ (srvName: ''touch "$out/contents.d/${srvName}"'')
+ srv.contents + "\n";
+
+ makeAtomicConfig = srv: makeCommonConfig srv
+ + optionalString (srv.timeoutUp != null) ''
+ echo "${toString srv.timeoutUp}" > $out/timeout-up
+ '' + optionalString (srv.timeoutDown != null) ''
+ echo "${toString srv.timeoutDown}" > $out/timeout-down
+ '' + optionalString (srv.dependencies != null) (
+ ''
+ mkdir $out/dependencies.d
+ '' + concatMapStringsSep "\n"
+ (srvName: ''touch "$out/dependencies.d/${srvName}"'')
+ srv.dependencies + "\n"
+ );
+
+ makeOneshotConfig = srv:
+ let
+ upScript = pkgs.writeText "up-script" srv.up;
+ downScript = mapNullable (pkgs.writeText "down-script") srv.down;
+ in
+ makeAtomicConfig srv + ''
+ echo oneshot > $out/type
+ cp ${upScript} $out/up
+ '' + optionalString (downScript != null) ''
+ cp ${downScript} $out/down
+ '';
+
+ makeLongrunConfig = srv:
+ let
+ runScript = writeExeclineScript "run-script" srv.run;
+ finishScript = mapNullable (writeExeclineScript "finish-script") srv.finish;
+ in
+ makeAtomicConfig srv + ''
+ echo longrun > $out/type
+ cp ${runScript} $out/run
+ '' + optionalString (finishScript != null) ''
+ cp ${finishScript} $out/finish
+ '' + optionalString (srv.notificationFd != null) ''
+ echo "${toString srv.notificationFd}" > $out/notification-fd
+ '' + optionalString (srv.timeoutKill != null) ''
+ echo "${toString srv.timeoutKill}" > $out/timeout-kill
+ '' + optionalString (srv.timeoutFinish != null) ''
+ echo "${toString srv.timeoutFinish}" > $out/timeout-finish
+ '' + optionalString (srv.maxDeathTally != null) ''
+ echo "${toString srv.maxDeathTally}" > $out/max-death-tally
+ '' + optionalString (srv.downSignal != null) ''
+ echo "${srv.downSignal}" > $out/down-signal
+ '' + optionalString (srv.producerFor != null) ''
+ echo "${srv.producerFor}" > $out/producer-for
+ '' + optionalString (srv.consumerFor != null) ''
+ echo "${concatStringsSep "\n" srv.consumerFor}" > $out/consumer-for
+ '' + optionalString (srv.pipelineName != null) ''
+ echo "${srv.pipelineName}" > $out/pipeline-name
+ '' + optionalString (srv.logger != null) ''
+ echo "${srv.logger}-log" > $out/producer-for
+ '';
+
+ makeBundleDefinitionDir = name: srv: pkgs.runCommand
+ "s6-rc-bundle-definition-${name}"
+ { }
+ (makeBundleConfig srv);
+ makeOneshotDefinitionDir = name: srv: pkgs.runCommand
+ "s6-rc-oneshot-definition-${name}"
+ { }
+ (makeOneshotConfig srv);
+ makeLongrunDefinitionDir = name: srv: pkgs.runCommand
+ "s6-rc-longrun-definition-${name}"
+ { }
+ (makeLongrunConfig srv);
+
+ bundleDefinitionDirs = mapAttrs makeBundleDefinitionDir cfg.bundles;
+ oneshotDefinitionDirs = mapAttrs makeOneshotDefinitionDir cfg.oneshots;
+ longrunDefinitionDirs = mapAttrs makeLongrunDefinitionDir allLongruns;
+
+ allDefinitionDirs = bundleDefinitionDirs // oneshotDefinitionDirs // longrunDefinitionDirs;
+
+ serviceSourceDir = pkgs.linkFarm "s6-rc-service-source"
+ (mapAttrsToList
+ (srvName: definitionDir: { name = srvName; path = definitionDir; })
+ allDefinitionDirs);
+
+ compiledDatabase = pkgs.runCommand "s6-rc-compiled-database" { } ''
+ ${pkgs.s6-rc}/bin/s6-rc-compile $out ${serviceSourceDir}
+ '';
+
+ # -------- init script -------- #
+
+ initialPath = lib.makeBinPath (with pkgs; [
+ coreutils
+ execline
+ s6
+ s6-rc
+ ]);
+
+ catchAllLoggerServiceDir =
+ let
+ runScript = writeExeclineScript "catch-all-logger-run-script" ''
+ s6-setuidgid "${cfg.logUser}"
+ redirfd -w 1 /dev/null
+ redirfd -rnb 0 "${cfg.logFifo}"
+ s6-log -bpd3 -- ${cfg.loggingScript "uncaught"}
+ '';
+ in
+ pkgs.runCommand "catch-all-logger-srvdir" { } ''
+ mkdir -p $out
+ cp ${runScript} $out/run
+ echo 3 > $out/notification-fd
+ '';
+
+ # this runs after NixOS' stage-2-init.sh
+ initScript = writeExeclineScript "init-script" ''
+ ${pkgs.execline}/bin/export PATH "${initialPath}"
+ ${pkgs.execline}/bin/exec
+
+ # stage-2-init.sh prints the line "starting systemd..."
+ foreground { echo "...NOT! starting s6-svscan on ${cfg.scanDir}..." }
+
+ if { mkdir -p "${cfg.scanDir}" }
+ if { cp -r "${catchAllLoggerServiceDir}" "${cfg.scanDir}/catch-all-log" }
+
+ if { mkdir -p "${cfg.logDir}" }
+ if { chown "${cfg.logUser}:${cfg.logGroup}" "${cfg.logDir}" }
+ if { chmod 700 "${cfg.logDir}" }
+
+ if { mkfifo "${cfg.logFifo}" }
+ if { chown "${cfg.logUser}:${cfg.logGroup}" "${cfg.logFifo}" }
+ if { chmod 600 "${cfg.logFifo}" }
+
+ redirfd -r 0 /dev/null # don't use /dev/console as stdin
+ redirfd -wnb 1 "${cfg.logFifo}" # redirect stdout to fifo
+ fdmove -c 2 1 # redirect stderr to fifo
+
+ background {
+ redirfd -w 3 "${cfg.logFifo}" # this blocks until the catch-all logger is running
+ if { s6-rc-init -c "${compiledDatabase}" "${cfg.scanDir}" }
+ s6-rc -v2 change "${cfg.allBundle}"
+ }
+
+ exec -a s6-svscan # name it 's6-svscan' instead of /nix/store/...
+ s6-svscan "${cfg.scanDir}"
+ '';
+in
+{
+ options = with types;
+ let
+ mkNullableOption = { type, ... } @ o:
+ mkOption o // { type = nullOr type; default = null; };
+
+ # see: https://skarnet.org/software/s6-rc/s6-rc-compile.html#source
+
+ commonOptions = {
+ flagEssential = mkOption {
+ type = bool;
+ default = false;
+ };
+ };
+
+ bundleOptions = {
+ contents = mkOption {
+ type = listOf str;
+ };
+ } // commonOptions;
+
+ atomicOptions = {
+ timeoutUp = mkNullableOption {
+ type = ints.unsigned;
+ };
+ timeoutDown = mkNullableOption {
+ type = ints.unsigned;
+ };
+ dependencies = mkNullableOption {
+ type = listOf str;
+ };
+ } // commonOptions;
+
+ oneshotOptions = {
+ up = mkOption {
+ type = str;
+ };
+ down = mkNullableOption {
+ type = str;
+ };
+ } // atomicOptions;
+
+ longrunOptions = {
+ run = mkOption {
+ type = str;
+ };
+ finish = mkNullableOption {
+ type = str;
+ };
+ notificationFd = mkNullableOption {
+ type = ints.unsigned;
+ };
+ timeoutKill = mkNullableOption {
+ type = ints.unsigned;
+ };
+ timeoutFinish = mkNullableOption {
+ type = ints.unsigned;
+ };
+ maxDeathTally = mkNullableOption {
+ type = ints.between 0 4096;
+ };
+ downSignal = mkNullableOption {
+ type = str;
+ };
+ producerFor = mkNullableOption {
+ type = str;
+ };
+ consumerFor = mkNullableOption {
+ type = listOf str;
+ };
+ pipelineName = mkNullableOption {
+ type = str;
+ };
+ # this is not an actual option in s6-rc; it creates a logger service
+ # with the name "<logger>-log" and adds the necessary "producer-for"
+ # and "consumer-for" files.
+ logger = mkNullableOption {
+ type = str;
+ };
+ } // atomicOptions;
+ in
+ {
+ s6 = {
+ enable = mkEnableOption "s6 init system";
+ allBundle = mkOption {
+ type = str;
+ };
+ logUser = mkOption {
+ type = str;
+ default = "log";
+ };
+ logGroup = mkOption {
+ type = str;
+ default = "log";
+ };
+ logDir = mkOption {
+ type = path;
+ default = "/log";
+ };
+ logFifo = mkOption {
+ type = path;
+ default = "/run/log-fifo";
+ };
+ loggingScript = mkOption {
+ type = anything;
+ default = name: "T s1000000 n20 ${cfg.logDir}/${name}";
+ };
+ scanDir = mkOption {
+ type = path;
+ default = "/run/service";
+ };
+
+ bundles = mkOption {
+ type = with types; attrsOf (submodule {
+ options = bundleOptions;
+ });
+ };
+
+ oneshots = mkOption {
+ type = with types; attrsOf (submodule {
+ options = oneshotOptions;
+ });
+ default = { };
+ };
+
+ longruns = mkOption {
+ type = with types; attrsOf (submodule {
+ options = longrunOptions;
+ });
+ default = { };
+ };
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = hasAttr cfg.allBundle cfg.bundles;
+ message = "the bundle '${cfg.allBundle}' does not exist!";
+ }
+ {
+ assertion = !(elem "uncaught" loggerNames);
+ message = "the logger name 'uncaught' is reserved";
+ }
+ {
+ assertion = length (attrNames allDefinitionDirs)
+ == length (attrNames cfg.bundles)
+ + length (attrNames cfg.oneshots)
+ + length (attrNames cfg.longruns)
+ + length (attrNames loggerLongruns);
+ message = "no two services can have the same name, even if they are of different types";
+ }
+ ] ++ mapAttrsToList
+ (name: def: {
+ assertion = def.producerFor == null || def.logger == null;
+ message = "in s6.longruns.${name}: 'producerFor' and 'logger' are mutually exclusive";
+ })
+ cfg.longruns;
+
+ users = {
+ users.${cfg.logUser} = {
+ isSystemUser = true;
+ group = cfg.logGroup;
+ home = cfg.logDir;
+ };
+ groups.${cfg.logGroup} = { };
+ };
+
+ # checkmate!
+ boot.systemdExecutable = "${initScript}";
+ };
+}