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" ) + ]; +}