{ 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. {option}`configFile` takes precedence over {option}`services`. {option}`checkInterval` and {option}`concurrent` will be ignored too. This option is deprecated, please use {option}`services` instead. You can use {option}`registrationConfigFile` and {option}`registrationFlags` for settings not covered by this module. ''; }; checkInterval = mkOption { type = types.int; default = 0; example = literalExpression "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](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#how-check_interval-works) for more information. ''; }; concurrent = mkOption { type = types.int; default = 1; example = literalExpression "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 {option}`listenAddress` 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 = literalExpression '' { 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](https://docs.gitlab.com/ee/ci/interactive_web_terminal/index.html). ''; }; 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 = literalExpression "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 = literalExpression '' { # 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=` `REGISTRATION_TOKEN=` ''; }; 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 {option}`registrationConfigFile` with `RUNNER_ENV` variable set. ''; }; executor = mkOption { type = types.str; default = "docker"; description = '' Select executor, eg. shell, docker, etc. See [runner documentation](https://docs.gitlab.com/runner/executors/README.html) 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 {option}`tagList` 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" ) ]; }