From 21b92addaf58b3b8f9f3c21b482f97f96d58895a Mon Sep 17 00:00:00 2001 From: Vincent Haupert Date: Sun, 20 Nov 2022 14:00:00 +0100 Subject: [PATCH] github-runners: init module Adds a new module which allows to configure multiple GitHub self-hosted runners on Darwin. The module is heavily inspired by the nixpkgs NixOS module. Its implementation differs in some ways: - There's currently no way to configure the user/group which runs the runner. All configured runners share the same user and group. - No automatic cleanup. - No advanced sandboxing apart from user/group isolation --- modules/module-list.nix | 1 + modules/services/github-runner/config.nix | 79 +++++++++ modules/services/github-runner/default.nix | 37 +++++ modules/services/github-runner/options.nix | 181 +++++++++++++++++++++ release.nix | 1 + tests/services-github-runners.nix | 23 +++ 6 files changed, 322 insertions(+) create mode 100644 modules/services/github-runner/config.nix create mode 100644 modules/services/github-runner/default.nix create mode 100644 modules/services/github-runner/options.nix create mode 100644 tests/services-github-runners.nix diff --git a/modules/module-list.nix b/modules/module-list.nix index 93c63bf8..f56e1560 100644 --- a/modules/module-list.nix +++ b/modules/module-list.nix @@ -54,6 +54,7 @@ ./services/dnsmasq.nix ./services/emacs.nix ./services/eternal-terminal.nix + ./services/github-runner ./services/gitlab-runner.nix ./services/hercules-ci-agent ./services/ipfs.nix diff --git a/modules/services/github-runner/config.nix b/modules/services/github-runner/config.nix new file mode 100644 index 00000000..42c9eab7 --- /dev/null +++ b/modules/services/github-runner/config.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ... }: +let + mkSvcName = name: "github-runner-${name}"; + mkRootDir = name: "${config.users.users.github-runner.home}/.github-runner/${name}"; + mkWorkDir = name: "${mkRootDir name}/_work"; +in +with lib; +{ + launchd.daemons = flip mapAttrs' config.services.github-runners (name: cfg: + nameValuePair + (mkSvcName name) + (mkIf cfg.enable { + environment = { + RUNNER_ROOT = mkRootDir name; + } // cfg.extraEnvironment; + + # Minimal package set for `actions/checkout` + path = (with pkgs; [ + bash + coreutils + git + gnutar + gzip + ]) ++ [ + config.nix.package + ] ++ cfg.extraPackages; + + script = '' + echo "Configuring GitHub Actions Runner" + mkdir -p ${escapeShellArg (mkRootDir name)} + cd ${escapeShellArg (mkRootDir name)} + + args=( + --unattended + --disableupdate + --work ${escapeShellArg (mkWorkDir name)} + --url ${escapeShellArg cfg.url} + --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)} + --name ${escapeShellArg cfg.name} + ${optionalString cfg.replace "--replace"} + ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"} + ${optionalString cfg.ephemeral "--ephemeral"} + ) + # 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 + ${cfg.package}/bin/config.sh "''${args[@]}" + + # Start the service + ${cfg.package}/bin/Runner.Listener run --startuptype service + ''; + + serviceConfig = mkMerge [ + { + KeepAlive = { + Crashed = false; + } // mkIf cfg.ephemeral { + SuccessfulExit = true; + }; + GroupName = "github-runner"; + ProcessType = "Interactive"; + RunAtLoad = true; + ThrottleInterval = 30; + UserName = "github-runner"; + WatchPaths = [ + "/etc/resolv.conf" + "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist" + ]; + WorkingDirectory = config.users.users.github-runner.home; + } + cfg.serviceOverrides + ]; + })); +} diff --git a/modules/services/github-runner/default.nix b/modules/services/github-runner/default.nix new file mode 100644 index 00000000..79379802 --- /dev/null +++ b/modules/services/github-runner/default.nix @@ -0,0 +1,37 @@ +{ config, lib, ... }: +let + anyEnabled = lib.any (cfg: cfg.enable) (lib.attrValues config.services.github-runners); +in +{ + imports = [ + ./options.nix + ./config.nix + ]; + + config.assertions = lib.mkIf anyEnabled [ + { + assertion = lib.elem "github-runner" config.users.knownGroups; + message = "set `users.knownGroups` to enable `github-runner` group"; + } + { + assertion = lib.elem "github-runner" config.users.knownUsers; + message = "set `users.knownUsers` to enable `github-runner` user"; + } + ]; + + config.users = lib.mkIf anyEnabled { + users."github-runner" = { + createHome = true; + uid = lib.mkDefault 533; + gid = lib.mkDefault config.users.groups.github-runner.gid; + home = lib.mkDefault "/var/lib/github-runners"; + shell = "/bin/bash"; + description = "GitHub Runner service user"; + }; + + groups."github-runner" = { + gid = lib.mkDefault 533; + description = "GitHub Runner service user group"; + }; + }; +} diff --git a/modules/services/github-runner/options.nix b/modules/services/github-runner/options.nix new file mode 100644 index 00000000..5e421df9 --- /dev/null +++ b/modules/services/github-runner/options.nix @@ -0,0 +1,181 @@ +{ lib +, pkgs +, ... +}: + +with lib; +{ + options.services.github-runners = mkOption { + default = { }; + description = mdDoc '' + Configure multiple GitHub Runners. + ''; + example = literalExpression '' + { + m1-runner = { + enable = true; + ephemeral = true; + replace = true; + tokenFile = "/secrets/github-org-pat.token"; + url = "https://github.com/nixos"; + }; + + m2-runner = { + enable = true; + extraLabels = [ "nixpkgs" ]; + replace = true; + tokenFile = "/secrets/github-repo-pat.token"; + url = "https://github.com/nixos/nixpkgs"; + }; + } + ''; + type = with types; attrsOf (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 = lib.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 runner registration token or a + (fine-grained) personal access token (PAT). + The file should contain exactly one line with the token without any newline. + If a registration token is given, it can be used to re-register a runner of the same + name but is time-limited. If the file contains a PAT, the service creates a new + registration token on startup as needed. Make sure the PAT has a scope of + `admin:org` for organization-wide registrations or a scope of + `repo` for a single repository. Fine-grained PATs need read and write permission + to the "Adminstration" resources. + + Changing this option or the file's content triggers a new runner registration. + ''; + example = "/run/secrets/github-runner/nixos.token"; + }; + + name = mkOption { + type = types.str; + description = mdDoc '' + Name of the runner to configure. Defaults to the attribute name. + + 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 (`["self-hosted", "Linux", "X64"]`). + + Changing this option triggers a new runner registration. + ''; + example = literalExpression ''[ "nixos" ]''; + default = [ ]; + }; + + 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 '' + Overrides for the systemd service. Can be used to adjust the sandboxing options. + ''; + example = { + ProtectHome = false; + }; + default = { }; + }; + + package = mkOption { + type = types.package; + description = mdDoc '' + Which github-runner derivation to use. + ''; + default = pkgs.github-runner; + defaultText = literalExpression "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 + - The runner wipes some state before it exists + + 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. + ''; + default = false; + }; + }; + })); + }; +} diff --git a/release.nix b/release.nix index ffdfc387..a0436bd7 100644 --- a/release.nix +++ b/release.nix @@ -119,6 +119,7 @@ let tests.services-activate-system = makeTest ./tests/services-activate-system.nix; tests.services-activate-system-changed-label-prefix = makeTest ./tests/services-activate-system-changed-label-prefix.nix; tests.services-buildkite-agent = makeTest ./tests/services-buildkite-agent.nix; + tests.services-github-runners = makeTest ./tests/services-github-runners.nix; tests.services-lorri = makeTest ./tests/services-lorri.nix; tests.services-nix-daemon = makeTest ./tests/services-nix-daemon.nix; tests.sockets-nix-daemon = makeTest ./tests/sockets-nix-daemon.nix; diff --git a/tests/services-github-runners.nix b/tests/services-github-runners.nix new file mode 100644 index 00000000..fc571027 --- /dev/null +++ b/tests/services-github-runners.nix @@ -0,0 +1,23 @@ +{ config, pkgs, ... }: +{ + users = { + knownUsers = [ "github-runner" ]; + knownGroups = [ "github-runner" ]; + }; + + services.github-runners."a-runner" = { + enable = true; + url = "https://github.com/nixos/nixpkgs"; + tokenFile = pkgs.writeText "fake-token" "not-a-token"; + package = pkgs.runCommand "github-runner-0.0.0" { } "touch $out"; + }; + + test = '' + echo >&2 "checking github-runner service in /Library/LaunchDaemons" + grep "org.nixos.github-runner-a-runner" ${config.out}/Library/LaunchDaemons/org.nixos.github-runner-a-runner.plist + grep "github-runner" ${config.out}/Library/LaunchDaemons/org.nixos.github-runner-a-runner.plist + + echo >&2 "checking for user in /activate" + grep "GitHub Runner service user" ${config.out}/activate + ''; +}