diff --git a/nix-services/default.nix b/nix-services/default.nix new file mode 100644 index 0000000..8c0d7f9 --- /dev/null +++ b/nix-services/default.nix @@ -0,0 +1,75 @@ +{ pkgs ? import { system = "x86_64-linux"; } +, name +, configuration ? [] +, baseImage ? "busybox" +}: + with pkgs.lib; + +let + moduleList = [ + ./user.nix ./supervisord.nix ./systemd.nix ./environment.nix + + + + + + + + + + + + + ]; + + config = (evalModules { + modules = configuration ++ moduleList; + args = { inherit pkgs; }; + }).config; + + systemd = import ./systemd.nix { inherit pkgs config; }; + + startServices = pkgs.writeScript "startServices" '' + #!/bin/sh + export STATEDIR="${"\$"}{STATEDIR-$(pwd)/var}" + export PATH="${pkgs.coreutils}/bin" + + mkdir -p $STATEDIR/{run,log} + + # Run start scripts first + ${config.userNix.startScript} + + # Run supervisord + ${pkgs.pythonPackages.supervisor}/bin/supervisord -c ${config.supervisord.configFile} -j $STATEDIR/run/supervisord.pid -d $STATEDIR -q $STATEDIR/log/ -l $STATEDIR/log/supervisord.log + ''; + + stopServices = pkgs.writeScript "stopServices" '' + #!/bin/sh + ${pkgs.pythonPackages.supervisor}/bin/supervisorctl -c ${config.supervisord.configFile} shutdown + ''; + + controlServices = pkgs.writeScript "controlServices" '' + #!/bin/sh + ${pkgs.pythonPackages.supervisor}/bin/supervisorctl -c ${config.supervisord.configFile} + ''; + + servicesControl = pkgs.stdenv.mkDerivation { + name = "${name}-servicesControl"; + src = ./.; + + phases = [ "installPhase" ]; + + installPhase = '' + ensureDir $out/bin/ + ln -s ${startServices} $out/bin/${name}-start-services + ln -s ${stopServices} $out/bin/${name}-stop-services + ln -s ${controlServices} $out/bin/${name}-control-services + ''; + + passthru.config = config; + }; + +in pkgs.buildEnv { + name = "${name}-services"; + paths = [ servicesControl ] ++ config.environment.systemPackages; +} diff --git a/nix-services/environment.nix b/nix-services/environment.nix new file mode 100644 index 0000000..fd3422b --- /dev/null +++ b/nix-services/environment.nix @@ -0,0 +1,23 @@ +{ config, pkgs, ... }: +with pkgs.lib; +{ + options = { + environment.systemPackages = mkOption { + default = []; + description = "Packages to be put in the system profile."; + }; + + environment.umask = mkOption { + default = "002"; + type = with types; string; + }; + + # HACK HACK + system.activationScripts.etc = mkOption {}; # Ignore + system.build.etc = mkOption {}; # Ignore + }; + + config = { + environment.systemPackages = with pkgs; [ coreutils ]; + }; +} diff --git a/nix-services/supervisord.nix b/nix-services/supervisord.nix new file mode 100644 index 0000000..9958371 --- /dev/null +++ b/nix-services/supervisord.nix @@ -0,0 +1,102 @@ +{ config, pkgs, ... }: +with pkgs.lib; +let + serviceOpts = { name, config, ...}: { + options = { + command = mkOption { + description = "The command to execute"; + }; + directory = mkOption { + default = "/"; + description = "Current directory when running the command"; + }; + environment = mkOption { + default = {}; + example = { + PATH = "/some/path"; + }; + }; + path = mkOption { + default = []; + description = "Current directory when running the command"; + }; + stopsignal = mkOption { + default = "TERM"; + }; + startsecs = mkOption { + default = 1; + example = 0; + }; + pidfile = mkOption { + default = null; + }; + }; + }; + services = config.supervisord.services; +in { + options = { + supervisord = { + enable = mkOption { + default = true; + type = types.bool; + }; + + services = mkOption { + default = {}; + type = types.loaOf types.optionSet; + description = '' + Supervisord services to start + ''; + options = [ serviceOpts ]; + }; + + tailLogs = mkOption { + default = false; + type = types.bool; + description = '' + Whether or not to tail all logs to standard out. + ''; + }; + + configFile = mkOption {}; + }; + }; + + config = mkIf config.supervisord.enable { + supervisord.configFile = pkgs.writeText "supervisord.conf" '' + [supervisord] + + [supervisorctl] + serverurl = http://localhost:64125 + + [inet_http_server] + port = 127.0.0.1:64125 + + [rpcinterface:supervisor] + supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + + ${concatMapStrings (name: + let + cfg = getAttr name services; + in + '' + [program:${name}] + command=${if cfg.pidfile == null then cfg.command else "${pkgs.pythonPackages.supervisor}/bin/pidproxy ${cfg.pidfile} ${cfg.command}"} + environment=${concatStrings + (mapAttrsToList (name: value: "${name}=\"${value}\",") ( + cfg.environment // { PATH = concatStringsSep ":" + [("%(ENV_PATH)s") (cfg.path) (maybeAttr "PATH" "" cfg.environment)]; + } + ) + )} + directory=${cfg.directory} + redirect_stderr=true + startsecs=${toString cfg.startsecs} + stopsignal=${cfg.stopsignal} + stopasgroup=true + '' + ) (attrNames services) + } + ''; + }; +} diff --git a/nix-services/systemd.nix b/nix-services/systemd.nix new file mode 100644 index 0000000..184ad87 --- /dev/null +++ b/nix-services/systemd.nix @@ -0,0 +1,65 @@ +{ pkgs, config, ... }: + with pkgs.lib; + with import { inherit config pkgs; }; +let + services = config.systemd.services; + + isOneShot = cfg: hasAttr "Type" cfg.serviceConfig && cfg.serviceConfig.Type == "oneshot"; + + runServices = filterAttrs (name: cfg: !(isOneShot cfg)) services; + + oneShotServices = filterAttrs (name: cfg: isOneShot cfg) services; + + filterCommand = cmd: + let + filtered = substring 1 (stringLength cmd -2) cmd; + splitted = pkgs.lib.splitString (" " + substring 0 0 filtered) filtered; + in if eqStrings (substring 0 1 cmd) "@" then + traceVal (head splitted) + concatStringsSep " " (drop 2 splitted) + else cmd; + + configToCommand = name: cfg: '' + #!/bin/sh -e + ${if hasAttr "preStart" cfg then cfg.preStart else ""} + ${if hasAttr "ExecStart" cfg.serviceConfig then + filterCommand cfg.serviceConfig.ExecStart + else if hasAttr "script" cfg then + cfg.script + else + "" + } + ${if hasAttr "postStart" cfg then cfg.postStart else ""} + ''; + +in { + + options = { + systemd.services = mkOption { + default = {}; + type = types.attrsOf types.optionSet; + options = [ serviceOptions ]; + }; # TODO make more specific + }; + + config = { + userNix.startScripts."1-systemd-oneshot" = concatMapStrings (name: "${configToCommand name (getAttr name oneShotServices)}\n") (attrNames oneShotServices); + + supervisord.services = listToAttrs (map (name: + let + cfg = getAttr name services; + in + { + name = name; + value = { + command = pkgs.writeScript "${name}-run" (configToCommand name cfg); + environment = cfg.environment; + path = cfg.path; + stopsignal = if hasAttr "KillSignal" cfg.serviceConfig then + substring 3 (stringLength cfg.serviceConfig.KillSignal) cfg.serviceConfig.KillSignal + else "TERM"; + pidfile = if hasAttr "PIDFile" cfg.serviceConfig then cfg.serviceConfig.PIDFile else null; + }; + } + ) (attrNames (filterAttrs (n: v: v.enable) runServices))); + }; +} diff --git a/nix-services/user.nix b/nix-services/user.nix new file mode 100644 index 0000000..b7dacbf --- /dev/null +++ b/nix-services/user.nix @@ -0,0 +1,17 @@ +{ config, pkgs, ... }: +with pkgs.lib; +{ + options = { + userNix.startScripts = mkOption { + default = {}; + description = "Scripts (as text) to be run during build, executed alphabetically"; + }; + + userNix.startScript = mkOption {}; + + }; + + config = { + userNix.startScript = concatStrings (attrValues config.userNix.startScripts); + }; +}