diff --git a/nixos/tests/gitlab/runner.nix b/nixos/tests/gitlab/runner.nix index 27eb42b44c9d..839148b08c39 100644 --- a/nixos/tests/gitlab/runner.nix +++ b/nixos/tests/gitlab/runner.nix @@ -126,7 +126,7 @@ in testScript = { nodes, ... }: let - auth = pkgs.writeText "auth.json" ( + authPayload = pkgs.writeText "auth.json" ( builtins.toJSON { grant_type = "password"; username = "root"; @@ -134,129 +134,35 @@ in } ); - runnerTokenTmpl = pkgs.writeText "runner-token.env" '' + runnerTokenEnv = pkgs.writeText "runner-token.env" '' CI_SERVER_URL=http://gitlab CI_SERVER_TOKEN=$token ''; - createRunner = pkgs.writeText "create-runner.json" ( + createRunnerPayload = pkgs.writeText "create-runner.json" ( builtins.toJSON { runner_type = "instance_type"; } ); - - # Wait for all GitLab services to be fully started. - waitForServices = '' - gitlab.wait_for_unit("gitaly.service") - gitlab.wait_for_unit("gitlab-workhorse.service") - gitlab.wait_for_unit("gitlab.service") - gitlab.wait_for_unit("gitlab-sidekiq.service") - gitlab.wait_for_file("${nodes.gitlab.services.gitlab.statePath}/tmp/sockets/gitlab.socket") - gitlab.wait_until_succeeds("curl -sSf http://gitlab/users/sign_in") - ''; - - testSetup = - # python - '' - import os - import json - from pathlib import Path - from string import Template - from dataclasses import dataclass - - print("==> Getting secrets and headers.") - gitlab.succeed("cp /var/gitlab/state/config/secrets.yml /root/gitlab-secrets.yml") - - gitlab.succeed( - "echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @${auth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers" - ) - - gitlab.copy_from_vm("/tmp/headers") - out_dir = os.environ.get("out", os.getcwd()) - gitlab_runner.copy_from_host(str(Path(out_dir, "headers")), "/tmp/headers") - - print("==> Testing connection.") - gitlab_runner.succeed("curl -v -H @/tmp/headers http://gitlab/api/v4/version") - - # Runner config data. - @dataclass - class Runner: - name: str - desc: str - tokenFile: str - - id: str - token: str - - runnerConfigs: dict[str, Runner] = {} - ''; - - testRegisterRunner = - runnerConfig: - # python - '' - r = Runner(name="${runnerConfig.name}", - desc="${runnerConfig.desc}", - tokenFile="${runnerConfig.tokenFile}", - token="", - id="") - runnerConfigs[r.name] = r - - print(f"==> Create Runner '{r.name}'") - resp = gitlab.execute(""" - curl -s -X POST \ - -H 'Content-Type: application/json' \ - -H @/tmp/headers \ - -d @${createRunner} \ - http://gitlab/api/v4/user/runners - """)[1] - obj = json.loads(resp) - r.id = obj["id"] - r.token = obj["token"] - - # Push the token to the runner machine. - print("==> Push runner token to machine.") - tokenF = Path(out_dir, f"token-{r.name}.env") - with open("${runnerTokenTmpl}", "r") as f: - tokenData = Template(f.read()).substitute({"token": token}) - with open(tokenF, "w") as w: - w.write(tokenData) - gitlab_runner.copy_from_host(str(tokenF), r.tokenFile) - ''; - - restartRunnerService = - # python - '' - print("==> Restart Gitlab Runner") - gitlab_runner.systemctl("restart gitlab-runner.service") - gitlab_runner.wait_for_unit("gitlab-runner.service") - ''; - - testRunnerConnected = - runnerConfig: - # python - '' - r = runnerConfigs["${runnerConfig.name}"] - - resp = gitlab.execute(f""" - curl -s -X GET \ - -H 'Content-Type: application/json' \ - -H @/tmp/headers \ - http://gitlab/api/v4/runners/{r.id} - """)[1] - runnerStatus = json.loads(resp) - - if not runnerStatus["active"]: - raise Exception(f"Runner '{r.name}' [id: '{r.id}'] status is not active: {resp}") - ''; in # python '' + # Define some globals for the python script below. + JQ_BINARY="${pkgs.jq}/bin/jq" + GITLAB_STATE_PATH="${nodes.gitlab.services.gitlab.statePath}" + RUNNER_TOKEN_ENV_FILE="${runnerTokenEnv}" + AUTH_PAYLOAD_FILE="${authPayload}" + CREATE_RUNNER_PAYLOAD_FILE="${createRunnerPayload}" + + ${lib.readFile ./runner_test.py} + start_all() - '' - + waitForServices - + testSetup - + (testRegisterRunner runnerConfigs.shell) - + restartRunnerService - + (testRunnerConnected runnerConfigs.shell); + wait_for_services() + + # Run all tests. + test_connection() + test_register_runner(name="shell", tokenFile="${runnerConfigs.shell.tokenFile}") + restart_gitlab_runner_service() + test_runner_registered(runnerConfigs["shell"]) + ''; } diff --git a/nixos/tests/gitlab/runner_test.py b/nixos/tests/gitlab/runner_test.py new file mode 100644 index 000000000000..aacba1f94c5f --- /dev/null +++ b/nixos/tests/gitlab/runner_test.py @@ -0,0 +1,140 @@ +import json +import os +from dataclasses import dataclass +from pathlib import Path +from string import Template +from typing import Any + + +@dataclass +class Runner: + name: str + tokenFile: str + + id: str + token: str + + +@dataclass +class Machines: + gitlab: Any + gitlab_runner: Any + + +@dataclass +class Nix: + jq: str + gitlab_state_path: str + create_runner_payload_file: str + auth_payload_file: str + runner_token_env_file: str + + +# Some global variables to work in the tests. +out_dir = os.environ.get("out", os.getcwd()) +nix = Nix( + jq=JQ_BINARY, + gitlab_state_path=GITLAB_STATE_PATH, + auth_payload_file=AUTH_PAYLOAD_FILE, + create_runner_payload_file=CREATE_RUNNER_PAYLOAD_FILE, + runner_token_env_file=RUNNER_TOKEN_ENV_FILE, +) +vms = Machines(gitlab, gitlab_runner) +runnerConfigs: dict[str, Runner] = {} + + +def wait_for_services(): + vms.gitlab.wait_for_unit("gitaly.service") + vms.gitlab.wait_for_unit("gitlab-workhorse.service") + vms.gitlab.wait_for_unit("gitlab.service") + vms.gitlab.wait_for_unit("gitlab-sidekiq.service") + vms.gitlab.wait_for_file(f"{nix.gitlab_state_path}/tmp/sockets/gitlab.socket") + vms.gitlab.wait_until_succeeds("curl -sSf http://gitlab/users/sign_in") + + +def test_connection(): + """ + Test the connection to Gitlab and check if it is reachable from the runner VM. + """ + + print("==> Getting secrets and headers.") + vms.gitlab.succeed( + "cp /var/gitlab/state/config/secrets.yml /root/gitlab-secrets.yml" + ) + + vms.gitlab.succeed( + f"echo \"Authorization: Bearer $(curl -X POST -H 'Content-Type: application/json' -d @{nix.auth_payload_file} http://gitlab/oauth/token | {nix.jq} -r '.access_token')\" >/tmp/headers" + ) + + vms.gitlab.copy_from_vm("/tmp/headers") + out_dir = os.environ.get("out", os.getcwd()) + vms.gitlab_runner.copy_from_host(str(Path(out_dir, "headers")), "/tmp/headers") + + print("==> Testing connection.") + vms.gitlab_runner.succeed("curl -v -H @/tmp/headers http://gitlab/api/v4/version") + + +def test_register_runner(name: str, tokenFile: str): + """ + Register the runner in Gitlab and write the token file to be picked up by + the gitlab-runner service on the other VM. + """ + + r = Runner( + name=name, + tokenFile=tokenFile, + token="", + id="", + ) + runnerConfigs[r.name] = r + + print(f"==> Create Runner '{r.name}'") + resp = vms.gitlab.execute( + f""" + curl -s -X POST \ + -H 'Content-Type: application/json' \ + -H @/tmp/headers \ + -d @{nix.create_runner_payload_file} \ + http://gitlab/api/v4/user/runners + """ + )[1] + obj = json.loads(resp) + r.id = obj["id"] + r.token = obj["token"] + + # Push the token to the runner machine. + print("==> Push runner token to machine.") + tokenF = Path(out_dir, f"token-{r.name}.env") + with open("${runnerTokenTmpl}", "r") as f: + tokenData = Template(f.read()).substitute({"token": r.token}) + with open(tokenF, "w") as w: + w.write(tokenData) + vms.gitlab_runner.copy_from_host(str(tokenF), r.tokenFile) + + +def restart_gitlab_runner_service(): + print("==> Restart Gitlab Runner") + vms.gitlab_runner.systemctl("restart gitlab-runner.service") + vms.gitlab_runner.wait_for_unit("gitlab-runner.service") + + +def test_runner_registered(r: Runner): + """ + Test that the runner `r` is registered in Gitlab and its status is active. + """ + + print("==> Check that runner '{r.name}' is registered.") + + resp = vms.gitlab.execute( + f""" + curl -s -X GET \ + -H 'Content-Type: application/json' \ + -H @/tmp/headers \ + http://gitlab/api/v4/runners/{r.id}""" + )[1] + runnerStatus = json.loads(resp) + + if not runnerStatus["active"]: + raise Exception( + f"Runner '{r.name}' [id: '{r.id}'] status is not active: {resp}" + )