diff --git a/files/forgejo/forgejo-runner-2.0.service b/files/forgejo/forgejo-runner-2.0.service new file mode 100644 index 000000000..ae96f4661 --- /dev/null +++ b/files/forgejo/forgejo-runner-2.0.service @@ -0,0 +1,16 @@ +[Unit] +Description=forgejo-runner as %i +After=network.target + +[Service] +Type=simple +TimeoutStartSec=0 +TimeoutStopSec=30 +Restart=always +RestartSec=10 +Environment="INT=%i" +ExecStart=/opt/forgejo-runner-orchestrator/libexec/runner-orchestrator +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/files/forgejo/runner-2.0.config b/files/forgejo/runner-2.0.config new file mode 100644 index 000000000..1680db0e9 --- /dev/null +++ b/files/forgejo/runner-2.0.config @@ -0,0 +1,194 @@ +# Example configuration file, it's safe to copy this as the default config file without any modification. + +# You don't have to copy this file to your instance, +# just run `forgejo-runner generate-config > config.yaml` to generate a config file. + +# +# The value of level or job_level can be trace, debug, info, warn, error or fatal +# +log: + # + # What is displayed in the output of the runner process but not sent + # to the Forgejo instance. + # + level: info + # + # What is sent to the Forgejo instance and therefore + # visible in the web UI for a given job. + # + job_level: info + +runner: + # Where to store the registration result. + file: /home/ubuntu/.runner + # Execute how many tasks concurrently at the same time. + capacity: 1 + # Extra environment variables to run jobs. + envs: + A_TEST_ENV_NAME_1: a_test_env_value_1 + A_TEST_ENV_NAME_2: a_test_env_value_2 + # Extra environment variables to run jobs from a file. + # It will be ignored if it's empty or the file doesn't exist. + env_file: .env + # The timeout for a job to be finished. + # Please note that the Forgejo instance also has a timeout (3h by default) for the job. + # So the job could be stopped by the Forgejo instance if it's timeout is shorter than this. + timeout: 3h + # The timeout for the runner to wait for running jobs to finish when + # shutting down because a TERM or INT signal has been received. Any + # running jobs that haven't finished after this timeout will be + # cancelled. + # If unset or zero the jobs will be cancelled immediately. + shutdown_timeout: 3h + # Whether skip verifying the TLS certificate of the instance. + insecure: false + # The timeout for fetching the job from the Forgejo instance. + fetch_timeout: 5s + # The interval for fetching the job from the Forgejo instance. + fetch_interval: 2s + # The interval for reporting the job status and logs to the Forgejo instance. + report_interval: 1s + # The labels of a runner are used to determine which jobs the runner can run, and how to run them. + # Like: ["macos-arm64:host", "ubuntu-latest:docker://node:20-bookworm", "ubuntu-22.04:docker://node:20-bookworm"] + # If it's empty when registering, it will ask for inputting labels. + # If it's empty when executing the `daemon`, it will use labels in the `.runner` file. + labels: + - "ubuntu24:host" + - "docker" + +cache: + # + # When enabled, workflows will be given the ACTIONS_CACHE_URL environment variable + # used by the https://code.forgejo.org/actions/cache action. The server at this + # URL must implement a compliant REST API and it must also be reachable from + # the container or host running the workflows. + # + # See also https://forgejo.org/docs/next/user/actions/advanced-features/#cache + # + # When it is not enabled, none of the following options apply. + # + # It works as follows: + # + # - the workflow is given a one time use ACTIONS_CACHE_URL + # - a cache proxy listens to ACTIONS_CACHE_URL + # - the cache proxy securely communicates with the cache server using + # a shared secret + # + enabled: true + # + ####################################################################### + # + # Only used for the internal cache server. + # + # If external_server is not set, the Forgejo runner will spawn a + # cache server that will be used by the cache proxy. + # + ####################################################################### + # + # The port bound by the internal cache server. + # 0 means to use a random available port. + # + port: 0 + # + # The directory to store the cache data. + # + # If empty, the cache data will be stored in $HOME/.cache/actcache. + # + dir: "" + # + ####################################################################### + # + # Only used for the external cache server. + # + # If external_server is set, the internal cache server is not + # spawned. + # + ####################################################################### + # + # The URL of the cache server. The URL should generally end with + # "/". The cache proxy will forward requests to the external + # server. The requests are authenticated with the "secret" that is + # shared with the external server. + # + external_server: "" + # + # The shared cache secret used to secure the communications between + # the cache proxy and the cache server. + # + # If empty, it will be generated to a new secret automatically when + # the server starts and it will stay the same until it restarts. + # + secret: "" + # + ####################################################################### + # + # Common to the internal and external cache server + # + ####################################################################### + # + # The IP or hostname (195.84.20.30 or example.com) to use when constructing + # ACTIONS_CACHE_URL which is the URL of the cache proxy. + # + # If empty it will be detected automatically. + # + # If the containers or host running the workflows reside on a + # different network than the Forgejo runner (for instance when the + # docker server used to create containers is not running on the same + # host as the Forgejo runner), it may be impossible to figure that + # out automatically. In that case you can specify which IP or + # hostname to use to reach the internal cache server created by the + # Forgejo runner. + # + host: "" + # + # The port bound by the internal cache proxy. + # 0 means to use a random available port. + # + proxy_port: 0 + # + # Overrides the ACTIONS_CACHE_URL variable passed to workflow + # containers. The URL should generally not end with "/". This should only + # be used if the runner host is not reachable from the workflow containers, + # and requires further setup. + # + actions_cache_url_override: "" + +container: + # Specifies the network to which the container will connect. + # Could be host, bridge or the name of a custom network. + # If it's empty, create a network automatically. + network: "" + # Whether to create networks with IPv6 enabled. Requires the Docker daemon to be set up accordingly. + # Only takes effect if "network" is set to "". + enable_ipv6: true + # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker). + privileged: true + # And other options to be used when the container is started (eg, --volume /etc/ssl/certs:/etc/ssl/certs:ro). + options: + # The parent directory of a job's working directory. + # If it's empty, /workspace will be used. + workdir_parent: + # Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob + # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted. + # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to: + # valid_volumes: + # - data + # - /etc/ssl/certs + # If you want to allow any volume, please use the following configuration: + # valid_volumes: + # - '**' + valid_volumes: [] + # overrides the docker client host with the specified one. + # If "-" or "", an available docker host will automatically be found. + # If "automount", an available docker host will automatically be found and mounted in the job container (e.g. /var/run/docker.sock). + # Otherwise the specified docker host will be used and an error will be returned if it doesn't work. + docker_host: "automount" + # Pull docker image(s) even if already present + force_pull: false + # Rebuild local docker image(s) even if already present + force_rebuild: false + +host: + # The parent directory of a job's working directory. + # If it's empty, $HOME/.cache/act/ will be used. + workdir_parent: diff --git a/manifests/forgejo/runner/orchestrator.pp b/manifests/forgejo/runner/orchestrator.pp new file mode 100644 index 000000000..e8f2437bb --- /dev/null +++ b/manifests/forgejo/runner/orchestrator.pp @@ -0,0 +1,104 @@ +# A class to install and manage Forgejo runner(s) +class sunet::forgejo::runner::orchestrator ( + String $version = '12.10.1', + String $version_sha256sum = '9e0378b4a22b95da98b350cfcb1b3844c1929a354a961f1ca7b49122b42abcc1', + Integer $runners = 4, + String $forgejo_instance = 'platform.sunet.se', + String $forgejo_prefix = 'runner', + String $openstack_key_name = 'dirigenten', + String $openstack_network = 'public', + String $dirigenten_version = 'latest', + Optional[String] $runner_image = undef, +) { + + $forgejo_registration_token = lookup('forgejo_registration_token', undef, undef, 'NOT_SET_IN_HIERA'); + + file {'/opt/forgejo-runner-orchestrator': + ensure => 'directory', + } + file {'/opt/forgejo-runner-orchestrator/bin': + ensure => 'directory', + } + + file {'/opt/forgejo-runner-orchestrator/libexec': + ensure => 'directory', + } + + file {'/opt/forgejo-runner-orchestrator/config': + ensure => 'directory', + } + + # Generate SSH-key used to access DB nodes + $key_path = '/opt/forgejo-runner-orchestrator/config/id_ed25519' + if lookup('forgejo_runner_ssh_key', undef, undef, undef) { + ensure_resource('sunet::snippets::secret_file', $key_path, { + hiera_key => 'forgejo_runner_ssh_key', + }) + } else { + if (!find_file($key_path)){ + sunet::snippets::ssh_keygen{$key_path:} # This will not overwrite an existing key + } + } + + file { '/opt/forgejo-runner-orchestrator/config/runner.config': + ensure => 'file', + content => file('sunet/forgejo/runner-2.0.config'), + } + + $clouds = lookup('clouds', undef, undef, {}) + file { '/opt/forgejo-runner-orchestrator/config/clouds.yaml': + ensure => file, + content => to_yaml({'clouds' => $clouds}), + } + + + file { "/opt/forgejo-runner-orchestrator/bin/forgejo-runner-${version}": + ensure => 'file', + source => "https://code.forgejo.org/forgejo/runner/releases/download/v${version}/forgejo-runner-${version}-linux-amd64", + checksum => 'sha256', + checksum_value => $version_sha256sum, + mode => '0755', + } + file { '/opt/forgejo-runner-orchestrator/bin/forgejo-runner': + ensure => link, + target => "/opt/forgejo-runner-orchestrator/bin/forgejo-runner-${version}", + } + + file { '/opt/forgejo-runner-orchestrator/libexec/runner-orchestrator': + ensure => 'file', + content => template('sunet/forgejo/runner-orchestrator.erb'), + mode => '0755', + } + + file { '/opt/forgejo-runner-orchestrator/libexec/runner-registration': + ensure => 'file', + content => template('sunet/forgejo/runner-registration.erb'), + mode => '0755', + } + + file { '/opt/forgejo-runner-orchestrator/libexec/runner-wrapper': + ensure => 'file', + content => template('sunet/forgejo/runner-wrapper.2.0.erb'), + mode => '0755', + } + + file { '/usr/local/bin/runnerctl': + ensure => 'file', + content => template('sunet/forgejo/runnerctl.erb'), + mode => '0755', + } + + file { '/etc/systemd/system/sunet-forgejo-runner-orchestrator@.service': + ensure => 'file', + content => file('sunet/forgejo/forgejo-runner-2.0.service'), + mode => '0644', + } + + range(0, $runners - 1).each |$runner|{ + + service { "sunet-forgejo-runner-orchestrator@runner-${runner}": + ensure => 'running', + enable => true, + } + } +} diff --git a/templates/forgejo/runner-orchestrator.erb b/templates/forgejo/runner-orchestrator.erb new file mode 100644 index 000000000..a6461f496 --- /dev/null +++ b/templates/forgejo/runner-orchestrator.erb @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -eu + +RUNNER=/opt/forgejo-runner-orchestrator/bin/forgejo-runner +ME=$(hostname -f) + +docker run --rm \ + --name "dirigent-${INT}" \ + -v /opt/forgejo-runner-orchestrator/config/runner.config:/runner.config:ro \ + -v /opt/forgejo-runner-orchestrator/libexec/runner-wrapper:/wrapper:ro \ + -v /opt/forgejo-runner-orchestrator/config/clouds.yaml:/etc/openstack/clouds.yaml:ro \ + -v /opt/forgejo-runner-orchestrator/config/id_ed25519:/id_ed25519:ro \ + -v /opt/forgejo-runner-orchestrator/libexec/runner-registration:/runner-registration:ro \ + -v $(realpath /opt/forgejo-runner-orchestrator/bin/forgejo-runner):/forgejo-runner:ro \ + -e DIRIGENTEN_HOSTNAME=${ME} \ +platform.sunet.se/platform/dirigenten:<%= @dirigenten_version %> \ +--network <%= @openstack_network %> \ +--prefix <%= @forgejo_prefix %>-${INT} \ +--key-name <%= @openstack_key_name %> \ +--pre-exec /runner-registration \ +--ssh-key /id_ed25519 \ +--ssh-host $(hostname -f) \ +--scp /wrapper:/home/ubuntu/wrapper \ +--scp /runner.config:/home/ubuntu/runner.config \ +--scp /tmp/uuid:/tmp/non-secret-uuid.txt \ +--scp /tmp/token:/tmp/secret-token.txt \ +--scp /forgejo-runner:/home/ubuntu/forgejo-runner \ +<% if @runner_image -%>--image <%= @runner_image %> \<%- end -%> +/home/ubuntu/wrapper diff --git a/templates/forgejo/runner-registration.erb b/templates/forgejo/runner-registration.erb new file mode 100755 index 000000000..3ace3e99d --- /dev/null +++ b/templates/forgejo/runner-registration.erb @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +if [[ ! -f /usr/bin/curl ]]; then + apt-get update + apt-get -y install \ + curl \ + jq +fi + +# Clean up if anything would be left from previous run +for file in uuid token; do + file_path="/tmp/${file}" + if [[ -f "${file_path}" ]]; then + rm "${file_path}" + fi +done + +response=$(curl -f -X 'POST' \ + 'https://<%= @forgejo_instance %>/api/v1/admin/actions/runners' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer <%= @forgejo_registration_token %>' \ + -H 'Content-Type: application/json' \ + -d "{ + \"name\": \"${DIRIGENTEN_HOSTNAME}-${DIRIGENTEN_PREFIX}-$(date +%s)\", + \"ephemeral\": true +}") + +for key in uuid token; do + echo "${response}" | jq -re --arg k "$key" '.[$k]' > "/tmp/${key}" +done diff --git a/templates/forgejo/runner-wrapper.2.0.erb b/templates/forgejo/runner-wrapper.2.0.erb new file mode 100755 index 000000000..61561cf72 --- /dev/null +++ b/templates/forgejo/runner-wrapper.2.0.erb @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +export RUNNER_TOOL_CACHE=/opt/hostedtoolcache + +RUNNER=/home/ubuntu/forgejo-runner +${RUNNER} --config ./runner.config one-job \ + --url "https://<%= @forgejo_instance %>" \ + --uuid "$(cat /tmp/non-secret-uuid.txt)" \ + --token-url file:///tmp/secret-token.txt \ + --wait + +echo "Exiting to destroy VM - BOOM" diff --git a/templates/forgejo/runnerctl.erb b/templates/forgejo/runnerctl.erb new file mode 100644 index 000000000..27cfc1089 --- /dev/null +++ b/templates/forgejo/runnerctl.erb @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 ssh PREFIX" + exit 1 +} + +COMMAND="${1:-}" + +case "$COMMAND" in + ssh) + PREFIX="${2:-}" + [[ -z "$PREFIX" ]] && usage + IP=$(docker run -v /opt/forgejo-runner-orchestrator/config/clouds.yaml:/etc/openstack/clouds.yaml:ro --rm --entrypoint "uv" platform.sunet.se/platform/dirigenten:<%= @dirigenten_version %> run /dirigenten/ssh-runner --prefix runner-"$PREFIX" --list-ip) + if [[ -z "$IP" ]]; then + echo "Error: no IP found for runner-'$PREFIX'." >&2 + exit 1 + fi + exec ssh -o StrictHostKeyChecking=no -i /opt/forgejo-runner-orchestrator/config/id_ed25519 -l ubuntu "$IP" + ;; + *) + usage + ;; +esac +