{ config, lib, pkgs, ... }: let cfg = config.services.radicle.ci.broker; settingsFormat = pkgs.formats.json { }; configFile = pkgs.runCommand "ci-broker.json" { } ( '' cp ${settingsFormat.generate "ci-broker.json" cfg.settings} $out '' + lib.optionalString cfg.checkConfig '' ${lib.getExe' cfg.package "cib"} --config $out config '' ); RAD_HOME = "/var/lib/radicle"; # Convenient wrapper to run `cibtool` in the namespaces of `radicle-ci-broker.service` cibtool-system = pkgs.writeShellScriptBin "cibtool-system" '' set -o allexport ${lib.toShellVars { inherit RAD_HOME; HOME = RAD_HOME; }} # Note that --env is not used to preserve host's envvars like $TERM exec ${lib.getExe' pkgs.util-linux "nsenter"} -a \ -t "$(${lib.getExe' config.systemd.package "systemctl"} show -P MainPID radicle-ci-broker.service)" \ -S "$(${lib.getExe' config.systemd.package "systemctl"} show -P UID radicle-ci-broker.service)" \ -G "$(${lib.getExe' config.systemd.package "systemctl"} show -P GID radicle-ci-broker.service)" \ ${lib.getExe' cfg.package "cibtool"} --db ${lib.escapeShellArg cfg.settings.db} "$@" ''; in { meta.maintainers = with lib.maintainers; [ defelo ]; options.services.radicle.ci.broker = { enable = lib.mkEnableOption "radicle-ci-broker"; package = lib.mkPackageOption pkgs "radicle-ci-broker" { }; stateDir = lib.mkOption { type = lib.types.path; description = "State directory of radicle-ci-broker."; default = "/var/lib/radicle-ci"; }; logDir = lib.mkOption { type = lib.types.path; description = "Log directory of radicle-ci-broker."; default = "/var/log/radicle-ci"; }; enableHardening = lib.mkEnableOption "systemd hardening" // { default = true; example = false; }; checkConfig = lib.mkEnableOption "checking the {file}`ci-broker.yaml` file resulting from [](#opt-services.radicle.ci.broker.settings)" // { default = true; example = false; }; settings = lib.mkOption { type = lib.types.submodule { freeformType = settingsFormat.type; options = { db = lib.mkOption { type = lib.types.path; description = "Database file path."; defaultText = lib.literalExpression ''"''${config.services.radicle.ci.broker.stateDir}/ci-broker.db"''; }; report_dir = lib.mkOption { type = lib.types.nullOr lib.types.path; description = "Directory where HTML and JSON report pages are written."; defaultText = lib.literalExpression ''"''${config.services.radicle.ci.broker.stateDir}/reports"''; }; adapters = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { freeformType = settingsFormat.type; options = { command = lib.mkOption { type = lib.types.str; description = "Adapter command to run."; }; env = lib.mkOption { type = lib.types.attrsOf settingsFormat.type; description = "Environment variables to add when running the adapter."; default = { }; }; }; } ); description = '' CI adapters. See also the options under [services.radicle.ci.adapters](#opt-services.radicle.ci.adapters.native.instances). ''; default = { }; }; triggers = lib.mkOption { type = lib.types.listOf ( lib.types.submodule { freeformType = settingsFormat.type; options = { adapter = lib.mkOption { type = lib.types.str; description = "Adapter name."; }; filters = lib.mkOption { type = lib.types.listOf settingsFormat.type; description = "Trigger filter."; }; }; } ); description = "CI triggers."; default = [ ]; }; }; }; description = '' Configuration of radicle-ci-broker. See for more information. ''; default = { }; example = lib.literalExpression '' { adapters.native = { command = lib.getExe pkgs.radicle-native-ci; config = { }; config_env = "RADICLE_NATIVE_CI"; env.PATH = lib.makeBinPath (with pkgs; [ bash coreutils ]); }; triggers = [ { adapter = "native"; filters = [ { And = [ { HasFile = ".radicle/native.yaml"; } { Node = "z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV"; } { Or = [ "DefaultBranch" "PatchCreated" "PatchUpdated" ]; } ]; } ]; } ]; } ''; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = config.services.radicle.enable; message = "radicle-ci-broker requires a local radicle node to be running."; } ]; services.radicle.ci.broker.settings = { db = lib.mkDefault "${cfg.stateDir}/ci-broker.db"; report_dir = lib.mkDefault "${cfg.stateDir}/reports"; }; systemd.services.radicle-ci-broker = { wantedBy = [ "multi-user.target" ]; bindsTo = [ "radicle-node.service" ]; after = [ "radicle-node.service" ]; environment = { inherit RAD_HOME; }; serviceConfig = lib.mkMerge [ { User = config.users.users.radicle.name; Group = config.users.groups.radicle.name; Restart = "always"; StateDirectory = lib.mkIf (cfg.stateDir == "/var/lib/radicle-ci") "radicle-ci"; LogsDirectory = lib.mkIf (cfg.logDir == "/var/log/radicle-ci") "radicle-ci"; RuntimeDirectory = "radicle-ci-broker"; WorkingDirectory = "/run/radicle-ci-broker"; BindReadOnlyPaths = config.systemd.services.radicle-node.serviceConfig.BindReadOnlyPaths; ReadWritePaths = [ RAD_HOME ]; ExecStart = "${lib.getExe' cfg.package "cib"} --config ${configFile} process-events"; } (lib.mkIf cfg.enableHardening { AmbientCapabilities = ""; CapabilityBoundingSet = [ "" ]; DevicePolicy = "closed"; LockPersonality = true; MemoryDenyWriteExecute = true; NoNewPrivileges = true; PrivateDevices = true; PrivateTmp = true; PrivateUsers = true; ProcSubset = "pid"; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProtectSystem = "strict"; RemoveIPC = true; RestrictAddressFamilies = [ "AF_INET AF_INET6 AF_UNIX" ]; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; UMask = "0066"; }) ]; }; systemd.tmpfiles.settings.radicle-ci-broker.${cfg.settings.report_dir}.d = { user = config.users.users.radicle.name; group = config.users.groups.radicle.name; }; environment.systemPackages = [ cibtool-system ]; }; }