Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2605.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@

- [perses](https://perses.dev/), the open dashboard tool for Prometheus and other data sources. Available as [services.perses](#opt-services.perses.enable).

- [Forgejo Runner](https://code.forgejo.org/forgejo/runner), a dedicated Forgejo Actions runner module for multi-instance deployments is now available as [services.forgejo.runner.instances](#opt-services.forgejo.runner.instances).

- [Drasl](https://github.com/unmojang/drasl), an alternative authentication server for Minecraft. Available as [services.drasl](#opt-services.drasl.enable).

- [tabbyAPI](https://github.com/theroyallab/tabbyAPI), the official OpenAI compatible API server for Exllama. Available as [services.tabbyapi](#opt-services.tabbyapi.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@
./services/continuous-integration/buildbot/master.nix
./services/continuous-integration/buildbot/worker.nix
./services/continuous-integration/buildkite-agents.nix
./services/continuous-integration/forgejo-runner.nix
./services/continuous-integration/gitea-actions-runner.nix
./services/continuous-integration/github-runners.nix
./services/continuous-integration/gitlab-runner/runner.nix
Expand Down
338 changes: 338 additions & 0 deletions nixos/modules/services/continuous-integration/forgejo-runner.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
{
config,
lib,
pkgs,
utils,
...
}:

let
inherit (lib)
any
concatStringsSep
escapeShellArg
hasInfix
hasSuffix
literalExpression
mapAttrs'
mkEnableOption
mkIf
mkOption
mkPackageOption
nameValuePair
optionalAttrs
optionals
teams
types
;

inherit (utils)
escapeSystemdPath
;

cfg = config.services.forgejo.runner;

settingsFormat = pkgs.formats.yaml { };

instanceLabels =
instance:
instance.labels
++ lib.concatMap (connection: connection.labels or [ ]) (
lib.attrValues (instance.settings.server.connections or { })
);

hasDockerScheme = labels: any (label: hasInfix ":docker:" label) labels;
hasHostScheme = labels: any (label: hasSuffix ":host" label) labels;

wantsContainerRuntime = any (instance: hasDockerScheme (instanceLabels instance)) (
lib.attrValues cfg.instances
);

hasDocker = config.virtualisation.docker.enable;
hasPodman = config.virtualisation.podman.enable;
in
{
meta.maintainers = teams.forgejo.members;

options.services.forgejo.runner = with types; {
package = mkPackageOption pkgs "forgejo-runner" { };

instances = mkOption {
default = { };
description = ''
Forgejo Actions Runner instances.
'';
type = attrsOf (
submodule (
{ name, ... }:
{
options = {
enable = mkEnableOption "Forgejo Actions Runner instance";

name = mkOption {
type = str;
example = literalExpression "config.networking.hostName";
description = ''
The name identifying the runner instance towards the Forgejo instance.
'';
default = name;
};

url = mkOption {
type = nullOr str;
default = null;
example = "https://forge.example.com";
description = ''
Base URL of your Forgejo instance.

Can also be specified in `settings.servier.connections`
'';
};

registrationTokenFile = mkOption {
type = nullOr (either str path);
default = null;
description = ''
Path to a file containing only the token that will be used to register
on start with the the configured Forgejo instance.

**Deprecated** Replaced by `settings.server.connections`

<https://forgejo.org/docs/latest/admin/actions/registration/>
'';
};

credentials = lib.mkOption {
type = attrsOf lib.types.path;
default = { };
example = {
WORKER1_TOKEN = "/run/keys/worker1";
};
Comment on lines +105 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we use these credentials properly?

I've tried this example for the first time, my full config:

{
  services.forgejo.runner.instances.test = {
    enable = true;
    url = "https://git.tchfoo.com";
    labels = [ "native:host" ];
    settings.server.connections.gepbird = {
      uuid = "634c3257-0e5a-4d35-b5bb-8620f1b70212";
      token_url = "file:$WORKER1_TOKEN";
    };
    credentials.WORKER1_TOKEN = "/run/keys/worker1";
  };
}

System log:

forgejo-runner[1849855]: Error: invalid configuration: invalid `server` settings: connection "gepbird" is invalid: invalid `token_url`: cannot read secret "file:$WORKER1_TOKEN": open : no such file or directory

According to this comment in the example config, only $CREDENTIALS_DIRECTORY will be resolved. After modifying this part of the configuration:

{
  services.forgejo.runner.instances.test = {
    settings.server.connections.gepbird.token_url = "file:$CREDENTIALS_DIRECTORY/worker1";
    credentials.CREDENTIALS_DIRECTORY = "/run/keys";
  };
}

It still fails, but the variable is resolved:

forgejo-runner[1867313]: Error: invalid configuration: invalid `server` settings: connection "gepbird" is invalid: invalid `token_url`: cannot read secret "file:/run/credentials/forgejo-runner@test.service/worker1": open /run/credentials/forgejo-runner@test.service/worker1: no such file or directory

This configuration works (if the permissions are broad enough):

{
  services.forgejo.runner.instances.test = {
    settings.server.connections.gepbird.token_url = "file:/run/keys/worker1";
    # no credentials option set
  };
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CREDENTIALS_DIRECTORY is a built in env var for LoadCredential. You should probably choose a different attr name.

{
  services.forgejo.runner.instances.test = {
    settings.server.connections.gepbird.token_url = "file:$CREDENTIALS_DIRECTORY/my_token"; # my_token needs to match the credential *name*
    credentials.my_token = "/run/keys/my_token";
  };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Works well, I think an example for the settings option would help many people :)

description = ''
Environment variables with absolute paths to credentials files to load
on runner startup.
'';
};

labels = mkOption {
type = listOf str;
default = [ ];
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 = ''
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.
'';
};

settings = mkOption {
description = ''
Configuration for `forgejo-runner daemon`.
See <https://code.forgejo.org/forgejo/runner/src/branch/main/internal/pkg/config/config.example.yaml> for an example configuration.
'';

type = types.submodule {
freeformType = settingsFormat.type;
};

default = { };
};

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 = ''
List of packages that are available to actions, when the runner is configured
with a host execution label.
'';
};
};
}
)
);
};
};

config = mkIf (cfg.instances != { }) {
assertions = [
{
assertion = wantsContainerRuntime -> hasDocker || hasPodman;
message = "Label configuration on forgejo.runner instance requires either docker or podman.";
}
]
++ (lib.foldlAttrs (
acc: _: instance:
acc
++ [
{
assertion = instance.registrationTokenFile != null -> instance.url != null;
message = "forgejo.runner.instances.${instance.name}.registrationTokenFile requires `url` to be set.";
}
]
) [ ] cfg.instances);

systemd.services =
let
mkRunnerInstance =
_: instance:
let
allLabels = instanceLabels instance;
escapedName = escapeSystemdPath instance.name;
wantsContainer = hasDockerScheme allLabels;
wantsHost = hasHostScheme allLabels;
wantsDocker = wantsContainer && hasDocker;
wantsPodman = wantsContainer && hasPodman;
configFile = settingsFormat.generate "forgejo-runner-${escapedName}.yaml" instance.settings;
in
nameValuePair "forgejo-runner@${escapedName}" {
overrideStrategy = "asDropin";
inherit (instance) enable;
wants = [
"network-online.target"
]
++ optionals wantsDocker [ "docker.service" ]
++ optionals wantsPodman [ "podman.service" ];
after = [
"network-online.target"
]
++ optionals wantsDocker [ "docker.service" ]
++ optionals wantsPodman [ "podman.service" ];
wantedBy = [ "multi-user.target" ];

environment = optionalAttrs wantsPodman {
DOCKER_HOST = "unix:///run/podman/podman.sock";
};

path = [ pkgs.coreutils ] ++ lib.optionals wantsHost instance.hostPackages;

serviceConfig = {
Comment thread
adamcstephens marked this conversation as resolved.
MemoryDenyWriteExecute = !wantsHost;

LoadCredential =
lib.optionals (instance.registrationTokenFile != null) [
"REGISTRATION_TOKEN:${instance.registrationTokenFile}"
]
++ lib.mapAttrsToList (name: value: "${name}:${value}") instance.credentials;

SupplementaryGroups = optionals wantsDocker [ "docker" ] ++ optionals wantsPodman [ "podman" ];
ExecPaths = lib.optionals wantsHost [ "/var/lib/forgejo-runner/${escapedName}" ];

ExecStartPre = lib.optionals (instance.registrationTokenFile != null) [
(lib.getExe (
pkgs.writeShellApplication {
name = "forgejo-register-runner-${escapedName}";
text = ''
INSTANCE_DIR="$STATE_DIRECTORY"
mkdir -vp "$INSTANCE_DIR"
cd "$INSTANCE_DIR"

LABELS_FILE="$INSTANCE_DIR/.labels.sha256"
LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)"
LABELS_WANTED_HASH="$(printf '%s' "$LABELS_WANTED" | sha256sum | cut -d' ' -f1)"
LABELS_CURRENT_HASH="$(cat "$LABELS_FILE" 2>/dev/null || true)"

if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED_HASH" != "$LABELS_CURRENT_HASH" ]; then
rm -vf "$INSTANCE_DIR/.runner" || true

${cfg.package}/bin/forgejo-runner register \
--no-interactive \
--instance ${escapeShellArg instance.url} \
--token "$(cat "$CREDENTIALS_DIRECTORY/REGISTRATION_TOKEN")" \
--name ${escapeShellArg instance.name} \
--labels ${escapeShellArg (concatStringsSep "," instance.labels)} \
--config ${configFile}

printf '%s' "$LABELS_WANTED_HASH" > "$LABELS_FILE"
fi
'';
}
))
];

ExecStart = lib.mkForce "${cfg.package}/bin/forgejo-runner daemon --config ${configFile}";
};
};
in
{
"forgejo-runner@" = {
description = "Forgejo Actions Runner (%I)";

environment = {
HOME = "/var/lib/forgejo-runner/%i";
};

serviceConfig = {
DynamicUser = true;
User = "forgejo-runner-%i";
StateDirectory = "forgejo-runner/%i";
WorkingDirectory = "/var/lib/forgejo-runner/%i";

Restart = "on-failure";
RestartSec = 2;

AmbientCapabilities = "";
CapabilityBoundingSet = "";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = 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" ];
UMask = "0077";
};
};
}
// mapAttrs' mkRunnerInstance cfg.instances;
};
}
Loading
Loading