diff --git a/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-base b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-base new file mode 100644 index 0000000..fae10ec --- /dev/null +++ b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-base @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/windows/servercore:ltsc2022 + +SHELL ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + +# Base image for Windows Gitea runners with Git for Windows tools and gitea runner. +ARG ACT_RUNNER_VERSION=2.0.0 +RUN Set-StrictMode -Version Latest; \ + $ErrorActionPreference = 'Stop'; \ + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \ + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')); \ + choco config set commandExecutionTimeoutSeconds 14400; \ + choco feature disable -n showDownloadProgress; \ + choco feature enable -n allowGlobalConfirmation; \ + choco install git --yes --no-progress --limit-output; \ + New-Item -ItemType Directory -Path C:\gitea-runner -Force | Out-Null; \ + $runnerUrl = 'https://gitea.com/gitea/runner/releases/download/v{0}/gitea-runner-{0}-windows-amd64.exe' -f $env:ACT_RUNNER_VERSION; \ + Invoke-WebRequest -UseBasicParsing -Uri $runnerUrl -OutFile C:\gitea-runner\act_runner.exe + +COPY ["start-runner.ps1", "C:/gitea-runner/start-runner.ps1"] + +ENV PATH="C:\ProgramData\chocolatey\bin;C:\Program Files\Git\cmd;C:\Program Files\Git\bin;C:\Program Files\Git\usr\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\ContainerAdministrator\AppData\Local\Microsoft\WindowsApps" +ENTRYPOINT ["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\gitea-runner\\start-runner.ps1"] diff --git a/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-cpp b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-cpp new file mode 100644 index 0000000..a9e8ba1 --- /dev/null +++ b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-cpp @@ -0,0 +1,9 @@ +FROM local/gitea-windows-runner:windows-base + +SHELL ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + +# C/C++ profile with cmake and ninja. +RUN choco feature enable -n allowGlobalConfirmation; \ + choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'; \ + choco install ninja; \ + choco clean --yes diff --git a/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-dotnet b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-dotnet new file mode 100644 index 0000000..db7d659 --- /dev/null +++ b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-dotnet @@ -0,0 +1,8 @@ +FROM local/gitea-windows-runner:windows-base + +SHELL ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + +# .NET profile for building and testing managed workloads. +RUN choco feature enable -n allowGlobalConfirmation; \ + choco install dotnet-sdk --version=8.0.204; \ + choco clean --yes diff --git a/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-go b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-go new file mode 100644 index 0000000..2d2ec7c --- /dev/null +++ b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-go @@ -0,0 +1,8 @@ +FROM local/gitea-windows-runner:windows-base + +SHELL ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + +# Go profile for Windows builds. +RUN choco feature enable -n allowGlobalConfirmation; \ + choco install golang --version=1.22.4; \ + choco clean --yes diff --git a/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-msys b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-msys new file mode 100644 index 0000000..35d7b55 --- /dev/null +++ b/ansible/files/dockerfiles/windows-runners/Dockerfile.windows-msys @@ -0,0 +1,7 @@ +FROM local/gitea-windows-runner:windows-base + +SHELL ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"] + +# MSYS-focused profile with common GNU toolchain extras. +RUN choco feature enable -n allowGlobalConfirmation; \ + choco install make msys2 unzip --yes --no-progress --limit-output diff --git a/ansible/files/dockerfiles/windows-runners/start-runner.ps1 b/ansible/files/dockerfiles/windows-runners/start-runner.ps1 new file mode 100644 index 0000000..845c9d3 --- /dev/null +++ b/ansible/files/dockerfiles/windows-runners/start-runner.ps1 @@ -0,0 +1,17 @@ +$ErrorActionPreference = "Stop" +$runner = "C:\gitea-runner\act_runner.exe" +$config = if ($env:CONFIG_FILE) { $env:CONFIG_FILE } else { "C:\runner-data\config.yaml" } +$runnerFile = if ($env:GITEA_RUNNER_FILE) { $env:GITEA_RUNNER_FILE } else { "C:\runner-data\.runner" } + +if (-not (Test-Path -Path $runnerFile) -and $env:GITEA_INSTANCE_URL -and $env:GITEA_RUNNER_REGISTRATION_TOKEN) { + $labels = if ($env:GITEA_RUNNER_LABELS) { $env:GITEA_RUNNER_LABELS } else { "windows:host" } + $name = if ($env:GITEA_RUNNER_NAME) { $env:GITEA_RUNNER_NAME } else { $env:COMPUTERNAME } + + & $runner register --no-interactive --instance $env:GITEA_INSTANCE_URL --token $env:GITEA_RUNNER_REGISTRATION_TOKEN --name $name --labels $labels --config $config + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} + +& $runner daemon --config $config +exit $LASTEXITCODE diff --git a/ansible/host_vars/ci-01.lab.alexpires.me.yml b/ansible/host_vars/ci-01.lab.alexpires.me.yml index 2d8c7e9..a806c40 100644 --- a/ansible/host_vars/ci-01.lab.alexpires.me.yml +++ b/ansible/host_vars/ci-01.lab.alexpires.me.yml @@ -109,6 +109,7 @@ 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_windows_build_root: "D:\\GiteaRunner\\windows-builds" windows_gitea_runner_runners: - profile: cicd-base name: "gitea-runner-cicd-base" @@ -167,3 +168,13 @@ windows_gitea_runner_runners: run_args: - "--privileged" enable_nested_build: true + - profile: windows-msys + mode: windows_mcr + name: "gitea-runner-windows-msys" + container_name: "gitea-runner-windows-msys" + image: "local/gitea-windows-runner:windows-msys" + dockerfile_src: "Dockerfile.windows-msys" + labels: "windows:host,windows-msys" + config_file: "D:\\GiteaRunner\\windows-msys\\config.yaml" + data_dir: "D:\\GiteaRunner\\windows-msys\\data" + log_dir: "D:\\Logs\\GiteaRunner\\windows-msys" diff --git a/ansible/roles/windows_gitea_runner/defaults/main.yml b/ansible/roles/windows_gitea_runner/defaults/main.yml index 99bbb69..882c8d6 100644 --- a/ansible/roles/windows_gitea_runner/defaults/main.yml +++ b/ansible/roles/windows_gitea_runner/defaults/main.yml @@ -3,6 +3,7 @@ windows_gitea_runner_instance_url: "" windows_gitea_runner_registration_token: "" windows_gitea_runner_runners: [] +windows_gitea_runner_default_mode: "linux_podman" 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" @@ -10,9 +11,22 @@ 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_windows_image_prefix: "local/gitea-windows-runner" +windows_gitea_runner_windows_dockerfile_source_root: "{{ playbook_dir }}/files/dockerfiles/windows-runners" +windows_gitea_runner_windows_build_root: "C:\\ProgramData\\GiteaRunner\\windows-builds" +windows_gitea_runner_windows_runtime_bin: "docker" +windows_gitea_runner_windows_runtime_user: "{{ windows_gitea_runner_podman_service_user }}" +windows_gitea_runner_windows_become_user: "{{ windows_gitea_runner_podman_service_user }}" +windows_gitea_runner_windows_base_image: "{{ windows_gitea_runner_windows_image_prefix }}:windows-base" +windows_gitea_runner_windows_base_dockerfile_src: "Dockerfile.windows-base" +windows_gitea_runner_windows_base_entrypoint_script_src: "start-runner.ps1" +windows_gitea_runner_windows_base_build_dir: "{{ windows_gitea_runner_windows_build_root }}\\windows-base" +windows_gitea_runner_windows_base_image_hash_label: "io.a13labs.gitea_runner.windows_base_dockerfile_hash" 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_config_runner_file: "/data/.runner" +windows_gitea_runner_config_cache_dir: "/data/cache" 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" diff --git a/ansible/roles/windows_gitea_runner/meta/main.yml b/ansible/roles/windows_gitea_runner/meta/main.yml index abdf426..07ab209 100644 --- a/ansible/roles/windows_gitea_runner/meta/main.yml +++ b/ansible/roles/windows_gitea_runner/meta/main.yml @@ -10,5 +10,5 @@ galaxy_info: platforms: - name: Windows -# dependencies: -# - role: windows_containers +dependencies: + - role: windows_containers diff --git a/ansible/roles/windows_gitea_runner/tasks/main.yml b/ansible/roles/windows_gitea_runner/tasks/main.yml index 93149f9..3b44e23 100644 --- a/ansible/roles/windows_gitea_runner/tasks/main.yml +++ b/ansible/roles/windows_gitea_runner/tasks/main.yml @@ -8,10 +8,52 @@ fail_msg: >- windows_gitea_runner_instance_url, windows_gitea_runner_registration_token, and at least one windows_gitea_runner_runners entry are required. + tags: + - always - name: Build effective runner matrix ansible.builtin.set_fact: __windows_gitea_runner_effective_runners: "{{ windows_gitea_runner_runners }}" + tags: + - always + +- name: Validate runner execution modes + ansible.builtin.assert: + that: + - >- + ( + windows_gitea_runner_runner.mode + | default(windows_gitea_runner_default_mode) + ) in ['linux_podman', 'windows_mcr'] + fail_msg: >- + Unsupported runner mode for + {{ windows_gitea_runner_runner.name | default(windows_gitea_runner_runner.profile | default('runner')) }}. + Supported modes: linux_podman, windows_mcr. + loop: "{{ __windows_gitea_runner_effective_runners }}" + loop_control: + loop_var: windows_gitea_runner_runner + tags: + - always + +- name: Resolve runner mode flags + ansible.builtin.set_fact: + __windows_gitea_runner_modes: >- + {{ + __windows_gitea_runner_effective_runners + | map(attribute='mode', default=windows_gitea_runner_default_mode) + | list + }} + tags: + - always + +- name: Set runner mode booleans + ansible.builtin.set_fact: + __windows_gitea_runner_has_linux_runners: >- + {{ 'linux_podman' in __windows_gitea_runner_modes }} + __windows_gitea_runner_has_windows_runners: >- + {{ 'windows_mcr' in __windows_gitea_runner_modes }} + tags: + - always - name: Ensure Podman auto-start service is running ansible.windows.win_service: @@ -21,6 +63,9 @@ become: true become_method: ansible.builtin.runas become_user: SYSTEM + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux - name: Wait until Podman machine is ready ansible.windows.win_shell: | @@ -42,16 +87,243 @@ become_method: ansible.builtin.runas become_user: "{{ windows_gitea_runner_podman_service_user }}" changed_when: false + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux + +- name: Ensure Docker service is running for Windows runners + ansible.windows.win_service: + name: docker + start_mode: auto + state: started + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Resolve Windows docker runtime user + ansible.builtin.set_fact: + __windows_gitea_runner_windows_runtime_user: >- + {{ + windows_gitea_runner_windows_runtime_user + | default(windows_gitea_runner_podman_service_user, true) + | default(ansible_user | default(''), true) + }} + __windows_gitea_runner_windows_become_user: >- + {{ + windows_gitea_runner_windows_become_user + | default(__windows_gitea_runner_windows_runtime_user, true) + | default('SYSTEM', true) + }} + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Check docker-users local group availability + ansible.windows.win_shell: | + $group = Get-LocalGroup -Name 'docker-users' -ErrorAction SilentlyContinue + if ($null -ne $group) { + Write-Output "__EXISTS__" + exit 0 + } + Write-Output "__MISSING__" + exit 0 + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + register: __windows_gitea_runner_docker_users_group + changed_when: false + when: + - __windows_gitea_runner_has_windows_runners + - __windows_gitea_runner_windows_runtime_user | length > 0 + tags: + - roles::windows_gitea_runner::windows + +- name: Add runtime user to docker-users group + ansible.windows.win_group_membership: + name: docker-users + members: + - >- + {{ + __windows_gitea_runner_windows_runtime_user + if ('\\' in __windows_gitea_runner_windows_runtime_user) + else '.\\' ~ __windows_gitea_runner_windows_runtime_user + }} + state: present + become: true + become_method: ansible.builtin.runas + become_user: SYSTEM + when: + - __windows_gitea_runner_has_windows_runners + - __windows_gitea_runner_windows_runtime_user | length > 0 + - "'__EXISTS__' in (__windows_gitea_runner_docker_users_group.stdout | default(''))" + tags: + - roles::windows_gitea_runner::windows + +- name: Ensure Windows base build directory exists + ansible.windows.win_file: + path: "{{ windows_gitea_runner_windows_base_build_dir }}" + state: directory + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Copy Windows base Dockerfile to build context + ansible.windows.win_copy: + src: >- + {{ + windows_gitea_runner_windows_dockerfile_source_root + ~ '/' + ~ windows_gitea_runner_windows_base_dockerfile_src + }} + dest: "{{ windows_gitea_runner_windows_base_build_dir }}\\Dockerfile" + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Copy Windows base entrypoint script to build context + ansible.windows.win_copy: + src: >- + {{ + windows_gitea_runner_windows_dockerfile_source_root + ~ '/' + ~ windows_gitea_runner_windows_base_entrypoint_script_src + }} + dest: >- + {{ + windows_gitea_runner_windows_base_build_dir + ~ '\\' + ~ windows_gitea_runner_windows_base_entrypoint_script_src + }} + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Read current Windows base image hash label + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + $image = "{{ windows_gitea_runner_windows_base_image }}" + + if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $PSNativeCommandUseErrorActionPreference = $false + } + $ErrorActionPreference = "Continue" + $raw = & $docker image inspect $image 2>$null + $inspectExit = $LASTEXITCODE + $ErrorActionPreference = "Stop" + + if ($inspectExit -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_windows_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_windows_become_user }}" + register: __windows_gitea_runner_current_windows_base_image_hash + changed_when: false + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Read desired Windows base Dockerfile checksum + ansible.builtin.set_fact: + __windows_gitea_runner_windows_base_dockerfile_checksum: >- + {{ + ( + lookup( + 'file', + windows_gitea_runner_windows_dockerfile_source_root + ~ '/' + ~ windows_gitea_runner_windows_base_dockerfile_src + ) + ~ '\n' + ~ lookup( + 'file', + windows_gitea_runner_windows_dockerfile_source_root + ~ '/' + ~ windows_gitea_runner_windows_base_entrypoint_script_src + ) + ) | hash('sha1') + }} + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Determine if Windows base image needs rebuild + ansible.builtin.set_fact: + __windows_gitea_runner_needs_windows_base_image_rebuild: >- + {{ + (__windows_gitea_runner_current_windows_base_image_hash.stdout | trim) + != (__windows_gitea_runner_windows_base_dockerfile_checksum | trim) + }} + when: __windows_gitea_runner_has_windows_runners + tags: + - roles::windows_gitea_runner::windows + +- name: Rebuild Windows base image when Dockerfile changed + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + $baseHash = "{{ __windows_gitea_runner_windows_base_dockerfile_checksum }}" + $baseLabel = "{{ windows_gitea_runner_windows_base_image_hash_label }}=$baseHash" + & $docker build ` + --label "$baseLabel" ` + --tag "{{ windows_gitea_runner_windows_base_image }}" ` + --file "{{ windows_gitea_runner_windows_base_build_dir }}\Dockerfile" ` + "{{ windows_gitea_runner_windows_base_build_dir }}" + if ($LASTEXITCODE -ne 0) { + exit 1 + } + Write-Output "__BUILT__" + become: true + become_method: ansible.builtin.runas + become_user: "{{ __windows_gitea_runner_windows_become_user }}" + register: __windows_gitea_runner_windows_base_build + changed_when: "'__BUILT__' in __windows_gitea_runner_windows_base_build.stdout" + when: + - __windows_gitea_runner_has_windows_runners + - __windows_gitea_runner_needs_windows_base_image_rebuild + tags: + - roles::windows_gitea_runner::windows - name: Ensure base build directory exists ansible.windows.win_file: path: "{{ windows_gitea_runner_base_build_dir }}" state: directory + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux - 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" + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux - name: Grant podman service user access to base build directory ansible.windows.win_acl: @@ -65,6 +337,9 @@ become: true become_method: ansible.builtin.runas become_user: SYSTEM + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux - name: Read current base image hash label ansible.windows.win_shell: | @@ -108,6 +383,9 @@ become_user: "{{ windows_gitea_runner_podman_service_user }}" register: __windows_gitea_runner_current_base_image_hash changed_when: false + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux - name: Read desired base Dockerfile checksum ansible.builtin.set_fact: @@ -118,6 +396,9 @@ windows_gitea_runner_dockerfile_source_root ~ '/' ~ windows_gitea_runner_base_dockerfile_src ) | hash('sha1') }} + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux - name: Determine if base image needs rebuild ansible.builtin.set_fact: @@ -126,6 +407,9 @@ (__windows_gitea_runner_current_base_image_hash.stdout | trim) != (__windows_gitea_runner_base_dockerfile_checksum | trim) }} + when: __windows_gitea_runner_has_linux_runners + tags: + - roles::windows_gitea_runner::linux - name: Rebuild base image when Dockerfile changed ansible.windows.win_shell: | @@ -145,12 +429,46 @@ 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 + when: + - __windows_gitea_runner_has_linux_runners + - __windows_gitea_runner_needs_base_image_rebuild + tags: + - roles::windows_gitea_runner::linux -- name: Reconcile all runners +- name: Reconcile Linux Podman runners ansible.builtin.include_tasks: runner.yml + args: + apply: + tags: + - roles::windows_gitea_runner::linux 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) }} + when: >- + ( + windows_gitea_runner_runner.mode + | default(windows_gitea_runner_default_mode) + ) == 'linux_podman' + tags: + - roles::windows_gitea_runner::linux + +- name: Reconcile Windows Mirantis runners + ansible.builtin.include_tasks: runner_windows.yml + args: + apply: + tags: + - roles::windows_gitea_runner::windows + 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) }} + when: >- + ( + windows_gitea_runner_runner.mode + | default(windows_gitea_runner_default_mode) + ) == 'windows_mcr' + tags: + - roles::windows_gitea_runner::windows diff --git a/ansible/roles/windows_gitea_runner/tasks/runner_windows.yml b/ansible/roles/windows_gitea_runner/tasks/runner_windows.yml new file mode 100644 index 0000000..dce7440 --- /dev/null +++ b/ansible/roles/windows_gitea_runner/tasks/runner_windows.yml @@ -0,0 +1,500 @@ +--- +- name: Resolve Windows 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 Windows 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_windows_image_prefix ~ ':' ~ __windows_gitea_runner_profile + ) + }} + __windows_gitea_runner_labels: >- + {{ + windows_gitea_runner_runner.labels | default( + 'windows:host,' ~ __windows_gitea_runner_profile + ) + }} + __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_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_windows_build_root ~ '\\' ~ __windows_gitea_runner_profile }} + __windows_gitea_runner_build_dockerfile_dest: >- + {{ windows_gitea_runner_windows_build_root ~ '\\' ~ __windows_gitea_runner_profile ~ '\\Dockerfile' }} + __windows_gitea_runner_dockerfile_source_path: >- + {{ + windows_gitea_runner_windows_dockerfile_source_root ~ '/' ~ ( + windows_gitea_runner_runner.dockerfile_src | default( + 'Dockerfile.' ~ __windows_gitea_runner_profile + ) + ) + }} + __windows_gitea_runner_run_args: >- + {{ windows_gitea_runner_runner.run_args | default([]) }} + +- name: Resolve Windows runner runtime config path + ansible.builtin.set_fact: + __windows_gitea_runner_runtime_config_file: "{{ __windows_gitea_runner_data_dir }}\\config.yaml" + windows_gitea_runner_config_runner_file: 'C:\runner-data\.runner' + windows_gitea_runner_config_cache_dir: 'C:\runner-data\cache' + __windows_gitea_runner_windows_become_user: >- + {{ windows_gitea_runner_windows_become_user | default('SYSTEM') }} + +- name: Read Windows runner Dockerfile checksum + ansible.builtin.set_fact: + __windows_gitea_runner_profile_dockerfile_checksum: >- + {{ lookup('file', __windows_gitea_runner_dockerfile_source_path) | hash('sha1') }} + +- name: Read current Windows base image hash label + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + $image = "{{ windows_gitea_runner_windows_base_image }}" + + if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $PSNativeCommandUseErrorActionPreference = $false + } + $ErrorActionPreference = "Continue" + $raw = & $docker image inspect $image 2>$null + $inspectExit = $LASTEXITCODE + $ErrorActionPreference = "Stop" + + if ($inspectExit -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_windows_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_windows_become_user }}" + register: __windows_gitea_runner_current_base_image_hash + changed_when: false + +- name: Compute desired Windows runner image hash + ansible.builtin.set_fact: + __windows_gitea_runner_desired_image_hash: >- + {{ { + 'base': __windows_gitea_runner_current_base_image_hash.stdout | trim, + 'profile': __windows_gitea_runner_profile_dockerfile_checksum + } | to_json | hash('sha1') }} + +- name: Compute desired Windows runner container hash + ansible.builtin.set_fact: + __windows_gitea_runner_desired_container_hash: >- + {{ { + 'mode': 'windows_mcr', + '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, + 'data': __windows_gitea_runner_data_dir, + 'log': __windows_gitea_runner_log_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 Windows runner directories exist + ansible.windows.win_file: + path: "{{ item }}" + state: directory + loop: + - "{{ windows_gitea_runner_windows_build_root }}" + - "{{ __windows_gitea_runner_build_dir }}" + - "{{ __windows_gitea_runner_data_dir }}" + - "{{ __windows_gitea_runner_log_dir }}" + - "{{ __windows_gitea_runner_config_dir }}" + +- name: Render Windows runner config + ansible.windows.win_template: + src: config.yaml.j2 + dest: "{{ __windows_gitea_runner_runtime_config_file }}" + +- name: Copy Windows 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 Windows runner image hash label + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + $image = "{{ __windows_gitea_runner_image }}" + + if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $PSNativeCommandUseErrorActionPreference = $false + } + $ErrorActionPreference = "Continue" + $raw = & $docker image inspect $image 2>$null + $inspectExit = $LASTEXITCODE + $ErrorActionPreference = "Stop" + + if ($inspectExit -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_windows_become_user }}" + register: __windows_gitea_runner_current_image_hash + changed_when: false + +- name: Determine if Windows 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 Windows runner image when Dockerfile changed + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + & $docker 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_windows_become_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 Windows runner container hash label + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $name = "{{ __windows_gitea_runner_container_name }}" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + + if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $PSNativeCommandUseErrorActionPreference = $false + } + $ErrorActionPreference = "Continue" + $raw = & $docker inspect $name 2>$null + $inspectExit = $LASTEXITCODE + $ErrorActionPreference = "Stop" + + if ($inspectExit -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_windows_become_user }}" + register: __windows_gitea_runner_current_hash + changed_when: false + +- name: Determine if Windows 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 Windows runner container before recreate + ansible.windows.win_shell: | + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + & $docker rm -f "{{ __windows_gitea_runner_container_name }}" 2>$null + become: true + become_method: ansible.builtin.runas + become_user: "{{ __windows_gitea_runner_windows_become_user }}" + when: __windows_gitea_runner_needs_recreate + changed_when: true + failed_when: false + +- name: Ensure Windows runner container is running + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + $name = "{{ __windows_gitea_runner_container_name }}" + if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $PSNativeCommandUseErrorActionPreference = $false + } + $ErrorActionPreference = "Continue" + $null = & $docker inspect $name 2>$null + $inspectExit = $LASTEXITCODE + $ErrorActionPreference = "Stop" + if ($inspectExit -eq 0) { + & $docker start $name 2>$null | Out-Null + Write-Output "__STARTED_EXISTING__" + exit 0 + } + + # Export values into process environment to avoid command-line parsing issues + # when secrets or labels contain special characters. + $env:CONFIG_FILE = "C:\runner-data\config.yaml" + $env:GITEA_INSTANCE_URL = {{ windows_gitea_runner_instance_url | to_json }} + $env:GITEA_RUNNER_REGISTRATION_TOKEN = {{ windows_gitea_runner_registration_token | to_json }} + $env:GITEA_RUNNER_NAME = {{ __windows_gitea_runner_name | to_json }} + $env:GITEA_RUNNER_LABELS = {{ __windows_gitea_runner_labels | to_json }} + + & $docker 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 }}" ` + --mount "type=bind,source={{ __windows_gitea_runner_data_dir }},target=C:\runner-data" ` + -e "CONFIG_FILE" ` + -e "GITEA_INSTANCE_URL" ` + -e "GITEA_RUNNER_REGISTRATION_TOKEN" ` + -e "GITEA_RUNNER_NAME" ` + -e "GITEA_RUNNER_LABELS" ` + "{{ __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_windows_become_user }}" + register: __windows_gitea_runner_container + changed_when: "'__CREATED__' in __windows_gitea_runner_container.stdout" + failed_when: false + no_log: true + +- name: Gather Windows runner container diagnostics when startup fails + ansible.windows.win_shell: | + $ErrorActionPreference = "Stop" + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + $name = "{{ __windows_gitea_runner_container_name }}" + $image = "{{ __windows_gitea_runner_image }}" + $hostData = "{{ __windows_gitea_runner_data_dir }}" + $hostConfig = "{{ __windows_gitea_runner_runtime_config_file }}" + + if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $PSNativeCommandUseErrorActionPreference = $false + } + + $ErrorActionPreference = "Continue" + + $imageInspect = & $docker image inspect --format "{{ '{{' }}.Id{{ '}}' }}" $image 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($imageInspect)) { + $imageInspect = "missing" + } + + $hostDataExists = Test-Path -Path $hostData + $hostConfigExists = Test-Path -Path $hostConfig + + $inspectFormat = ( + "{{ '{{' }}.State.Status{{ '}}' }}|" + + "{{ '{{' }}.State.ExitCode{{ '}}' }}|" + + "{{ '{{' }}.State.Error{{ '}}' }}" + ) + $inspect = & $docker inspect --format $inspectFormat $name 2>$null + $inspectExit = $LASTEXITCODE + if ($inspectExit -ne 0) { + $inspect = "missing" + } + + $logs = & $docker logs --tail {{ windows_gitea_runner_container_log_tail }} $name 2>&1 + $logsExit = $LASTEXITCODE + if ($logsExit -ne 0 -or [string]::IsNullOrWhiteSpace($logs)) { + $logs = "no logs available" + } + + $diagName = "$name-diag-$([guid]::NewGuid().ToString('N').Substring(0, 8))" + $diagRun = & $docker run --rm --name $diagName ` + --entrypoint "C:\Windows\System32\cmd.exe" ` + @({{ __windows_gitea_runner_run_args | map('to_json') | join(', ') }}) ` + --mount "type=bind,source={{ __windows_gitea_runner_data_dir }},target=C:\runner-data" ` + -e "CONFIG_FILE=C:\runner-data\config.yaml" ` + -e "GITEA_INSTANCE_URL={{ windows_gitea_runner_instance_url }}" ` + -e "GITEA_RUNNER_REGISTRATION_TOKEN=diag-token" ` + -e "GITEA_RUNNER_NAME=diag-runner" ` + -e "GITEA_RUNNER_LABELS={{ __windows_gitea_runner_labels }}" ` + "{{ __windows_gitea_runner_image }}" /S /C "echo DIAG_OK" 2>&1 + $diagRunExit = $LASTEXITCODE + if ([string]::IsNullOrWhiteSpace($diagRun)) { + $diagRun = "(empty)" + } + + $ErrorActionPreference = "Stop" + + Write-Output "IMAGE: $imageInspect" + Write-Output "HOST_DATA_EXISTS: $hostDataExists" + Write-Output "HOST_CONFIG_EXISTS: $hostConfigExists" + Write-Output "STATE: $inspect" + Write-Output "---DIAG_RUN (rc=$diagRunExit)---" + Write-Output $diagRun + Write-Output "---LOGS---" + Write-Output $logs + become: true + become_method: ansible.builtin.runas + become_user: "{{ __windows_gitea_runner_windows_become_user }}" + register: __windows_gitea_runner_start_failure_diag + changed_when: false + when: (__windows_gitea_runner_container.rc | default(0)) != 0 + +- name: Fail with Windows runner startup diagnostics + ansible.builtin.fail: + msg: >- + Windows runner container failed to start (rc={{ __windows_gitea_runner_container.rc | default('unknown') }}). + RUN_STDERR={{ (__windows_gitea_runner_container.stderr | default('')) + | replace(windows_gitea_runner_registration_token | default(''), '***') }} + RUN_STDOUT={{ (__windows_gitea_runner_container.stdout | default('')) + | replace(windows_gitea_runner_registration_token | default(''), '***') }} + {{ [ + __windows_gitea_runner_start_failure_diag.stdout | default(''), + __windows_gitea_runner_start_failure_diag.stderr | default('') + ] | join('\n') | trim }} + when: (__windows_gitea_runner_container.rc | default(0)) != 0 + +- name: Verify Windows runner container state + ansible.windows.win_shell: | + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + & $docker inspect "{{ __windows_gitea_runner_container_name }}" --format "{{ '{{' }}.State.Status{{ '}}' }}" + become: true + become_method: ansible.builtin.runas + become_user: "{{ __windows_gitea_runner_windows_become_user }}" + register: __windows_gitea_runner_status + changed_when: false + failed_when: (__windows_gitea_runner_status.stdout | trim) != 'running' + when: (__windows_gitea_runner_container.rc | default(0)) == 0 + +- name: Show tail of Windows runner logs + ansible.windows.win_shell: | + $docker = "{{ windows_gitea_runner_windows_runtime_bin }}" + & $docker 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_windows_become_user }}" + register: __windows_gitea_runner_logs + changed_when: false + when: (__windows_gitea_runner_container.rc | default(0)) == 0 + +- name: Print Windows runner logs + ansible.builtin.debug: + msg: "{{ __windows_gitea_runner_logs.stdout | default('') }}" + +- name: Build normalized Windows 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 Windows 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" + fail_msg: >- + Windows Gitea runner logs indicate a registration/startup failure. + Check token, instance URL, and runner image entrypoint behavior. diff --git a/ansible/roles/windows_gitea_runner/templates/config.yaml.j2 b/ansible/roles/windows_gitea_runner/templates/config.yaml.j2 index 02c4c8b..98917aa 100644 --- a/ansible/roles/windows_gitea_runner/templates/config.yaml.j2 +++ b/ansible/roles/windows_gitea_runner/templates/config.yaml.j2 @@ -2,10 +2,10 @@ log: level: info runner: - file: /data/.runner + file: {{ windows_gitea_runner_config_runner_file | default('/data/.runner') | to_json }} capacity: 1 envs: {} cache: enabled: true - dir: /data/cache + dir: {{ windows_gitea_runner_config_cache_dir | default('/data/cache') | to_json }}