diff --git a/modules/module-list.nix b/modules/module-list.nix
index fc27ff05..57b0bf87 100644
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -46,6 +46,7 @@
./services/cachix-agent.nix
./services/dnsmasq.nix
./services/emacs.nix
+ ./services/gitlab-runner.nix
./services/khd
./services/kwm
./services/lorri.nix
diff --git a/modules/services/gitlab-runner.nix b/modules/services/gitlab-runner.nix
new file mode 100644
index 00000000..a8f5f0b2
--- /dev/null
+++ b/modules/services/gitlab-runner.nix
@@ -0,0 +1,643 @@
+{ config, lib, pkgs, ... }:
+with builtins;
+with lib;
+let
+ cfg = config.services.gitlab-runner;
+ hasDocker = config.virtualisation.docker.enable;
+ hashedServices = mapAttrs'
+ (name: service: nameValuePair
+ "${name}_${config.networking.hostName}_${
+ substring 0 12
+ (hashString "md5" (unsafeDiscardStringContext (toJSON service)))}"
+ service)
+ cfg.services;
+ configPath = "$HOME/.gitlab-runner/config.toml";
+ configureScript = pkgs.writeShellScriptBin "gitlab-runner-configure" (
+ if (cfg.configFile != null) then ''
+ mkdir -p $(dirname ${configPath})
+ cp ${cfg.configFile} ${configPath}
+ # make config file readable by service
+ chown -R --reference=$HOME $(dirname ${configPath})
+ '' else ''
+ export CONFIG_FILE=${configPath}
+
+ mkdir -p $(dirname ${configPath})
+
+ # remove no longer existing services
+ gitlab-runner verify --delete
+
+ # current and desired state
+ NEEDED_SERVICES=$(echo ${concatStringsSep " " (attrNames hashedServices)} | tr " " "\n")
+ REGISTERED_SERVICES=$(gitlab-runner list 2>&1 | grep 'Executor' | awk '{ print $1 }')
+
+ # difference between current and desired state
+ NEW_SERVICES=$(grep -vxF -f <(echo "$REGISTERED_SERVICES") <(echo "$NEEDED_SERVICES") || true)
+ OLD_SERVICES=$(grep -vxF -f <(echo "$NEEDED_SERVICES") <(echo "$REGISTERED_SERVICES") || true)
+
+ # register new services
+ ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
+ if echo "$NEW_SERVICES" | grep -xq ${name}; then
+ bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
+ "set -a && source ${service.registrationConfigFile} &&"
+ "gitlab-runner register"
+ "--non-interactive"
+ "--name ${name}"
+ "--executor ${service.executor}"
+ "--limit ${toString service.limit}"
+ "--request-concurrency ${toString service.requestConcurrency}"
+ "--maximum-timeout ${toString service.maximumTimeout}"
+ ] ++ service.registrationFlags
+ ++ optional (service.buildsDir != null)
+ "--builds-dir ${service.buildsDir}"
+ ++ optional (service.cloneUrl != null)
+ "--clone-url ${service.cloneUrl}"
+ ++ optional (service.preCloneScript != null)
+ "--pre-clone-script ${service.preCloneScript}"
+ ++ optional (service.preBuildScript != null)
+ "--pre-build-script ${service.preBuildScript}"
+ ++ optional (service.postBuildScript != null)
+ "--post-build-script ${service.postBuildScript}"
+ ++ optional (service.tagList != [ ])
+ "--tag-list ${concatStringsSep "," service.tagList}"
+ ++ optional service.runUntagged
+ "--run-untagged"
+ ++ optional service.protected
+ "--access-level ref_protected"
+ ++ optional service.debugTraceDisabled
+ "--debug-trace-disabled"
+ ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
+ ++ optionals (hasPrefix "docker" service.executor) (
+ assert (
+ assertMsg (service.dockerImage != null)
+ "dockerImage option is required for ${service.executor} executor (${name})");
+ [ "--docker-image ${service.dockerImage}" ]
+ ++ optional service.dockerDisableCache
+ "--docker-disable-cache"
+ ++ optional service.dockerPrivileged
+ "--docker-privileged"
+ ++ map (v: "--docker-volumes ${escapeShellArg v}") service.dockerVolumes
+ ++ map (v: "--docker-extra-hosts ${escapeShellArg v}") service.dockerExtraHosts
+ ++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
+ ++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
+ )
+ ))} && sleep 1 || exit 1
+ fi
+ '') hashedServices)}
+
+ # unregister old services
+ for NAME in $(echo "$OLD_SERVICES")
+ do
+ [ ! -z "$NAME" ] && gitlab-runner unregister \
+ --name "$NAME" && sleep 1
+ done
+
+ # update global options
+ remarshal --if toml --of json ${configPath} \
+ | jq -cM ${escapeShellArg (concatStringsSep " | " [
+ ".check_interval = ${toJSON cfg.checkInterval}"
+ ".concurrent = ${toJSON cfg.concurrent}"
+ ".sentry_dsn = ${toJSON cfg.sentryDSN}"
+ ".listen_address = ${toJSON cfg.prometheusListenAddress}"
+ ".session_server.listen_address = ${toJSON cfg.sessionServer.listenAddress}"
+ ".session_server.advertise_address = ${toJSON cfg.sessionServer.advertiseAddress}"
+ ".session_server.session_timeout = ${toJSON cfg.sessionServer.sessionTimeout}"
+ "del(.[] | nulls)"
+ "del(.session_server[] | nulls)"
+ ])} \
+ | remarshal --if json --of toml \
+ | sponge ${configPath}
+
+ # make config file readable by service
+ chown -R --reference=$HOME $(dirname ${configPath})
+ '');
+ startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
+ export CONFIG_FILE=${configPath}
+ exec gitlab-runner run --working-directory $HOME
+ '';
+in
+{
+ options.services.gitlab-runner = {
+ enable = mkEnableOption "Gitlab Runner";
+ configFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Configuration file for gitlab-runner.
+
+ takes precedence over .
+ and will be ignored too.
+
+ This option is deprecated, please use instead.
+ You can use and
+
+ for settings not covered by this module.
+ '';
+ };
+ checkInterval = mkOption {
+ type = types.int;
+ default = 0;
+ example = literalExample "with lib; (length (attrNames config.services.gitlab-runner.services)) * 3";
+ description = ''
+ Defines the interval length, in seconds, between new jobs check.
+ The default value is 3;
+ if set to 0 or lower, the default value will be used.
+ See runner documentation for more information.
+ '';
+ };
+ concurrent = mkOption {
+ type = types.int;
+ default = 1;
+ example = literalExample "config.nix.maxJobs";
+ description = ''
+ Limits how many jobs globally can be run concurrently.
+ The most upper limit of jobs using all defined runners.
+ 0 does not mean unlimited.
+ '';
+ };
+ sentryDSN = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "https://public:private@host:port/1";
+ description = ''
+ Data Source Name for tracking of all system level errors to Sentry.
+ '';
+ };
+ prometheusListenAddress = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "localhost:8080";
+ description = ''
+ Address (<host>:<port>) on which the Prometheus metrics HTTP server
+ should be listening.
+ '';
+ };
+ sessionServer = mkOption {
+ type = types.submodule {
+ options = {
+ listenAddress = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "0.0.0.0:8093";
+ description = ''
+ An internal URL to be used for the session server.
+ '';
+ };
+ advertiseAddress = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "runner-host-name.tld:8093";
+ description = ''
+ The URL that the Runner will expose to GitLab to be used
+ to access the session server.
+ Fallbacks to if not defined.
+ '';
+ };
+ sessionTimeout = mkOption {
+ type = types.int;
+ default = 1800;
+ description = ''
+ How long in seconds the session can stay active after
+ the job completes (which will block the job from finishing).
+ '';
+ };
+ };
+ };
+ default = { };
+ example = literalExample ''
+ {
+ listenAddress = "0.0.0.0:8093";
+ }
+ '';
+ description = ''
+ The session server allows the user to interact with jobs
+ that the Runner is responsible for. A good example of this is the
+ interactive web terminal.
+ '';
+ };
+ gracefulTermination = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Finish all remaining jobs before stopping.
+ If not set gitlab-runner will stop immediatly without waiting
+ for jobs to finish, which will lead to failed builds.
+ '';
+ };
+ gracefulTimeout = mkOption {
+ type = types.str;
+ default = "infinity";
+ example = "5min 20s";
+ description = ''
+ Time to wait until a graceful shutdown is turned into a forceful one.
+ '';
+ };
+ package = mkOption {
+ type = types.package;
+ default = pkgs.gitlab-runner;
+ defaultText = "pkgs.gitlab-runner";
+ example = literalExample "pkgs.gitlab-runner_1_11";
+ description = "Gitlab Runner package to use.";
+ };
+ extraPackages = mkOption {
+ type = types.listOf types.package;
+ default = [ ];
+ description = ''
+ Extra packages to add to PATH for the gitlab-runner process.
+ '';
+ };
+ services = mkOption {
+ description = "GitLab Runner services.";
+ default = { };
+ example = literalExample ''
+ {
+ # runner for building in docker via host's nix-daemon
+ # nix store will be readable in runner, might be insecure
+ nix = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ dockerImage = "alpine";
+ dockerVolumes = [
+ "/nix/store:/nix/store:ro"
+ "/nix/var/nix/db:/nix/var/nix/db:ro"
+ "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro"
+ ];
+ dockerDisableCache = true;
+ preBuildScript = pkgs.writeScript "setup-container" '''
+ mkdir -p -m 0755 /nix/var/log/nix/drvs
+ mkdir -p -m 0755 /nix/var/nix/gcroots
+ mkdir -p -m 0755 /nix/var/nix/profiles
+ mkdir -p -m 0755 /nix/var/nix/temproots
+ mkdir -p -m 0755 /nix/var/nix/userpool
+ mkdir -p -m 1777 /nix/var/nix/gcroots/per-user
+ mkdir -p -m 1777 /nix/var/nix/profiles/per-user
+ mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root
+ mkdir -p -m 0700 "$HOME/.nix-defexpr"
+
+ . ''${pkgs.nix}/etc/profile.d/nix.sh
+
+ ''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])}
+
+ ''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable
+ ''${pkgs.nix}/bin/nix-channel --update nixpkgs
+ ''';
+ environmentVariables = {
+ ENV = "/etc/profile";
+ USER = "root";
+ NIX_REMOTE = "daemon";
+ PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin";
+ NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt";
+ };
+ tagList = [ "nix" ];
+ };
+ # runner for building docker images
+ docker-images = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ dockerImage = "docker:stable";
+ dockerVolumes = [
+ "/var/run/docker.sock:/var/run/docker.sock"
+ ];
+ tagList = [ "docker-images" ];
+ };
+ # runner for executing stuff on host system (very insecure!)
+ # make sure to add required packages (including git!)
+ # to `environment.systemPackages`
+ shell = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ executor = "shell";
+ tagList = [ "shell" ];
+ };
+ # runner for everything else
+ default = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ dockerImage = "debian:stable";
+ };
+ }
+ '';
+ type = types.attrsOf (types.submodule {
+ options = {
+ registrationConfigFile = mkOption {
+ type = types.path;
+ description = ''
+ Absolute path to a file with environment variables
+ used for gitlab-runner registration.
+ A list of all supported environment variables can be found in
+ gitlab-runner register --help.
+
+ Ones that you probably want to set is
+
+ CI_SERVER_URL=<CI server URL>
+
+ REGISTRATION_TOKEN=<registration secret>
+ '';
+ };
+ registrationFlags = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--docker-helper-image my/gitlab-runner-helper" ];
+ description = ''
+ Extra command-line flags passed to
+ gitlab-runner register.
+ Execute gitlab-runner register --help
+ for a list of supported flags.
+ '';
+ };
+ environmentVariables = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = { NAME = "value"; };
+ description = ''
+ Custom environment variables injected to build environment.
+ For secrets you can use
+ with RUNNER_ENV variable set.
+ '';
+ };
+ executor = mkOption {
+ type = types.str;
+ default = "docker";
+ description = ''
+ Select executor, eg. shell, docker, etc.
+ See runner documentation for more information.
+ '';
+ };
+ buildsDir = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ example = "/var/lib/gitlab-runner/builds";
+ description = ''
+ Absolute path to a directory where builds will be stored
+ in context of selected executor (Locally, Docker, SSH).
+ '';
+ };
+ cloneUrl = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "http://gitlab.example.local";
+ description = ''
+ Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
+ '';
+ };
+ dockerImage = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Docker image to be used.
+ '';
+ };
+ dockerVolumes = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
+ description = ''
+ Bind-mount a volume and create it
+ if it doesn't exist prior to mounting.
+ '';
+ };
+ dockerDisableCache = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Disable all container caching.
+ '';
+ };
+ dockerPrivileged = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Give extended privileges to container.
+ '';
+ };
+ dockerExtraHosts = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "other-host:127.0.0.1" ];
+ description = ''
+ Add a custom host-to-IP mapping.
+ '';
+ };
+ dockerAllowedImages = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
+ description = ''
+ Whitelist allowed images.
+ '';
+ };
+ dockerAllowedServices = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "postgres:9" "redis:*" "mysql:*" ];
+ description = ''
+ Whitelist allowed services.
+ '';
+ };
+ preCloneScript = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Runner-specific command script executed before code is pulled.
+ '';
+ };
+ preBuildScript = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Runner-specific command script executed after code is pulled,
+ just before build executes.
+ '';
+ };
+ postBuildScript = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Runner-specific command script executed after code is pulled
+ and just after build executes.
+ '';
+ };
+ tagList = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = ''
+ Tag list.
+ '';
+ };
+ runUntagged = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Register to run untagged builds; defaults to
+ true when is empty.
+ '';
+ };
+ limit = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ Limit how many jobs can be handled concurrently by this service.
+ 0 (default) simply means don't limit.
+ '';
+ };
+ requestConcurrency = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ Limit number of concurrent requests for new jobs from GitLab.
+ '';
+ };
+ maximumTimeout = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ What is the maximum timeout (in seconds) that will be set for
+ job when using this Runner. 0 (default) simply means don't limit.
+ '';
+ };
+ protected = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ When set to true Runner will only run on pipelines
+ triggered on protected branches.
+ '';
+ };
+ debugTraceDisabled = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ When set to true Runner will disable the possibility of
+ using the CI_DEBUG_TRACE feature.
+ '';
+ };
+ };
+ });
+ };
+ };
+ config = mkIf cfg.enable {
+
+ users.users.gitlab-runner =
+ { name = "gitlab-runner";
+ uid = mkDefault 532;
+ # gid = mkDefault config.users.groups.gitlab-runner.gid;
+ home = mkDefault "/var/lib/gitlab-runner";
+ shell = "/bin/bash";
+ description = "Gitlab agent user";
+ };
+ users.groups.gitlab-runner =
+ { name = "gitlab-runner";
+ gid = mkDefault 532;
+ description = "Gitlab agent user group";
+ };
+
+
+ # system.activationScripts.preActivation.text = let user = config.users.users.gitlab-runner; in ''
+ # mkdir -p '${user.home}'
+ # chown ${toString user.uid}:${toString user.gid} '${user.home}'
+ #'';
+
+
+ warnings = optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
+ environment.systemPackages = [ cfg.package ];
+
+ launchd.daemons.gitlab-runner = {
+ environment = { #config.networking.proxy.envVars // {
+ HOME = "${config.users.users.gitlab-runner.home}";
+ NIX_SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+ } // (if config.nix.useDaemon then { NIX_REMOTE = "daemon"; } else {});
+ path = with pkgs; [
+ bash
+ gawk
+ jq
+ moreutils
+ remarshal
+ # util-linux
+ cfg.package
+ coreutils
+ gnugrep
+ gnused
+ ] ++ cfg.extraPackages;
+
+ script = ''
+ ${configureScript}/bin/gitlab-runner-configure && ${startScript}/bin/gitlab-runner-start
+ '';
+
+ serviceConfig = {
+ ProcessType = "Interactive";
+ ThrottleInterval = 30;
+
+ # StandardOutPath = "/var/lib/gitlab-runner/out.log";
+ # StandardErrorPath = "/var/lib/gitlab-runner/err.log";
+ # The combination of KeepAlive.NetworkState and WatchPaths
+ # will ensure that buildkite-agent is started on boot, but
+ # after networking is available (so the hostname is
+ # correct).
+ RunAtLoad = true;
+ # KeepAlive.NetworkState = true;
+ WatchPaths = [
+ "/etc/resolv.conf"
+ "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"
+ ];
+
+ GroupName = "gitlab-runner";
+ UserName = "gitlab-runner";
+ WorkingDirectory = config.users.users.gitlab-runner.home;
+ };
+
+ };
+ # systemd.services.gitlab-runner = {
+ # description = "Gitlab Runner";
+ # documentation = [ "https://docs.gitlab.com/runner/" ];
+ # after = [ "network.target" ]
+ # ++ optional hasDocker "docker.service";
+ # requires = optional hasDocker "docker.service";
+ # wantedBy = [ "multi-user.target" ];
+ # environment = config.networking.proxy.envVars // {
+ # HOME = "/var/lib/gitlab-runner";
+ # };
+ # path = with pkgs; [
+ # bash
+ # gawk
+ # jq
+ # moreutils
+ # remarshal
+ # util-linux
+ # cfg.package
+ # ] ++ cfg.extraPackages;
+ # reloadIfChanged = true;
+ # serviceConfig = {
+ # # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
+ # # to `lib.mkForce false` in your configuration to run this service as root.
+ # # You can also set `User` and `Group` options to run this service as desired user.
+ # # Make sure to restart service or changes won't apply.
+ # DynamicUser = true;
+ # StateDirectory = "gitlab-runner";
+ # SupplementaryGroups = optional hasDocker "docker";
+ # ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
+ # ExecStart = "${startScript}/bin/gitlab-runner-start";
+ # ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
+ # } // optionalAttrs (cfg.gracefulTermination) {
+ # TimeoutStopSec = "${cfg.gracefulTimeout}";
+ # KillSignal = "SIGQUIT";
+ # KillMode = "process";
+ # };
+ # };
+ # # Enable docker if `docker` executor is used in any service
+ # virtualisation.docker.enable = mkIf (
+ # any (s: s.executor == "docker") (attrValues cfg.services)
+ # ) (mkDefault true);
+ };
+ imports = [
+ (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] )
+ (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" )
+ (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" )
+ ];
+}