1
0
Fork 0
mirror of https://github.com/LnL7/nix-darwin.git synced 2025-03-05 16:27:03 +00:00
nix-darwin/modules/services/github-runner/service.nix
Vincent Haupert 06f5dab065 github-runners: adapt to NixOS module
While #859 added basic support for configuring GitHub runners through
nix-darwin, it did not yet support all of the options the NixOS module
offers.

I am aware that this is a rather big overhaul. I think, however, that
it's worth it:

- Copies the `options.nix` from the [NixOS module] with only minor
  adaptations. This should help to keep track of any changes to it.
- Respect the `workDir` config option. So far, the implementation didn't
  even read the value of the option.
- Allow configuring a custom user and group.
  If both are `null`, nix-darwin manages the `_github-runner` user
  shared among all instances. Take care of creating your own users if
  that's not what you want.
- Also creates the necessary directories for state, logs and the working
  directory (unless `workDir != null`). It uses the following locations:
    * state: `/var/lib/github-runners/${name}`
    * logs: `/var/log/github-runners/${name}`
    * work: The value of `workDir` or `/var/run/github-runners/${name}`
            if (`workDir == null`).
  We have to create the logs directory before starting the service since
  launchd expects that the `Standard{Error,Out}Path` exist. We do this
  by prepending to [`system.activationScripts.launchd.text`].
  All directories belong to the configured `user` and `group`.
- Warn if a `tokenFile` points to the Nix store.

[NixOS module]: https://github.com/NixOS/nixpkgs/blob/3c30c56/nixos/modules/services/continuous-integration/github-runner/options.nix
[`system.activationScripts.launchd.text`]: https://github.com/LnL7/nix-darwin/blob/bbde06b/modules/system/launchd.nix#L99-L123
2024-02-28 09:40:25 +01:00

