diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix index bd4cf6a37bad..84f04a276412 100644 --- a/nixos/modules/services/continuous-integration/gitlab-runner.nix +++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix @@ -1,160 +1,494 @@ { config, lib, pkgs, ... }: - with lib; - let cfg = config.services.gitlab-runner; - configFile = - if (cfg.configFile == null) then - (pkgs.runCommand "config.toml" { - buildInputs = [ pkgs.remarshal ]; - preferLocalBuild = true; - } '' - remarshal -if json -of toml \ - < ${pkgs.writeText "config.json" (builtins.toJSON cfg.configOptions)} \ - > $out - '') - else - cfg.configFile; hasDocker = config.virtualisation.docker.enable; + hashedServices = with builtins; (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.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 (service.executor == "docker") ( + assert ( + assertMsg (service.dockerImage != null) + "dockerImage option is required for docker 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 + 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 '.check_interval = ${toString cfg.checkInterval} | + .concurrent = ${toString cfg.concurrent}' \ + | 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. - Use this option in favor of configOptions to avoid placing CI tokens in the nix store. - takes precedence over . + takes precedence over . + and will be ignored too. - Warning: Not using will potentially result in secrets - leaking into the WORLD-READABLE nix store. + This option is deprecated, please use instead. + You can use and + + for settings not covered by this module. ''; - type = types.nullOr types.path; }; - - configOptions = mkOption { + checkInterval = mkOption { + type = types.int; + default = 0; + example = literalExample "with lib; (length (attrNames config.services.gitlab-runner.services)) * 3"; description = '' - Configuration for gitlab-runner - will take precedence over this option. - - Warning: all Configuration, especially CI token, will be stored in a - WORLD-READABLE file in the Nix Store. - - If you want to protect your CI token use instead. + 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. ''; - type = types.attrs; - example = { - concurrent = 2; - runners = [{ - name = "docker-nix-1.11"; - url = "https://CI/"; - token = "TOKEN"; - executor = "docker"; - builds_dir = ""; - docker = { - host = ""; - image = "nixos/nix:1.11"; - privileged = true; - disable_cache = true; - cache_dir = ""; - }; - }]; - }; }; - gracefulTermination = mkOption { - default = false; type = types.bool; + default = false; description = '' - Finish all remaining jobs before stopping, restarting or reconfiguring. - If not set gitlab-runner will stop immediatly without waiting for jobs to finish, - which will lead to failed builds. + 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 { - default = "infinity"; type = types.str; + default = "infinity"; example = "5min 20s"; - description = ''Time to wait until a graceful shutdown is turned into a forceful one.''; + description = '' + Time to wait until a graceful shutdown is turned into a forceful one. + ''; }; - - workDir = mkOption { - default = "/var/lib/gitlab-runner"; - type = types.path; - description = "The working directory used"; - }; - package = mkOption { - description = "Gitlab Runner package to use"; + type = types.package; default = pkgs.gitlab-runner; defaultText = "pkgs.gitlab-runner"; - type = types.package; example = literalExample "pkgs.gitlab-runner_1_11"; + description = "Gitlab Runner package to use."; }; - - packages = mkOption { - default = [ pkgs.bash pkgs.docker-machine ]; - defaultText = "[ pkgs.bash pkgs.docker-machine ]"; + extraPackages = mkOption { type = types.listOf types.package; + default = [ ]; description = '' - Packages to add to PATH for the gitlab-runner process. + 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). + ''; + }; + 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 { + warnings = optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`."; + environment.systemPackages = [ cfg.package ]; systemd.services.gitlab-runner = { - path = cfg.packages; - environment = config.networking.proxy.envVars // { - # Gitlab runner will not start if the HOME variable is not set - HOME = cfg.workDir; - }; 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 + utillinux + cfg.package.bin + ] ++ cfg.extraPackages; reloadIfChanged = true; - restartTriggers = [ - config.environment.etc."gitlab-runner/config.toml".source - ]; 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"; - ExecReload= "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; - ExecStart = ''${cfg.package.bin}/bin/gitlab-runner run \ - --working-directory ${cfg.workDir} \ - --config /etc/gitlab-runner/config.toml \ - --service gitlab-runner \ - --user gitlab-runner \ - ''; - - } // optionalAttrs (cfg.gracefulTermination) { + 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"; }; }; - - # Make the gitlab-runner command availabe so users can query the runner - environment.systemPackages = [ cfg.package ]; - - # Make sure the config can be reloaded on change - environment.etc."gitlab-runner/config.toml".source = configFile; - - users.users.gitlab-runner = { - group = "gitlab-runner"; - extraGroups = optional hasDocker "docker"; - uid = config.ids.uids.gitlab-runner; - home = cfg.workDir; - createHome = true; - }; - - users.groups.gitlab-runner.gid = config.ids.gids.gitlab-runner; + # 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" ) + ]; }