From e59a832d5d9cccde3b8150b2dbb6561e8bb42ad9 Mon Sep 17 00:00:00 2001 From: Alexandre Pires Date: Tue, 30 Jun 2026 20:31:12 +0200 Subject: [PATCH] Added linux gitea runners --- .../files/dockerfiles/Dockerfile.cicd-base | 18 + .../files/dockerfiles/Dockerfile.cpp-build | 20 + .../dockerfiles/Dockerfile.gitea-runner-base | 13 + ansible/files/dockerfiles/Dockerfile.go-build | 19 + .../files/dockerfiles/Dockerfile.image-build | 32 ++ .../files/dockerfiles/Dockerfile.infra-tools | 28 ++ .../dockerfiles/Dockerfile.security-scan | 25 + ansible/host_vars/ci-01.lab.alexpires.me.yml | 65 ++- ansible/host_vars/gpu-01.lab.alexpires.me.yml | 3 +- ansible/playbook_windows_gitea_runner.yml | 6 + .../windows_gitea_runner/defaults/main.yml | 30 ++ .../roles/windows_gitea_runner/meta/main.yml | 14 + .../roles/windows_gitea_runner/tasks/main.yml | 156 +++++++ .../windows_gitea_runner/tasks/runner.yml | 430 ++++++++++++++++++ .../templates/config.yaml.j2 | 11 + ansible/sectool.env | 1 + inventory/hosts | 3 + terraform/iac/scaleway/containers.tf | 5 + 18 files changed, 875 insertions(+), 4 deletions(-) create mode 100644 ansible/files/dockerfiles/Dockerfile.cicd-base create mode 100644 ansible/files/dockerfiles/Dockerfile.cpp-build create mode 100644 ansible/files/dockerfiles/Dockerfile.gitea-runner-base create mode 100644 ansible/files/dockerfiles/Dockerfile.go-build create mode 100644 ansible/files/dockerfiles/Dockerfile.image-build create mode 100644 ansible/files/dockerfiles/Dockerfile.infra-tools create mode 100644 ansible/files/dockerfiles/Dockerfile.security-scan create mode 100644 ansible/playbook_windows_gitea_runner.yml create mode 100644 ansible/roles/windows_gitea_runner/defaults/main.yml create mode 100644 ansible/roles/windows_gitea_runner/meta/main.yml create mode 100644 ansible/roles/windows_gitea_runner/tasks/main.yml create mode 100644 ansible/roles/windows_gitea_runner/tasks/runner.yml create mode 100644 ansible/roles/windows_gitea_runner/templates/config.yaml.j2 create mode 100644 terraform/iac/scaleway/containers.tf diff --git a/ansible/files/dockerfiles/Dockerfile.cicd-base b/ansible/files/dockerfiles/Dockerfile.cicd-base new file mode 100644 index 0000000..bc4fba1 --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.cicd-base @@ -0,0 +1,18 @@ +FROM local/gitea-runner-base:latest + +# cicd-base runner profile: common CI build and release tooling. +USER root +RUN apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + git \ + jq \ + make \ + openssh-client \ + python3 \ + py3-pip \ + rsync \ + unzip \ + yq \ + zip \ No newline at end of file diff --git a/ansible/files/dockerfiles/Dockerfile.cpp-build b/ansible/files/dockerfiles/Dockerfile.cpp-build new file mode 100644 index 0000000..2d8224b --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.cpp-build @@ -0,0 +1,20 @@ +FROM local/gitea-runner-base:latest + +# cpp-build runner profile: dedicated C/C++ build environment. +USER root +RUN apk add --no-cache \ + bash \ + build-base \ + ca-certificates \ + cmake \ + curl \ + git \ + jq \ + make \ + openssh-client \ + python3 \ + py3-pip \ + rsync \ + unzip \ + yq \ + zip diff --git a/ansible/files/dockerfiles/Dockerfile.gitea-runner-base b/ansible/files/dockerfiles/Dockerfile.gitea-runner-base new file mode 100644 index 0000000..56666ab --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.gitea-runner-base @@ -0,0 +1,13 @@ +FROM docker.io/gitea/runner:latest + +USER root + +RUN apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + git \ + jq \ + openssh-client \ + unzip \ + zip \ No newline at end of file diff --git a/ansible/files/dockerfiles/Dockerfile.go-build b/ansible/files/dockerfiles/Dockerfile.go-build new file mode 100644 index 0000000..d689c3e --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.go-build @@ -0,0 +1,19 @@ +FROM local/gitea-runner-base:latest + +# go-build runner profile: dedicated Go build environment. +USER root +RUN apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + git \ + go \ + jq \ + make \ + openssh-client \ + python3 \ + py3-pip \ + rsync \ + unzip \ + yq \ + zip diff --git a/ansible/files/dockerfiles/Dockerfile.image-build b/ansible/files/dockerfiles/Dockerfile.image-build new file mode 100644 index 0000000..704c40b --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.image-build @@ -0,0 +1,32 @@ +FROM local/gitea-runner-base:latest + +# image-build runner profile: dedicated environment for container image builds. +USER root +RUN apk add --no-cache \ + bash \ + buildah \ + ca-certificates \ + curl \ + fuse-overlayfs \ + git \ + iptables \ + jq \ + make \ + openssh-client \ + podman \ + python3 \ + py3-pip \ + rsync \ + shadow-uidmap \ + skopeo \ + slirp4netns \ + unzip \ + yq \ + zip + +RUN mkdir -p /etc/containers /var/lib/containers /run/containers && \ + printf '%s\n' '[storage]' 'driver = "vfs"' > /etc/containers/storage.conf && \ + printf '%s\n' '[engine]' > /etc/containers/containers.conf + +ENV BUILDAH_ISOLATION=chroot +ENV STORAGE_DRIVER=vfs diff --git a/ansible/files/dockerfiles/Dockerfile.infra-tools b/ansible/files/dockerfiles/Dockerfile.infra-tools new file mode 100644 index 0000000..6b09550 --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.infra-tools @@ -0,0 +1,28 @@ +FROM local/gitea-runner-base:latest + +# infra-tools runner profile: Ansible and infrastructure automation tools. +USER root +RUN apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + git \ + jq \ + make \ + openssh-client \ + python3 \ + py3-pip \ + rsync \ + unzip \ + yq \ + zip && \ + python3 -m venv /opt/infra-venv && \ + /opt/infra-venv/bin/pip install --no-cache-dir \ + ansible \ + ansible-lint \ + bcrypt \ + fabric \ + passlib \ + pywinrm + +ENV PATH="/opt/infra-venv/bin:${PATH}" \ No newline at end of file diff --git a/ansible/files/dockerfiles/Dockerfile.security-scan b/ansible/files/dockerfiles/Dockerfile.security-scan new file mode 100644 index 0000000..59aad5b --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.security-scan @@ -0,0 +1,25 @@ +FROM local/gitea-runner-base:latest + +# security-scan runner profile: image and dependency scanning tooling. +USER root +RUN apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + git \ + jq \ + make \ + openssh-client \ + python3 \ + py3-pip \ + rsync \ + unzip \ + yq \ + zip + +ARG TRIVY_VERSION=0.72.0 +RUN curl -fsSL -o /tmp/trivy.tar.gz \ + "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" && \ + tar -xzf /tmp/trivy.tar.gz -C /tmp && \ + install -m 0755 /tmp/trivy /usr/local/bin/trivy && \ + rm -f /tmp/trivy.tar.gz /tmp/trivy \ No newline at end of file diff --git a/ansible/host_vars/ci-01.lab.alexpires.me.yml b/ansible/host_vars/ci-01.lab.alexpires.me.yml index b9d47ba..2d8c7e9 100644 --- a/ansible/host_vars/ci-01.lab.alexpires.me.yml +++ b/ansible/host_vars/ci-01.lab.alexpires.me.yml @@ -87,7 +87,7 @@ llama_server_args_extra: "-np", 1, "-c", - 8192, + 4092, "-b", 1024, "-ub", @@ -99,10 +99,71 @@ llama_server_args_extra: "-ctv", "q4_0", "-cram", - 0, + 2048, "-fit", "off", "--mmap", ] windows_containers_service_password: "{{ lookup('env', 'WIN_CI_01_PASSWORD') }}" +windows_gitea_runner_instance_url: "https://code.alexpires.me" +windows_gitea_runner_registration_token: "{{ lookup('env', 'GITEA_RUNNER_REGISTRATION_TOKEN') }}" +windows_gitea_runner_build_root: "D:\\GiteaRunner\\builds" +windows_gitea_runner_runners: + - profile: cicd-base + name: "gitea-runner-cicd-base" + container_name: "gitea-runner-cicd-base" + image: "local/gitea-runner:cicd-base" + dockerfile_src: "Dockerfile.cicd-base" + labels: "cicd-base:host" + config_file: "D:\\GiteaRunner\\cicd-base\\config.yaml" + data_dir: "D:\\GiteaRunner\\cicd-base\\data" + log_dir: "D:\\Logs\\GiteaRunner\\cicd-base" + - profile: infra-tools + name: "gitea-runner-infra-tools" + container_name: "gitea-runner-infra-tools" + image: "local/gitea-runner:infra-tools" + dockerfile_src: "Dockerfile.infra-tools" + labels: "infra-tools:host" + config_file: "D:\\GiteaRunner\\infra-tools\\config.yaml" + data_dir: "D:\\GiteaRunner\\infra-tools\\data" + log_dir: "D:\\Logs\\GiteaRunner\\infra-tools" + - profile: security-scan + name: "gitea-runner-security-scan" + container_name: "gitea-runner-security-scan" + image: "local/gitea-runner:security-scan" + dockerfile_src: "Dockerfile.security-scan" + labels: "security-scan:host" + config_file: "D:\\GiteaRunner\\security-scan\\config.yaml" + data_dir: "D:\\GiteaRunner\\security-scan\\data" + log_dir: "D:\\Logs\\GiteaRunner\\security-scan" + - profile: cpp-build + name: "gitea-runner-cpp-build" + container_name: "gitea-runner-cpp-build" + image: "local/gitea-runner:cpp-build" + dockerfile_src: "Dockerfile.cpp-build" + labels: "cpp-build:host" + config_file: "D:\\GiteaRunner\\cpp-build\\config.yaml" + data_dir: "D:\\GiteaRunner\\cpp-build\\data" + log_dir: "D:\\Logs\\GiteaRunner\\cpp-build" + - profile: go-build + name: "gitea-runner-go-build" + container_name: "gitea-runner-go-build" + image: "local/gitea-runner:go-build" + dockerfile_src: "Dockerfile.go-build" + labels: "go-build:host" + config_file: "D:\\GiteaRunner\\go-build\\config.yaml" + data_dir: "D:\\GiteaRunner\\go-build\\data" + log_dir: "D:\\Logs\\GiteaRunner\\go-build" + - profile: image-build + name: "gitea-runner-image-build" + container_name: "gitea-runner-image-build" + image: "local/gitea-runner:image-build" + dockerfile_src: "Dockerfile.image-build" + labels: "image-build:host" + config_file: "D:\\GiteaRunner\\image-build\\config.yaml" + data_dir: "D:\\GiteaRunner\\image-build\\data" + log_dir: "D:\\Logs\\GiteaRunner\\image-build" + run_args: + - "--privileged" + enable_nested_build: true diff --git a/ansible/host_vars/gpu-01.lab.alexpires.me.yml b/ansible/host_vars/gpu-01.lab.alexpires.me.yml index 16e3a2f..4a8ed8b 100644 --- a/ansible/host_vars/gpu-01.lab.alexpires.me.yml +++ b/ansible/host_vars/gpu-01.lab.alexpires.me.yml @@ -267,9 +267,8 @@ llama_server_argv_extra: auto_suspend_enabled: true auto_suspend_check_interval: 1min -auto_suspend_idle_timeout_cycles: 2 +auto_suspend_idle_timeout_cycles: 10 auto_suspend_cpu_idle_threshold: 90.0 -auto_suspend_gpu_util_threshold: 10.0 # # Optional global model preset options applied under [*] in models.ini. # # Keys map to llama.cpp CLI args without leading dashes, short args, or LLAMA_ARG_* env names. # llama_server_preset_global: diff --git a/ansible/playbook_windows_gitea_runner.yml b/ansible/playbook_windows_gitea_runner.yml new file mode 100644 index 0000000..767f658 --- /dev/null +++ b/ansible/playbook_windows_gitea_runner.yml @@ -0,0 +1,6 @@ +--- +- name: Setup Gitea Runner on Windows (Podman) + hosts: windows_gitea_runner + + roles: + - windows_gitea_runner diff --git a/ansible/roles/windows_gitea_runner/defaults/main.yml b/ansible/roles/windows_gitea_runner/defaults/main.yml new file mode 100644 index 0000000..99bbb69 --- /dev/null +++ b/ansible/roles/windows_gitea_runner/defaults/main.yml @@ -0,0 +1,30 @@ +--- +windows_gitea_runner_instance_url: "" +windows_gitea_runner_registration_token: "" + +windows_gitea_runner_runners: [] +windows_gitea_runner_base_image: "local/gitea-runner-base:latest" +windows_gitea_runner_base_dockerfile_src: "Dockerfile.gitea-runner-base" +windows_gitea_runner_base_image_hash_label: "io.a13labs.gitea_runner.base_dockerfile_hash" +windows_gitea_runner_image_prefix: "local/gitea-runner" +windows_gitea_runner_dockerfile_source_root: "{{ playbook_dir }}/files/dockerfiles" +windows_gitea_runner_build_root: "C:\\ProgramData\\GiteaRunner\\builds" +windows_gitea_runner_base_build_dir: "{{ windows_gitea_runner_build_root }}\\base" +windows_gitea_runner_config_root: "C:\\ProgramData\\GiteaRunner\\config" +windows_gitea_runner_data_root: "C:\\ProgramData\\GiteaRunner\\data" +windows_gitea_runner_log_root: "C:\\Logs\\GiteaRunner" +windows_gitea_runner_container_run_args: [] +windows_gitea_runner_container_enable_nested_build: false +windows_gitea_runner_container_storage_root: "{{ windows_gitea_runner_data_root }}\\containers" +windows_gitea_runner_container_log_tail: 120 + +windows_gitea_runner_podman_machine_name: >- + {{ windows_containers_machine_name | default('podman-machine-default') }} +windows_gitea_runner_podman_install_dir: >- + {{ windows_containers_install_dir | default('C:\\Program Files\\RedHat\\Podman') }} +windows_gitea_runner_podman_service_user: "{{ windows_containers_service_user | default('podman') }}" +windows_gitea_runner_podman_service_name: "PodmanAutoStart" + +windows_gitea_runner_restart_policy: "always" +windows_gitea_runner_image_hash_label: "io.a13labs.gitea_runner.dockerfile_hash" +windows_gitea_runner_container_hash_label: "io.a13labs.gitea_runner.config_hash" diff --git a/ansible/roles/windows_gitea_runner/meta/main.yml b/ansible/roles/windows_gitea_runner/meta/main.yml new file mode 100644 index 0000000..abdf426 --- /dev/null +++ b/ansible/roles/windows_gitea_runner/meta/main.yml @@ -0,0 +1,14 @@ +--- +galaxy_info: + author: Alexandre Pires + description: Run a persistent Gitea runner container on Podman on Windows + company: A13Labs + role_name: windows_gitea_runner + namespace: a13labs + license: MIT + min_ansible_version: "2.1" + platforms: + - name: Windows + +# dependencies: +# - role: windows_containers diff --git a/ansible/roles/windows_gitea_runner/tasks/main.yml b/ansible/roles/windows_gitea_runner/tasks/main.yml new file mode 100644 index 0000000..93149f9 --- /dev/null +++ b/ansible/roles/windows_gitea_runner/tasks/main.yml @@ -0,0 +1,156 @@ +--- +- name: Validate required runner settings + ansible.builtin.assert: + that: + - windows_gitea_runner_instance_url | length > 0 + - windows_gitea_runner_registration_token | length > 0 + - windows_gitea_runner_runners | length > 0 + fail_msg: >- + windows_gitea_runner_instance_url, windows_gitea_runner_registration_token, + and at least one windows_gitea_runner_runners entry are required. + +- name: Build effective runner matrix + ansible.builtin.set_fact: + __windows_gitea_runner_effective_runners: "{{ windows_gitea_runner_runners }}" + +- name: Ensure Podman auto-start service is running + ansible.windows.win_service: + name: "{{ windows_gitea_runner_podman_service_name }}" + start_mode: auto + state: started + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + +- name: Wait until Podman machine is ready + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $ready = $false + for ($i = 0; $i -lt 90; $i++) { + $null = & "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" info --format json 2>$null + if ($LASTEXITCODE -eq 0) { + $ready = $true + break + } + Start-Sleep -Seconds 2 + } + if (-not $ready) { + Write-Error "Podman machine {{ windows_gitea_runner_podman_machine_name }} did not become ready" + exit 1 + } + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + changed_when: false + +- name: Ensure base build directory exists + ansible.windows.win_file: + path: "{{ windows_gitea_runner_base_build_dir }}" + state: directory + +- name: Copy base Dockerfile to build context + ansible.windows.win_copy: + src: "{{ windows_gitea_runner_dockerfile_source_root }}/{{ windows_gitea_runner_base_dockerfile_src }}" + dest: "{{ windows_gitea_runner_base_build_dir }}\\Dockerfile" + +- name: Grant podman service user access to base build directory + ansible.windows.win_acl: + path: "{{ windows_gitea_runner_base_build_dir }}" + user: ".\\{{ windows_gitea_runner_podman_service_user }}" + rights: FullControl + type: allow + state: present + inherit: ContainerInherit, ObjectInherit + propagation: None + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + +- name: Read current base image hash label + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + $image = "{{ windows_gitea_runner_base_image }}" + + $null = & $podman image exists $image 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Output "__MISSING__" + exit 0 + } + + $raw = & $podman image inspect $image --format json 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) { + Write-Output "__MISSING__" + exit 0 + } + + $obj = $raw | ConvertFrom-Json + if ($obj -is [System.Array]) { + $obj = $obj[0] + } + + $labels = $obj.Config.Labels + if ($null -eq $labels) { + Write-Output "__UNLABELED__" + exit 0 + } + + $key = "{{ windows_gitea_runner_base_image_hash_label }}" + $current = $labels.$key + if ([string]::IsNullOrWhiteSpace($current)) { + Write-Output "__UNLABELED__" + exit 0 + } + + Write-Output $current.Trim() + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_current_base_image_hash + changed_when: false + +- name: Read desired base Dockerfile checksum + ansible.builtin.set_fact: + __windows_gitea_runner_base_dockerfile_checksum: >- + {{ + lookup( + 'file', + windows_gitea_runner_dockerfile_source_root ~ '/' ~ windows_gitea_runner_base_dockerfile_src + ) | hash('sha1') + }} + +- name: Determine if base image needs rebuild + ansible.builtin.set_fact: + __windows_gitea_runner_needs_base_image_rebuild: >- + {{ + (__windows_gitea_runner_current_base_image_hash.stdout | trim) + != (__windows_gitea_runner_base_dockerfile_checksum | trim) + }} + +- name: Rebuild base image when Dockerfile changed + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + & $podman build ` + --label "{{ windows_gitea_runner_base_image_hash_label }}={{ __windows_gitea_runner_base_dockerfile_checksum }}" ` + --tag "{{ windows_gitea_runner_base_image }}" ` + --file "{{ windows_gitea_runner_base_build_dir }}\\Dockerfile" ` + "{{ windows_gitea_runner_base_build_dir }}" + if ($LASTEXITCODE -ne 0) { + exit 1 + } + Write-Output "__BUILT__" + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_base_build + changed_when: "'__BUILT__' in __windows_gitea_runner_base_build.stdout" + when: __windows_gitea_runner_needs_base_image_rebuild + +- name: Reconcile all runners + ansible.builtin.include_tasks: runner.yml + loop: "{{ __windows_gitea_runner_effective_runners }}" + loop_control: + loop_var: windows_gitea_runner_runner + label: >- + {{ windows_gitea_runner_runner.name | default(windows_gitea_runner_runner.profile) }} diff --git a/ansible/roles/windows_gitea_runner/tasks/runner.yml b/ansible/roles/windows_gitea_runner/tasks/runner.yml new file mode 100644 index 0000000..4ad56a4 --- /dev/null +++ b/ansible/roles/windows_gitea_runner/tasks/runner.yml @@ -0,0 +1,430 @@ +--- +- name: Resolve runner profile + ansible.builtin.set_fact: + __windows_gitea_runner_profile: >- + {{ windows_gitea_runner_runner.profile | default(windows_gitea_runner_runner.name | default('runner')) }} + +- name: Resolve runner-specific settings + ansible.builtin.set_fact: + __windows_gitea_runner_name: >- + {{ + windows_gitea_runner_runner.name | default( + 'gitea-runner-' ~ __windows_gitea_runner_profile + ) + }} + __windows_gitea_runner_container_name: >- + {{ + windows_gitea_runner_runner.container_name | default( + 'gitea-runner-' ~ __windows_gitea_runner_profile + ) + }} + __windows_gitea_runner_image: >- + {{ + windows_gitea_runner_runner.image | default( + windows_gitea_runner_image_prefix ~ ':' ~ __windows_gitea_runner_profile + ) + }} + __windows_gitea_runner_labels: >- + {{ + windows_gitea_runner_runner.labels | default( + __windows_gitea_runner_profile ~ ':host' + ) + }} + __windows_gitea_runner_dockerfile_src: >- + {{ + windows_gitea_runner_runner.dockerfile_src | default( + 'Dockerfile.' ~ __windows_gitea_runner_profile + ) + }} + __windows_gitea_runner_config_file: >- + {{ + windows_gitea_runner_runner.config_file | default( + windows_gitea_runner_config_root ~ '\\' ~ __windows_gitea_runner_profile ~ '\\config.yaml' + ) + }} + __windows_gitea_runner_config_dir: >- + {{ + ( + windows_gitea_runner_runner.config_file | default( + windows_gitea_runner_config_root ~ '\\' ~ __windows_gitea_runner_profile ~ '\\config.yaml' + ) + ) | regex_replace('\\\\[^\\\\]+$', '') + }} + __windows_gitea_runner_config_name: >- + {{ + ( + windows_gitea_runner_runner.config_file | default( + windows_gitea_runner_config_root ~ '\\' ~ __windows_gitea_runner_profile ~ '\\config.yaml' + ) + ) | regex_search('[^\\\\]+$') + }} + __windows_gitea_runner_data_dir: >- + {{ + windows_gitea_runner_runner.data_dir | default( + windows_gitea_runner_data_root ~ '\\' ~ __windows_gitea_runner_profile ~ '\\data' + ) + }} + __windows_gitea_runner_log_dir: >- + {{ + windows_gitea_runner_runner.log_dir | default( + windows_gitea_runner_log_root ~ '\\' ~ __windows_gitea_runner_profile + ) + }} + __windows_gitea_runner_build_dir: >- + {{ windows_gitea_runner_build_root ~ '\\' ~ __windows_gitea_runner_profile }} + __windows_gitea_runner_build_dockerfile_dest: >- + {{ windows_gitea_runner_build_root ~ '\\' ~ __windows_gitea_runner_profile ~ '\\Dockerfile' }} + __windows_gitea_runner_dockerfile_source_path: >- + {{ + windows_gitea_runner_dockerfile_source_root ~ '/' ~ ( + windows_gitea_runner_runner.dockerfile_src | default( + 'Dockerfile.' ~ __windows_gitea_runner_profile + ) + ) + }} + __windows_gitea_runner_enable_nested_build: >- + {{ + windows_gitea_runner_runner.enable_nested_build + | default(windows_gitea_runner_container_enable_nested_build) + }} + __windows_gitea_runner_storage_dir: >- + {{ + ( + windows_gitea_runner_runner.storage_dir | default( + windows_gitea_runner_container_storage_root ~ '\\' ~ __windows_gitea_runner_profile + ) + ) + if ( + windows_gitea_runner_runner.enable_nested_build + | default(windows_gitea_runner_container_enable_nested_build) + ) + else '' + }} + __windows_gitea_runner_run_args: >- + {{ windows_gitea_runner_runner.run_args | default(windows_gitea_runner_container_run_args) }} + +- name: Resolve runner runtime config path + ansible.builtin.set_fact: + __windows_gitea_runner_runtime_config_file: "{{ __windows_gitea_runner_data_dir }}\\config.yaml" + +- name: Read runner Dockerfile checksum + ansible.builtin.set_fact: + __windows_gitea_runner_base_dockerfile_checksum: >- + {{ + lookup( + 'file', + windows_gitea_runner_dockerfile_source_root ~ '/' ~ windows_gitea_runner_base_dockerfile_src + ) | hash('sha1') + }} + __windows_gitea_runner_profile_dockerfile_checksum: >- + {{ lookup('file', __windows_gitea_runner_dockerfile_source_path) | hash('sha1') }} + +- name: Compute desired runner image hash + ansible.builtin.set_fact: + __windows_gitea_runner_desired_image_hash: >- + {{ { + 'base': __windows_gitea_runner_base_dockerfile_checksum, + 'profile': __windows_gitea_runner_profile_dockerfile_checksum + } | to_json | hash('sha1') }} + +- name: Compute desired runner container hash + ansible.builtin.set_fact: + __windows_gitea_runner_desired_container_hash: >- + {{ { + 'image': __windows_gitea_runner_image, + 'name': __windows_gitea_runner_name, + 'labels': __windows_gitea_runner_labels, + 'instance': windows_gitea_runner_instance_url, + 'config': __windows_gitea_runner_runtime_config_file, + 'config_dir': __windows_gitea_runner_data_dir, + 'config_name': ( + (__windows_gitea_runner_runtime_config_file | regex_search('[^\\\\]+$')) + | default('config.yaml', true) + ), + 'config_mount_mode': 'data', + 'data': __windows_gitea_runner_data_dir, + 'log': __windows_gitea_runner_log_dir, + 'nested_build': __windows_gitea_runner_enable_nested_build, + 'storage': __windows_gitea_runner_storage_dir, + 'run_args': __windows_gitea_runner_run_args, + 'restart': windows_gitea_runner_restart_policy, + 'image_hash': __windows_gitea_runner_desired_image_hash + } | to_json | hash('sha1') }} + +- name: Ensure runner directories exist + ansible.windows.win_file: + path: "{{ item }}" + state: directory + loop: + - "{{ windows_gitea_runner_build_root }}" + - "{{ __windows_gitea_runner_build_dir }}" + - "{{ __windows_gitea_runner_data_dir }}" + - "{{ __windows_gitea_runner_log_dir }}" + - "{{ __windows_gitea_runner_config_dir }}" + +- name: Ensure nested build storage directory exists + ansible.windows.win_file: + path: "{{ __windows_gitea_runner_storage_dir }}" + state: directory + when: __windows_gitea_runner_enable_nested_build + +- name: Grant podman service user access to runner directories + ansible.windows.win_acl: + path: "{{ item }}" + user: ".\\{{ windows_gitea_runner_podman_service_user }}" + rights: FullControl + type: allow + state: present + inherit: ContainerInherit, ObjectInherit + propagation: None + loop: + - "{{ windows_gitea_runner_build_root }}" + - "{{ __windows_gitea_runner_data_dir }}" + - "{{ __windows_gitea_runner_log_dir }}" + - "{{ __windows_gitea_runner_config_dir }}" + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + +- name: Grant podman service user access to nested build storage directory + ansible.windows.win_acl: + path: "{{ __windows_gitea_runner_storage_dir }}" + user: ".\\{{ windows_gitea_runner_podman_service_user }}" + rights: FullControl + type: allow + state: present + inherit: ContainerInherit, ObjectInherit + propagation: None + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __windows_gitea_runner_enable_nested_build + +- name: Render runner config + ansible.windows.win_template: + src: config.yaml.j2 + dest: "{{ __windows_gitea_runner_runtime_config_file }}" + +- name: Copy runner Dockerfile to build context + ansible.windows.win_copy: + src: "{{ __windows_gitea_runner_dockerfile_source_path }}" + dest: "{{ __windows_gitea_runner_build_dockerfile_dest }}" + +- name: Read current runner image hash label + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + $image = "{{ __windows_gitea_runner_image }}" + + $null = & $podman image exists $image 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Output "__MISSING__" + exit 0 + } + + $raw = & $podman image inspect $image --format json 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) { + Write-Output "__MISSING__" + exit 0 + } + + $obj = $raw | ConvertFrom-Json + if ($obj -is [System.Array]) { + $obj = $obj[0] + } + + $labels = $obj.Config.Labels + if ($null -eq $labels) { + Write-Output "__UNLABELED__" + exit 0 + } + + $key = "{{ windows_gitea_runner_image_hash_label }}" + $current = $labels.$key + if ([string]::IsNullOrWhiteSpace($current)) { + Write-Output "__UNLABELED__" + exit 0 + } + + Write-Output $current.Trim() + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_current_image_hash + changed_when: false + +- name: Determine if runner image needs rebuild + ansible.builtin.set_fact: + __windows_gitea_runner_needs_image_rebuild: >- + {{ + (__windows_gitea_runner_current_image_hash.stdout | trim) + != (__windows_gitea_runner_desired_image_hash | trim) + }} + +- name: Rebuild runner image when Dockerfile changed + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + & $podman build ` + --label "{{ windows_gitea_runner_image_hash_label }}={{ __windows_gitea_runner_desired_image_hash }}" ` + --tag "{{ __windows_gitea_runner_image }}" ` + --file "{{ __windows_gitea_runner_build_dockerfile_dest }}" ` + "{{ __windows_gitea_runner_build_dir }}" + if ($LASTEXITCODE -ne 0) { + exit 1 + } + Write-Output "__BUILT__" + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_build + changed_when: "'__BUILT__' in __windows_gitea_runner_build.stdout" + when: __windows_gitea_runner_needs_image_rebuild + +- name: Read current runner container hash label + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $name = "{{ __windows_gitea_runner_container_name }}" + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + + $null = & $podman container exists $name 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Output "__MISSING__" + exit 0 + } + + $raw = & $podman inspect $name --format json 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) { + Write-Output "__MISSING__" + exit 0 + } + + $obj = $raw | ConvertFrom-Json + if ($obj -is [System.Array]) { + $obj = $obj[0] + } + + $labels = $obj.Config.Labels + if ($null -eq $labels) { + Write-Output "__UNLABELED__" + exit 0 + } + + $key = "{{ windows_gitea_runner_container_hash_label }}" + $current = $labels.$key + if ([string]::IsNullOrWhiteSpace($current)) { + Write-Output "__UNLABELED__" + exit 0 + } + + Write-Output $current.Trim() + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_current_hash + changed_when: false + +- name: Determine if runner container needs recreation + ansible.builtin.set_fact: + __windows_gitea_runner_needs_recreate: >- + {{ + (__windows_gitea_runner_current_hash.stdout | trim) + != (__windows_gitea_runner_desired_container_hash | trim) + }} + +- name: Remove stale runner container before recreate + ansible.windows.win_shell: | + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + & $podman rm -f "{{ __windows_gitea_runner_container_name }}" 2>$null + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + when: __windows_gitea_runner_needs_recreate + changed_when: true + failed_when: false + +- name: Ensure runner container is running + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + $name = "{{ __windows_gitea_runner_container_name }}" + $exists = & $podman container exists $name + if ($LASTEXITCODE -eq 0) { + & $podman start $name 2>$null | Out-Null + Write-Output "__STARTED_EXISTING__" + exit 0 + } + + & $podman run -d --name $name --restart "{{ windows_gitea_runner_restart_policy }}" ` + @({{ __windows_gitea_runner_run_args | map('to_json') | join(', ') }}) ` + --label "{{ windows_gitea_runner_container_hash_label }}={{ __windows_gitea_runner_desired_container_hash }}" ` + -v "{{ __windows_gitea_runner_data_dir }}:/data" ` + -e "CONFIG_FILE=/data/config.yaml" ` + -e "GITEA_INSTANCE_URL={{ windows_gitea_runner_instance_url }}" ` + -e "GITEA_RUNNER_REGISTRATION_TOKEN={{ windows_gitea_runner_registration_token }}" ` + -e "GITEA_RUNNER_NAME={{ __windows_gitea_runner_name }}" ` + -e "GITEA_RUNNER_LABELS={{ __windows_gitea_runner_labels }}" ` + {% if __windows_gitea_runner_enable_nested_build %} + -v "{{ __windows_gitea_runner_storage_dir }}:/var/lib/containers" ` + -e "BUILDAH_ISOLATION=chroot" ` + -e "STORAGE_DRIVER=vfs" ` + {% endif %} + "{{ __windows_gitea_runner_image }}" | Out-Null + + if ($LASTEXITCODE -ne 0) { + exit 1 + } + + Write-Output "__CREATED__" + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_container + changed_when: "'__CREATED__' in __windows_gitea_runner_container.stdout" + no_log: true + +- name: Verify runner container state + ansible.windows.win_shell: | + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + & $podman inspect "{{ __windows_gitea_runner_container_name }}" --format "{{ '{{' }}.State.Status{{ '}}' }}" + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_status + changed_when: false + failed_when: (__windows_gitea_runner_status.stdout | trim) != 'running' + +- name: Show tail of runner logs + ansible.windows.win_shell: | + $podman = "{{ windows_gitea_runner_podman_install_dir }}\podman.exe" + & $podman logs --tail {{ windows_gitea_runner_container_log_tail }} "{{ __windows_gitea_runner_container_name }}" + become: true + become_method: ansible.builtin.runas + become_user: "{{ windows_gitea_runner_podman_service_user }}" + register: __windows_gitea_runner_logs + changed_when: false + +- name: Print runner logs + ansible.builtin.debug: + msg: "{{ __windows_gitea_runner_logs.stdout | default('') }}" + +- name: Build normalized runner log text + ansible.builtin.set_fact: + __windows_gitea_runner_logs_all: >- + {{ + [ + __windows_gitea_runner_logs.stdout | default(''), + __windows_gitea_runner_logs.stderr | default('') + ] | join('\n') | lower + }} + +- name: Assert runner did not fail registration + ansible.builtin.assert: + that: + - "'invalid token' not in __windows_gitea_runner_logs_all" + - "'unauthorized' not in __windows_gitea_runner_logs_all" + - "'open config file' not in __windows_gitea_runner_logs_all" + - "'is a directory' not in __windows_gitea_runner_logs_all" + fail_msg: >- + Gitea runner logs indicate a registration/startup failure. Check token, + instance URL, and config mount path. diff --git a/ansible/roles/windows_gitea_runner/templates/config.yaml.j2 b/ansible/roles/windows_gitea_runner/templates/config.yaml.j2 new file mode 100644 index 0000000..02c4c8b --- /dev/null +++ b/ansible/roles/windows_gitea_runner/templates/config.yaml.j2 @@ -0,0 +1,11 @@ +log: + level: info + +runner: + file: /data/.runner + capacity: 1 + envs: {} + +cache: + enabled: true + dir: /data/cache diff --git a/ansible/sectool.env b/ansible/sectool.env index 05dc74f..4eea01a 100644 --- a/ansible/sectool.env +++ b/ansible/sectool.env @@ -11,6 +11,7 @@ KMS_ACCESS_KEY=$KMS_ACCESS_KEY KMS_SECRET_KEY=$KMS_SECRET_KEY KMS_KEY_ARN=$KMS_KEY_ARN GITEA_APP_TOKEN=$GITEA_APP_TOKEN +GITEA_RUNNER_REGISTRATION_TOKEN=$GITEA_RUNNER_REGISTRATION_TOKEN CLOUDNS_AUTH_ID=$CLOUDNS_AUTH_ID CLOUDNS_PASSWORD=$CLOUDNS_PASSWORD WIN_PROVISION_PASSWORD=$WIN_PROVISION_PASSWORD diff --git a/inventory/hosts b/inventory/hosts index b349ecb..634041a 100644 --- a/inventory/hosts +++ b/inventory/hosts @@ -132,3 +132,6 @@ gpu-01.lab.alexpires.me [windows_containers] ci-01.lab.alexpires.me +[windows_gitea_runner] +ci-01.lab.alexpires.me + diff --git a/terraform/iac/scaleway/containers.tf b/terraform/iac/scaleway/containers.tf new file mode 100644 index 0000000..a80bfba --- /dev/null +++ b/terraform/iac/scaleway/containers.tf @@ -0,0 +1,5 @@ +resource "scaleway_registry_namespace" "main" { + name = "main-cr" + description = "Main container registry" + is_public = false +}