From c61f554c1aa9fbec38b73cb7bbb205572673a5ba Mon Sep 17 00:00:00 2001 From: Matthieu Coudron Date: Fri, 21 Oct 2022 15:18:10 +0200 Subject: [PATCH] modules.gitlab-runner: accept space in names when you register a runner with spaces in its name (possible if you use 'description' option) then the runners never get unregistered because our bash scripts assume no space in names. This solves the issue Retreiving the fullname of the runner via `gitlab-runner list` got surprisingly hard between lazy-capture issues and `gitlab-runner list` displaying invisible (CSI) characters that break the regex etc. Which is why I fell back on the pseudo-json format. This PR adds the hash in the name, which allows to keep both the stateless aspect of the module while allowing for a freeform name. I found using bash associative arrays easier to use/debug than the current approach. --- .../continuous-integration/gitlab-runner.nix | 90 ++++++++++++++----- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix index 7b1c4da86260..d18c4cff0405 100644 --- a/nixos/modules/services/continuous-integration/gitlab-runner.nix +++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix @@ -4,24 +4,41 @@ with lib; let cfg = config.services.gitlab-runner; hasDocker = config.virtualisation.docker.enable; + + /* The whole logic of this module is to diff the hashes of the desired vs existing runners + The hash is recorded in the runner's name because we can't do better yet + See https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29350 for more details + */ + genRunnerName = service: let + hash = substring 0 12 (hashString "md5" (unsafeDiscardStringContext (toJSON service))); + in if service ? description + then "${hash} ${service.description}" + else "${name}_${config.networking.hostName}_${hash}"; + 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}) + (name: service: nameValuePair (genRunnerName service) service) cfg.services; + configPath = ''"$HOME"/.gitlab-runner/config.toml''; + configureScript = pkgs.writeShellApplication { + name = "gitlab-runner-configure"; + runtimeInputs = with pkgs; [ + bash + gawk + jq + moreutils + remarshal + util-linux + cfg.package + perl + python3 + ]; + text = if (cfg.configFile != null) then '' 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}) + mkdir -p "$(dirname "${configPath}")" touch ${configPath} # update global options @@ -34,22 +51,43 @@ let # 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 }') + ${toShellVar "NEEDED_SERVICES" (lib.mapAttrs (name: value: 1) hashedServices)} + + declare -A REGISTERED_SERVICES + + while IFS="," read -r name token; + do + REGISTERED_SERVICES["$name"]="$token" + done < <(gitlab-runner --log-format json list 2>&1 | grep Token | jq -r '.msg +"," + .Token') + + echo "NEEDED_SERVICES: " "''${!NEEDED_SERVICES[@]}" + echo "REGISTERED_SERVICES:" "''${!REGISTERED_SERVICES[@]}" # 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) + declare -A NEW_SERVICES + for name in "''${!NEEDED_SERVICES[@]}"; do + if [ ! -v 'REGISTERED_SERVICES[$name]' ]; then + NEW_SERVICES[$name]=1 + fi + done + + declare -A OLD_SERVICES + # shellcheck disable=SC2034 + for name in "''${!REGISTERED_SERVICES[@]}"; do + if [ ! -v 'NEEDED_SERVICES[$name]' ]; then + OLD_SERVICES[$name]=1 + fi + done # register new services ${concatStringsSep "\n" (mapAttrsToList (name: service: '' - if echo "$NEW_SERVICES" | grep -xq "${name}"; then + # TODO so here we should mention NEW_SERVICES + if [ -v 'NEW_SERVICES["${name}"]' ] ; then bash -c ${escapeShellArg (concatStringsSep " \\\n " ([ "set -a && source ${service.registrationConfigFile} &&" "gitlab-runner register" "--non-interactive" - (if service.description != null then "--description \"${service.description}\"" else "--name '${name}'") + "--name '${name}'" "--executor ${service.executor}" "--limit ${toString service.limit}" "--request-concurrency ${toString service.requestConcurrency}" @@ -92,22 +130,26 @@ let fi '') hashedServices)} + # check key is in array https://stackoverflow.com/questions/30353951/how-to-check-if-dictionary-contains-a-key-in-bash + + echo "NEW_SERVICES: ''${NEW_SERVICES[*]}" + echo "OLD_SERVICES: ''${OLD_SERVICES[*]}" # unregister old services - for NAME in $(echo "$OLD_SERVICES") + for NAME in "''${!OLD_SERVICES[@]}" do - [ ! -z "$NAME" ] && gitlab-runner unregister \ + [ -n "$NAME" ] && gitlab-runner unregister \ --name "$NAME" && sleep 1 done # make config file readable by service - chown -R --reference=$HOME $(dirname ${configPath}) - ''); + chown -R --reference="$HOME" "$(dirname ${configPath})" + ''; + }; startScript = pkgs.writeShellScriptBin "gitlab-runner-start" '' export CONFIG_FILE=${configPath} exec gitlab-runner run --working-directory $HOME ''; -in -{ +in { options.services.gitlab-runner = { enable = mkEnableOption (lib.mdDoc "Gitlab Runner"); configFile = mkOption {