181 lines
7 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
mkSvcName = name: "github-runner-${name}";
mkStateDir = cfg: "/var/lib/github-runners/${cfg.name}";
mkLogDir = cfg: "/var/log/github-runners/${cfg.name}";
mkWorkDir = cfg: if (cfg.workDir != null) then cfg.workDir else "/var/run/github-runners/${cfg.name}";
in
{
config.assertions = flatten (
flip mapAttrsToList config.services.github-runners (name: cfg: map (mkIf cfg.enable) [
{
assertion = (cfg.user == null && cfg.group == null) || (cfg.user != null);
message = "`services.github-runners.${name}`: Either set `user` and `group` to `null` to have nix-darwin manage them or set at least `user` explicitly";
}
{
assertion = !cfg.noDefaultLabels || (cfg.extraLabels != [ ]);
message = "`services.github-runners.${name}`: The `extraLabels` option is mandatory if `noDefaultLabels` is set";
}
])
);
config.warnings = flatten (
flip mapAttrsToList config.services.github-runners (name: cfg: map (mkIf cfg.enable) [
(
mkIf (hasPrefix builtins.storeDir cfg.tokenFile)
"`services.github-runners.${name}`: `tokenFile` contains a secret but points to the world-readable Nix store."
)
])
);
# Create the necessary directories and make the service user/group their owner
# This has to happen *after* nix-darwin user creation and *before* any launchd service gets started.
config.system.activationScripts = mkMerge (flip mapAttrsToList config.services.github-runners (name: cfg:
let
user = config.launchd.daemons.${mkSvcName name}.serviceConfig.UserName;
group =
if config.launchd.daemons.${mkSvcName name}.serviceConfig.GroupName != null
then config.launchd.daemons.${mkSvcName name}.serviceConfig.GroupName
else "";
in
{
launchd = mkIf cfg.enable {
text = mkBefore (''
echo >&2 "setting up GitHub Runner '${cfg.name}'..."
${pkgs.coreutils}/bin/mkdir -p -m 0750 ${escapeShellArg (mkStateDir cfg)}
${pkgs.coreutils}/bin/chown ${user}:${group} ${escapeShellArg (mkStateDir cfg)}
${pkgs.coreutils}/bin/mkdir -p -m 0750 ${escapeShellArg (mkLogDir cfg)}
${pkgs.coreutils}/bin/chown ${user}:${group} ${escapeShellArg (mkLogDir cfg)}
'' + optionalString (cfg.workDir == null) ''
${pkgs.coreutils}/bin/mkdir -p -m 0750 ${escapeShellArg (mkWorkDir cfg)}
${pkgs.coreutils}/bin/chown ${user}:${group} ${escapeShellArg (mkWorkDir cfg)}
'');
};
}));
config.launchd.daemons = flip mapAttrs' config.services.github-runners (name: cfg:
let
package = cfg.package.override (old: optionalAttrs (hasAttr "nodeRuntimes" old) { inherit (cfg) nodeRuntimes; });
stateDir = mkStateDir cfg;
logDir = mkLogDir cfg;
workDir = mkWorkDir cfg;
in
nameValuePair
(mkSvcName name)
(mkIf cfg.enable {
environment = {
HOME = stateDir;
RUNNER_ROOT = stateDir;
} // cfg.extraEnvironment;
# Minimal package set for `actions/checkout`
path = (with pkgs; [
bash
coreutils
git
gnutar
gzip
]) ++ [
config.nix.package
] ++ cfg.extraPackages;
script =
let
configure = pkgs.writeShellApplication {
name = "configure-github-runner-${name}";
text = ''
export RUNNER_ROOT
args=(
--unattended
--disableupdate
--work ${escapeShellArg workDir}
--url ${escapeShellArg cfg.url}
--labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
${optionalString (cfg.name != null ) "--name ${escapeShellArg cfg.name}"}
${optionalString cfg.replace "--replace"}
${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
${optionalString cfg.ephemeral "--ephemeral"}
${optionalString cfg.noDefaultLabels "--no-default-labels"}
)
# If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
# if it is not a PAT, we assume it contains a registration token and use the --token option
token=$(<"${cfg.tokenFile}")
if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
args+=(--pat "$token")
else
args+=(--token "$token")
fi
${package}/bin/config.sh "''${args[@]}"
'';
};
in
''
echo "Configuring GitHub Actions Runner"
# Always clean the working directory
${pkgs.findutils}/bin/find ${escapeShellArg workDir} -mindepth 1 -delete
# Clean the $RUNNER_ROOT if we are in ephemeral mode
if ${boolToString cfg.ephemeral}; then
echo "Cleaning $RUNNER_ROOT"
${pkgs.findutils}/bin/find "$RUNNER_ROOT" -mindepth 1 -delete
fi
# If the `.runner` file does not exist, we assume the runner is not configured
if [[ ! -f "$RUNNER_ROOT/.runner" ]]; then
${getExe configure}
fi
# Start the service
${package}/bin/Runner.Listener run --startuptype service
'';
serviceConfig = mkMerge [
{
GroupName = cfg.group;
KeepAlive = {
Crashed = false;
} // mkIf cfg.ephemeral {
SuccessfulExit = true;
};
ProcessType = "Interactive";
RunAtLoad = true;
StandardErrorPath = "${logDir}/launchd-stderr.log";
StandardOutPath = "${logDir}/launchd-stdout.log";
ThrottleInterval = 30;
UserName = if (cfg.user != null) then cfg.user else "_github-runner";
WatchPaths = [
"/etc/resolv.conf"
"/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"
cfg.tokenFile
];
WorkingDirectory = stateDir;
}
cfg.serviceOverrides
];
}));
# If any GitHub runner configuration has set both `user` and `group` set to `null`,
# manage the user and group `_github-runner` through nix-darwin.
config.users = mkIf (any (cfg: cfg.enable && cfg.user == null && cfg.group == null) (attrValues config.services.github-runners)) {
users."_github-runner" = {
createHome = false;
description = "GitHub Runner service user";
gid = config.users.groups."_github-runner".gid;
home = "/var/lib/github-runners";
shell = "/bin/bash";
uid = mkDefault 533;
};
knownUsers = [ "_github-runner" ];
groups."_github-runner" = {
gid = mkDefault 533;
description = "GitHub Runner service user group";
};
knownGroups = [ "_github-runner" ];
};
}