From 1c963cea481fcf01e5386492149fa11eeeed7469 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 24 Apr 2023 17:04:50 +0200 Subject: [PATCH] nixos/gitea-actions-runner: init --- .../manual/release-notes/rl-2305.section.md | 2 + nixos/modules/module-list.nix | 1 + .../gitea-actions-runner.nix | 237 ++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 nixos/modules/services/continuous-integration/gitea-actions-runner.nix diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index a89c46152ca..66f09efbd56 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -49,6 +49,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [gemstash](https://github.com/rubygems/gemstash), a RubyGems.org cache and private gem server. Available as [services.gemstash](#opt-services.gemstash.enable). +- [gitea-actions-runner](https://gitea.com/gitea/act_runner), a CI runner for Gitea/Forgejo Actions. Available as [services.gitea-actions-runner](#opt-services.gitea-actions-runner.instances). + - [gmediarender](https://github.com/hzeller/gmrender-resurrect), a simple, headless UPnP/DLNA renderer. Available as [services.gmediarender](options.html#opt-services.gmediarender.enable). - [hyprland](https://github.com/hyprwm/hyprland), a dynamic tiling Wayland compositor that doesn't sacrifice on its looks. Available as [programs.hyprland](#opt-programs.hyprland.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f1c459f7557..07bcfd30894 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -370,6 +370,7 @@ ./services/continuous-integration/buildbot/master.nix ./services/continuous-integration/buildbot/worker.nix ./services/continuous-integration/buildkite-agents.nix + ./services/continuous-integration/gitea-actions-runner.nix ./services/continuous-integration/github-runner.nix ./services/continuous-integration/github-runners.nix ./services/continuous-integration/gitlab-runner.nix diff --git a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix new file mode 100644 index 00000000000..4b9046c98e8 --- /dev/null +++ b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix @@ -0,0 +1,237 @@ +{ config +, lib +, pkgs +, utils +, ... +}: + +let + inherit (lib) + any + attrValues + concatStringsSep + escapeShellArg + hasInfix + hasSuffix + optionalAttrs + optionals + literalExpression + mapAttrs' + mkEnableOption + mkOption + mkPackageOptionMD + mkIf + nameValuePair + types + ; + + inherit (utils) + escapeSystemdPath + ; + + cfg = config.services.gitea-actions-runner; + + # Check whether any runner instance label requires a container runtime + # Empty label strings result in the upstream defined defaultLabels, which require docker + # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98 + hasDockerScheme = instance: + instance.labels == [] || any (label: hasInfix ":docker:" label) instance.labels; + wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances); + + hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels; + + # provide shorthands for whether container runtimes are enabled + hasDocker = config.virtualisation.docker.enable; + hasPodman = config.virtualisation.podman.enable; + + tokenXorTokenFile = instance: + (instance.token == null && instance.tokenFile != null) || + (instance.token != null && instance.tokenFile == null); +in +{ + meta.maintainers = with lib.maintainers; [ + hexa + ]; + + options.services.gitea-actions-runner = with types; { + package = mkPackageOptionMD pkgs "gitea-actions-runner" { }; + + instances = mkOption { + default = {}; + description = lib.mdDoc '' + Gitea Actions Runner instances. + ''; + type = attrsOf (submodule { + options = { + enable = mkEnableOption (lib.mdDoc "Gitea Actions Runner instance"); + + name = mkOption { + type = str; + example = literalExpression "config.networking.hostName"; + description = lib.mdDoc '' + The name identifying the runner instance towards the Gitea/Forgejo instance. + ''; + }; + + url = mkOption { + type = str; + example = "https://forge.example.com"; + description = lib.mdDoc '' + Base URL of your Gitea/Forgejo instance. + ''; + }; + + token = mkOption { + type = nullOr str; + default = null; + description = lib.mdDoc '' + Plain token to register at the configured Gitea/Forgejo instance. + ''; + }; + + tokenFile = mkOption { + type = nullOr (either str path); + default = null; + description = lib.mdDoc '' + Path to an environment file, containing the `TOKEN` environment + variable, that holds a token to register at the configured + Gitea/Forgejo instance. + ''; + }; + + labels = mkOption { + type = listOf str; + example = literalExpression '' + [ + # provide a debian base with nodejs for actions + "debian-latest:docker://node:18-bullseye" + # fake the ubuntu name, because node provides no ubuntu builds + "ubuntu-latest:docker://node:18-bullseye" + # provide native execution on the host + #"native:host" + ] + ''; + description = lib.mdDoc '' + Labels used to map jobs to their runtime environment. Changing these + labels currently requires a new registration token. + + Many common actions require bash, git and nodejs, as well as a filesystem + that follows the filesystem hierarchy standard. + ''; + }; + + hostPackages = mkOption { + type = listOf package; + default = with pkgs; [ + bash + coreutils + curl + gawk + gitMinimal + gnused + nodejs + wget + ]; + defaultText = literalExpression '' + with pkgs; [ + bash + coreutils + curl + gawk + gitMinimal + gnused + nodejs + wget + ] + ''; + description = lib.mdDoc '' + List of packages, that are available to actions, when the runner is configured + with a host execution label. + ''; + }; + }; + }); + }; + }; + + config = mkIf (cfg.instances != {}) { + assertions = [ { + assertion = any tokenXorTokenFile (attrValues cfg.instances); + message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both."; + } { + assertion = wantsContainerRuntime -> hasDocker || hasPodman; + message = "Label configuration on gitea-actions-runner instance requires either docker or podman."; + } ]; + + systemd.services = let + mkRunnerService = name: instance: let + wantsContainerRuntime = hasDockerScheme instance; + wantsHost = hasHostScheme instance; + wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable; + wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable; + in + nameValuePair "gitea-runner-${escapeSystemdPath name}" { + inherit (instance) enable; + description = "Gitea Actions Runner"; + after = [ + "network-online.target" + ] ++ optionals (wantsDocker) [ + "docker.service" + ] ++ optionals (wantsPodman) [ + "podman.service" + ]; + wantedBy = [ + "multi-user.target" + ]; + environment = optionalAttrs (instance.token != null) { + TOKEN = "${instance.token}"; + } // optionalAttrs (wantsPodman) { + DOCKER_HOST = "unix:///run/podman/podman.sock"; + }; + path = with pkgs; [ + coreutils + ] ++ lib.optionals wantsHost instance.hostPackages; + serviceConfig = { + DynamicUser = true; + User = "gitea-runner"; + StateDirectory = "gitea-runner"; + WorkingDirectory = "-/var/lib/gitea-runner/${name}"; + ExecStartPre = pkgs.writeShellScript "gitea-register-runner-${name}" '' + export INSTANCE_DIR="$STATE_DIRECTORY/${name}" + mkdir -vp "$INSTANCE_DIR" + cd "$INSTANCE_DIR" + + # force reregistration on changed labels + export LABELS_FILE="$INSTANCE_DIR/.labels" + export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)" + export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)" + + if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then + # remove existing registration file, so that changing the labels forces a re-registation + rm -v "$INSTANCE_DIR/.runner" || true + + # perform the registration + ${cfg.package}/bin/act_runner register --no-interactive \ + --instance ${escapeShellArg instance.url} \ + --token "$TOKEN" \ + --name ${escapeShellArg instance.name} \ + --labels ${escapeShellArg (concatStringsSep "," instance.labels)} + + # and write back the configured labels + echo "$LABELS_WANTED" > "$LABELS_FILE" + fi + + ''; + ExecStart = "${cfg.package}/bin/act_runner daemon"; + SupplementaryGroups = optionals (wantsDocker) [ + "docker" + ] ++ optionals (wantsPodman) [ + "podman" + ]; + } // optionalAttrs (instance.tokenFile != null) { + EnvironmentFile = instance.tokenFile; + }; + }; + in mapAttrs' mkRunnerService cfg.instances; + }; +}