summary refs log tree commit diff
path: root/module.nix
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 /module.nix
downloads6-rc.nix-785f9e04b123b8fd3b5a6fd0901f4ac5177fd752.tar.gz
s6-rc.nix-785f9e04b123b8fd3b5a6fd0901f4ac5177fd752.zip
nixos module
Diffstat (limited to 'module.nix')
-rw-r--r--module.nix371
1 files changed, 371 insertions, 0 deletions
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}";
+  };
+}