diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix index 84f04a276412..bd4cf6a37bad 100644 --- a/nixos/modules/services/continuous-integration/gitlab-runner.nix +++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix @@ -1,494 +1,160 @@ { 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 . - and will be ignored too. + takes precedence over . - This option is deprecated, please use instead. - You can use and - - for settings not covered by this module. + Warning: Not using will potentially result in secrets + leaking into the WORLD-READABLE nix store. ''; + type = types.nullOr types.path; }; - checkInterval = mkOption { - type = types.int; - default = 0; - example = literalExample "with lib; (length (attrNames config.services.gitlab-runner.services)) * 3"; + + configOptions = mkOption { 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. + 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. ''; + 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 { - type = types.bool; default = false; + type = types.bool; 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. + 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. ''; }; + gracefulTimeout = mkOption { - type = types.str; default = "infinity"; + type = types.str; 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 { - type = types.package; + description = "Gitlab Runner package to use"; default = pkgs.gitlab-runner; defaultText = "pkgs.gitlab-runner"; + type = types.package; example = literalExample "pkgs.gitlab-runner_1_11"; - description = "Gitlab Runner package to use."; }; - extraPackages = mkOption { + + packages = mkOption { + default = [ pkgs.bash pkgs.docker-machine ]; + defaultText = "[ pkgs.bash pkgs.docker-machine ]"; type = types.listOf types.package; - default = [ ]; description = '' - Extra packages to add to PATH for the gitlab-runner process. + 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"; - 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) { + 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) { 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); + + # 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; }; - 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" ) - ]; }