diff options
| author | Mike Vink <mike1994vink@gmail.com> | 2024-04-05 10:59:38 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-04-05 10:59:38 +0200 |
| commit | 6bd39d420578aacf7c0bab7de3e7027b952115ae (patch) | |
| tree | e84978f345a91441855c396331aee4c68ee77c03 /modules/services/github-runner | |
| parent | d7f1482c47503d6508657ebcbaeef281b78c6c56 (diff) | |
| parent | 36524adc31566655f2f4d55ad6b875fb5c1a4083 (diff) | |
Merge branch 'LnL7:master' into master
Diffstat (limited to 'modules/services/github-runner')
| -rw-r--r-- | modules/services/github-runner/default.nix | 6 | ||||
| -rw-r--r-- | modules/services/github-runner/options.nix | 269 | ||||
| -rw-r--r-- | modules/services/github-runner/service.nix | 181 |
3 files changed, 456 insertions, 0 deletions
diff --git a/modules/services/github-runner/default.nix b/modules/services/github-runner/default.nix new file mode 100644 index 0000000..e860eca --- /dev/null +++ b/modules/services/github-runner/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./options.nix + ./service.nix + ]; +} diff --git a/modules/services/github-runner/options.nix b/modules/services/github-runner/options.nix new file mode 100644 index 0000000..300ca32 --- /dev/null +++ b/modules/services/github-runner/options.nix @@ -0,0 +1,269 @@ +{ lib +, pkgs +, ... +}: + +with lib; +{ + options.services.github-runners = mkOption { + description = mdDoc '' + Multiple GitHub Runners. + + If `user` and `group` are set to `null`, the module will configure nix-darwin to + manage the `_github-runner` user and group. Note that multiple runner + configurations share the same user/group, which means they can access + resources from other runners. Make each runner use its own user and group if + this is not what you want. In this case, you will have to do the user and + group creation yourself. If only `user` is set, while `group` is set to + `null`, the service will infer the primary group of the `user`. + + For each GitHub runner, the system activation script creates the following + directories: + + * `/var/lib/github-runners/<name>`: + State directory to store the runner registration credentials + * `/var/log/github-runners/<name>`: + The launchd service writes the stdout and stderr streams to this + directory. + * `/var/run/github-runners/<name>`: + Working directory for workflow files. The runner only uses this + directory if `workDir` is `null` (see the `workDir` option for details). + ''; + example = { + runner1 = { + enable = true; + url = "https://github.com/owner/repo"; + name = "runner1"; + tokenFile = "/secrets/token1"; + }; + + runner2 = { + enable = true; + url = "https://github.com/owner/repo"; + name = "runner2"; + tokenFile = "/secrets/token2"; + }; + }; + default = { }; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + enable = mkOption { + default = false; + example = true; + description = mdDoc '' + Whether to enable GitHub Actions runner. + + Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here: + [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners). + ''; + type = types.bool; + }; + + url = mkOption { + type = types.str; + description = mdDoc '' + Repository to add the runner to. + + Changing this option triggers a new runner registration. + + IMPORTANT: If your token is org-wide (not per repository), you need to + provide a github org link, not a single repository, so do it like this + `https://github.com/nixos`, not like this + `https://github.com/nixos/nixpkgs`. + Otherwise, you are going to get a `404 NotFound` + from `POST https://api.github.com/actions/runner-registration` + in the configure script. + ''; + example = "https://github.com/nixos/nixpkgs"; + }; + + tokenFile = mkOption { + type = types.path; + description = mdDoc '' + The full path to a file which contains either + + * a fine-grained personal access token (PAT), + * a classic PAT + * or a runner registration token + + Changing this option or the `tokenFile`’s content triggers a new runner registration. + + We suggest using the fine-grained PATs. A runner registration token is valid + only for 1 hour after creation, so the next time the runner configuration changes + this will give you hard-to-debug HTTP 404 errors in the configure step. + + The file should contain exactly one line with the token without any newline. + (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.) + + If the file contains a PAT, the service creates a new registration token + on startup as needed. + If a registration token is given, it can be used to re-register a runner of the same + name but is time-limited as noted above. + + For fine-grained PATs: + + Give it "Read and Write access to organization/repository self hosted runners", + depending on whether it is organization wide or per-repository. You might have to + experiment a little, fine-grained PATs are a `beta` Github feature and still subject + to change; nonetheless they are the best option at the moment. + + For classic PATs: + + Make sure the PAT has a scope of `admin:org` for organization-wide registrations + or a scope of `repo` for a single repository. + + For runner registration tokens: + + Nothing special needs to be done, but updating will break after one hour, + so these are not recommended. + ''; + example = "/run/secrets/github-runner/nixos.token"; + }; + + name = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Name of the runner to configure. If null, defaults to the hostname. + + Changing this option triggers a new runner registration. + ''; + example = "nixos"; + default = name; + }; + + runnerGroup = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Name of the runner group to add this runner to (defaults to the default runner group). + + Changing this option triggers a new runner registration. + ''; + default = null; + }; + + extraLabels = mkOption { + type = types.listOf types.str; + description = mdDoc '' + Extra labels in addition to the default (unless disabled through the `noDefaultLabels` option). + + Changing this option triggers a new runner registration. + ''; + example = literalExpression ''[ "nixos" ]''; + default = [ ]; + }; + + noDefaultLabels = mkOption { + type = types.bool; + description = mdDoc '' + Disables adding the default labels. Also see the `extraLabels` option. + + Changing this option triggers a new runner registration. + ''; + default = false; + }; + + replace = mkOption { + type = types.bool; + description = mdDoc '' + Replace any existing runner with the same name. + + Without this flag, registering a new runner with the same name fails. + ''; + default = false; + }; + + extraPackages = mkOption { + type = types.listOf types.package; + description = mdDoc '' + Extra packages to add to `PATH` of the service to make them available to workflows. + ''; + default = [ ]; + }; + + extraEnvironment = mkOption { + type = types.attrs; + description = mdDoc '' + Extra environment variables to set for the runner, as an attrset. + ''; + example = { + GIT_CONFIG = "/path/to/git/config"; + }; + default = { }; + }; + + serviceOverrides = mkOption { + type = types.attrs; + description = mdDoc '' + Modify the service. Can be used to, e.g., adjust the sandboxing options. + ''; + default = { }; + }; + + package = mkPackageOptionMD pkgs "github-runner" { }; + + ephemeral = mkOption { + type = types.bool; + description = mdDoc '' + If enabled, causes the following behavior: + + - Passes the `--ephemeral` flag to the runner configuration script + - De-registers and stops the runner with GitHub after it has processed one job + - Restarts the service after its successful exit + - On start, wipes the state directory and configures a new runner + + You should only enable this option if `tokenFile` points to a file which contains a + personal access token (PAT). If you're using the option with a registration token, restarting the + service will fail as soon as the registration token expired. + + Changing this option triggers a new runner registration. + ''; + default = false; + }; + + user = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + User under which to run the service. + + If this option and the `group` option is set to `null`, nix-darwin creates + the `github-runner` user and group. + ''; + defaultText = literalExpression "username"; + default = null; + }; + + group = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Group under which to run the service. + + If this option and the `user` option is set to `null`, nix-darwin creates + the `github-runner` user and group. + ''; + defaultText = literalExpression "groupname"; + default = null; + }; + + workDir = mkOption { + type = with types; nullOr str; + description = mdDoc '' + Working directory, available as `$GITHUB_WORKSPACE` during workflow runs + and used as a default for [repository checkouts](https://github.com/actions/checkout). + The service cleans this directory on every service start. + + Changing this option triggers a new runner registration. + ''; + default = null; + }; + + nodeRuntimes = mkOption { + type = with types; nonEmptyListOf (enum [ "node20" ]); + default = [ "node20" ]; + description = mdDoc '' + List of Node.js runtimes the runner should support. + ''; + }; + }; + })); + }; +} diff --git a/modules/services/github-runner/service.nix b/modules/services/github-runner/service.nix new file mode 100644 index 0000000..53f2cdd --- /dev/null +++ b/modules/services/github-runner/service.nix @@ -0,0 +1,181 @@ +{ 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" ]; + }; +} |
