From fdf51531307a3c5b6cc6b7b09c835b7d5d733267 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 24 Jan 2026 11:28:35 -0800 Subject: [PATCH] Containerize forgejo-runner with Tailscale gateway for tailnet access Architecture: - tailscale_ci_gateway role: Runs Tailscale container on tailnet-jobs network - forgejo_runner role: Runs runner daemon in container on same network - Job containers also use tailnet-jobs network This allows the runner and jobs to reach forge.tail8d86e.ts.net via the Tailscale gateway, avoiding hairpinning issues with localhost. Changes: - Add tailscale_ci_gateway role with launchd management - Refactor forgejo_runner to use containerized daemon - Runner registers with Tailscale URL instead of localhost - Job containers run on tailnet-jobs network - Update playbook role ordering (gateway before runner) Co-Authored-By: Claude Opus 4.5 --- ansible/playbooks/indri.yml | 19 ++++++ .../roles/forgejo_runner/defaults/main.yml | 33 ++++++---- .../roles/forgejo_runner/handlers/main.yml | 4 +- ansible/roles/forgejo_runner/tasks/main.yml | 61 ++++++++----------- .../forgejo_runner/templates/config.yaml.j2 | 9 ++- .../templates/forgejo-runner.plist.j2 | 28 ++++++--- .../tailscale_ci_gateway/defaults/main.yml | 9 +++ .../tailscale_ci_gateway/handlers/main.yml | 7 +++ .../roles/tailscale_ci_gateway/tasks/main.yml | 46 ++++++++++++++ .../templates/tailscale-ci-gateway.plist.j2 | 45 ++++++++++++++ 10 files changed, 199 insertions(+), 62 deletions(-) create mode 100644 ansible/roles/tailscale_ci_gateway/defaults/main.yml create mode 100644 ansible/roles/tailscale_ci_gateway/handlers/main.yml create mode 100644 ansible/roles/tailscale_ci_gateway/tasks/main.yml create mode 100644 ansible/roles/tailscale_ci_gateway/templates/tailscale-ci-gateway.plist.j2 diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index b12e905..09779ec 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -78,6 +78,23 @@ no_log: true tags: [forgejo_runner] + # Tailscale CI gateway auth key (for job container tailnet access) + - name: Fetch tailscale ci-gateway auth key + ansible.builtin.command: + cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields ci-gateway-ts-auth-key --reveal + delegate_to: localhost + register: _tailscale_ci_gateway_auth_key + changed_when: false + no_log: true + check_mode: false + tags: [tailscale_ci_gateway] + + - name: Set tailscale ci-gateway auth key fact + ansible.builtin.set_fact: + tailscale_ci_gateway_auth_key: "{{ _tailscale_ci_gateway_auth_key.stdout }}" + no_log: true + tags: [tailscale_ci_gateway] + roles: - role: alloy tags: alloy @@ -99,5 +116,7 @@ tags: plex_metrics - role: tailscale_serve tags: tailscale-serve + - role: tailscale_ci_gateway + tags: tailscale_ci_gateway - role: forgejo_runner tags: forgejo_runner diff --git a/ansible/roles/forgejo_runner/defaults/main.yml b/ansible/roles/forgejo_runner/defaults/main.yml index 7d18548..6c87755 100644 --- a/ansible/roles/forgejo_runner/defaults/main.yml +++ b/ansible/roles/forgejo_runner/defaults/main.yml @@ -1,25 +1,37 @@ --- -forgejo_runner_repo_dir: /Users/erichblume/code/3rd/forgejo-runner -forgejo_runner_binary: "{{ forgejo_runner_repo_dir }}/forgejo-runner" +# Forgejo Runner - containerized daemon on tailnet-jobs network +# +# The runner daemon runs in a Docker container with access to the tailnet +# via the tailscale-ci-gateway. This allows it to register with Forgejo +# using the Tailscale URL, so job containers can also reach Forgejo. + forgejo_runner_data_dir: /Users/erichblume/.forgejo-runner forgejo_runner_config_dir: /Users/erichblume/.config/forgejo-runner forgejo_runner_log_dir: /Users/erichblume/Library/Logs -# Runner registration -forgejo_runner_instance_url: "http://localhost:3001" +# Container settings +forgejo_runner_container_name: forgejo-runner +forgejo_runner_image: code.forgejo.org/forgejo/runner:6.2.1 +forgejo_runner_network: tailnet-jobs + +# Runner registration - use Tailscale URL since we're on tailnet-jobs network +forgejo_runner_instance_url: "https://forge.tail8d86e.ts.net" forgejo_runner_name: "indri-docker-runner" + # Labels format: label:docker://image # -# Bootstrap mode (use upstream images to build our own): +# Job containers also run on tailnet-jobs network and can reach: +# - forge.tail8d86e.ts.net for git clone +# - registry.tail8d86e.ts.net for container push/pull +# +# Bootstrap mode (use upstream images until we build ci-base): # docker-builder:docker://docker:27-cli # ubuntu-latest:docker://catthehacker/ubuntu:act-22.04 # -# Production mode (use our own images from zot via host.docker.internal): -# docker-builder:docker://host.docker.internal:5050/blumeops/ci-base:latest -# ubuntu-latest:docker://host.docker.internal:5050/blumeops/ci-base:latest +# Production mode (use our own images from zot): +# docker-builder:docker://registry.tail8d86e.ts.net/blumeops/ci-base:latest +# ubuntu-latest:docker://registry.tail8d86e.ts.net/blumeops/ci-base:latest # -# Note: Docker daemon.json must include host.docker.internal:5050 in insecure-registries -# Currently using bootstrap mode until ci-base is built forgejo_runner_labels: "docker-builder:docker://docker:27-cli,ubuntu-latest:docker://catthehacker/ubuntu:act-22.04,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" # Runner config @@ -27,5 +39,4 @@ forgejo_runner_capacity: 2 forgejo_runner_timeout: 3h # Docker container settings for jobs -# Note: network is hardcoded to "host" so containers can reach localhost:3001 (Forgejo) forgejo_runner_privileged: true # Needed for container builds diff --git a/ansible/roles/forgejo_runner/handlers/main.yml b/ansible/roles/forgejo_runner/handlers/main.yml index a1798f4..8ace37c 100644 --- a/ansible/roles/forgejo_runner/handlers/main.yml +++ b/ansible/roles/forgejo_runner/handlers/main.yml @@ -2,6 +2,6 @@ - name: Restart forgejo-runner listen: Restart forgejo-runner ansible.builtin.shell: | - launchctl unload ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist 2>/dev/null || true - launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist + launchctl unload ~/Library/LaunchAgents/mcquack.forgejo-runner.plist 2>/dev/null || true + launchctl load ~/Library/LaunchAgents/mcquack.forgejo-runner.plist changed_when: true diff --git a/ansible/roles/forgejo_runner/tasks/main.yml b/ansible/roles/forgejo_runner/tasks/main.yml index d7106c1..72e114b 100644 --- a/ansible/roles/forgejo_runner/tasks/main.yml +++ b/ansible/roles/forgejo_runner/tasks/main.yml @@ -1,34 +1,12 @@ --- -# Forgejo Runner on indri +# Forgejo Runner - containerized daemon on tailnet-jobs network # -# Uses Docker container mode for job isolation. -# Can build containers using Docker (via socket). +# The runner daemon runs in a Docker container with access to the tailnet +# via the tailscale-ci-gateway. Job containers also run on tailnet-jobs +# and can reach Forgejo via Tailscale. # -# ONE-TIME SETUP (before running ansible): -# -# 1. Clone forgejo-runner from forge mirror: -# ssh indri 'git clone https://forge.tail8d86e.ts.net/eblume/forgejo-runner.git ~/code/3rd/forgejo-runner' -# -# 2. Set up Go via mise: -# ssh indri 'cd ~/code/3rd/forgejo-runner && mise use go@1.24' -# -# 3. Build: -# ssh indri 'cd ~/code/3rd/forgejo-runner && mise x -- make build' -# -# 4. Run ansible to deploy config and LaunchAgent - -- name: Verify forgejo-runner binary exists - ansible.builtin.stat: - path: "{{ forgejo_runner_binary }}" - register: forgejo_runner_binary_stat - -- name: Fail if forgejo-runner binary not found - ansible.builtin.fail: - msg: | - Forgejo-runner binary not found at {{ forgejo_runner_binary }}. - Please build from source first: - ssh indri 'cd ~/code/3rd/forgejo-runner && mise x -- make build' - when: not forgejo_runner_binary_stat.stat.exists +# DEPENDENCIES: +# - tailscale_ci_gateway role must run first (creates tailnet-jobs network) - name: Ensure forgejo-runner directories exist ansible.builtin.file: @@ -46,38 +24,47 @@ mode: '0644' notify: Restart forgejo-runner +- name: Pull forgejo-runner image + ansible.builtin.command: + cmd: docker pull {{ forgejo_runner_image }} + register: forgejo_runner_pull + changed_when: "'Downloaded newer image' in forgejo_runner_pull.stdout or 'Pull complete' in forgejo_runner_pull.stdout" + - name: Check if runner is registered ansible.builtin.stat: path: "{{ forgejo_runner_data_dir }}/.runner" register: forgejo_runner_registered -- name: Register runner with Forgejo +- name: Register runner with Forgejo (via tailnet) ansible.builtin.command: cmd: > - {{ forgejo_runner_binary }} register + docker run --rm + --network {{ forgejo_runner_network }} + -v {{ forgejo_runner_data_dir }}:/data + {{ forgejo_runner_image }} + forgejo-runner register --instance "{{ forgejo_runner_instance_url }}" --token "{{ forgejo_runner_token }}" --name "{{ forgejo_runner_name }}" --labels "{{ forgejo_runner_labels }}" --no-interactive - chdir: "{{ forgejo_runner_data_dir }}" when: not forgejo_runner_registered.stat.exists changed_when: true -- name: Deploy forgejo-runner LaunchAgent plist +- name: Deploy forgejo-runner launchd plist ansible.builtin.template: src: forgejo-runner.plist.j2 - dest: ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist + dest: ~/Library/LaunchAgents/mcquack.forgejo-runner.plist mode: '0644' notify: Restart forgejo-runner -- name: Check if forgejo-runner LaunchAgent is loaded - ansible.builtin.command: launchctl list mcquack.eblume.forgejo-runner +- name: Check if forgejo-runner is loaded + ansible.builtin.command: launchctl list mcquack.forgejo-runner register: forgejo_runner_launchctl_check changed_when: false failed_when: false -- name: Load forgejo-runner LaunchAgent if not loaded - ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo-runner.plist +- name: Load forgejo-runner if not loaded + ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.forgejo-runner.plist when: forgejo_runner_launchctl_check.rc != 0 changed_when: true diff --git a/ansible/roles/forgejo_runner/templates/config.yaml.j2 b/ansible/roles/forgejo_runner/templates/config.yaml.j2 index c7aa4e6..397fbbd 100644 --- a/ansible/roles/forgejo_runner/templates/config.yaml.j2 +++ b/ansible/roles/forgejo_runner/templates/config.yaml.j2 @@ -3,16 +3,15 @@ log: level: info runner: - file: {{ forgejo_runner_data_dir }}/.runner + # Path inside the container (data dir mounted at /data) + file: /data/.runner capacity: {{ forgejo_runner_capacity }} timeout: {{ forgejo_runner_timeout }} container: - network: "bridge" + # Use tailnet-jobs network so job containers can reach Forgejo via Tailscale gateway + network: "{{ forgejo_runner_network }}" privileged: {{ forgejo_runner_privileged | lower }} - # Map localhost to Docker host so containers can reach Forgejo at localhost:3001 - # host-gateway is a special Docker value that resolves to the host IP - options: "--add-host localhost:host-gateway" # Mount Docker socket so jobs can build containers valid_volumes: - /var/run/docker.sock diff --git a/ansible/roles/forgejo_runner/templates/forgejo-runner.plist.j2 b/ansible/roles/forgejo_runner/templates/forgejo-runner.plist.j2 index 4bac25f..dc8c2ba 100644 --- a/ansible/roles/forgejo_runner/templates/forgejo-runner.plist.j2 +++ b/ansible/roles/forgejo_runner/templates/forgejo-runner.plist.j2 @@ -4,16 +4,30 @@ Label - mcquack.eblume.forgejo-runner + mcquack.forgejo-runner ProgramArguments - {{ forgejo_runner_binary }} - daemon - --config - {{ forgejo_runner_config_dir }}/config.yaml + /bin/bash + -c + /dev/null || true +docker rm {{ forgejo_runner_container_name }} 2>/dev/null || true + +# Run the forgejo-runner daemon in a container +# - On tailnet-jobs network (can reach Forgejo via Tailscale gateway) +# - Mounts docker socket to spawn job containers +# - Mounts config and data directories +exec docker run --rm \ + --name {{ forgejo_runner_container_name }} \ + --network {{ forgejo_runner_network }} \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v {{ forgejo_runner_config_dir }}/config.yaml:/config.yaml:ro \ + -v {{ forgejo_runner_data_dir }}:/data \ + {{ forgejo_runner_image }} \ + forgejo-runner daemon --config /config.yaml +]]> - WorkingDirectory - {{ forgejo_runner_data_dir }} RunAtLoad KeepAlive diff --git a/ansible/roles/tailscale_ci_gateway/defaults/main.yml b/ansible/roles/tailscale_ci_gateway/defaults/main.yml new file mode 100644 index 0000000..707df2e --- /dev/null +++ b/ansible/roles/tailscale_ci_gateway/defaults/main.yml @@ -0,0 +1,9 @@ +--- +# Tailscale CI Gateway - provides tailnet access for Forgejo runner job containers + +tailscale_ci_gateway_state_dir: /Users/erichblume/.tailscale-ci-gateway +tailscale_ci_gateway_network: tailnet-jobs +tailscale_ci_gateway_network_subnet: "172.30.0.0/24" +tailscale_ci_gateway_container_name: tailscale-ci-gateway +tailscale_ci_gateway_hostname: ci-gateway +tailscale_ci_gateway_image: tailscale/tailscale:latest diff --git a/ansible/roles/tailscale_ci_gateway/handlers/main.yml b/ansible/roles/tailscale_ci_gateway/handlers/main.yml new file mode 100644 index 0000000..e35e9b9 --- /dev/null +++ b/ansible/roles/tailscale_ci_gateway/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart tailscale-ci-gateway + listen: Restart tailscale-ci-gateway + ansible.builtin.shell: | + launchctl unload ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist 2>/dev/null || true + launchctl load ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist + changed_when: true diff --git a/ansible/roles/tailscale_ci_gateway/tasks/main.yml b/ansible/roles/tailscale_ci_gateway/tasks/main.yml new file mode 100644 index 0000000..99f1948 --- /dev/null +++ b/ansible/roles/tailscale_ci_gateway/tasks/main.yml @@ -0,0 +1,46 @@ +--- +# Tailscale CI Gateway role +# Manages a Tailscale container that provides tailnet access for CI job containers + +- name: Ensure state directory exists + ansible.builtin.file: + path: "{{ tailscale_ci_gateway_state_dir }}" + state: directory + mode: "0700" + +- name: Check if Docker network exists + ansible.builtin.command: + cmd: docker network inspect {{ tailscale_ci_gateway_network }} + register: tailscale_ci_gateway_network_check + failed_when: false + changed_when: false + +- name: Create Docker network for CI jobs + ansible.builtin.command: + cmd: >- + docker network create + --driver bridge + --subnet {{ tailscale_ci_gateway_network_subnet }} + {{ tailscale_ci_gateway_network }} + when: tailscale_ci_gateway_network_check.rc != 0 + changed_when: true + +- name: Pull Tailscale image + ansible.builtin.command: + cmd: docker pull {{ tailscale_ci_gateway_image }} + register: tailscale_ci_gateway_pull + changed_when: "'Downloaded newer image' in tailscale_ci_gateway_pull.stdout or 'Pull complete' in tailscale_ci_gateway_pull.stdout" + +- name: Deploy launchd plist for Tailscale CI gateway + ansible.builtin.template: + src: tailscale-ci-gateway.plist.j2 + dest: ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist + mode: "0644" + notify: Restart tailscale-ci-gateway + +- name: Ensure Tailscale CI gateway is loaded + ansible.builtin.command: + cmd: launchctl load ~/Library/LaunchAgents/mcquack.tailscale-ci-gateway.plist + register: tailscale_ci_gateway_load + failed_when: false + changed_when: tailscale_ci_gateway_load.rc == 0 diff --git a/ansible/roles/tailscale_ci_gateway/templates/tailscale-ci-gateway.plist.j2 b/ansible/roles/tailscale_ci_gateway/templates/tailscale-ci-gateway.plist.j2 new file mode 100644 index 0000000..d3fe652 --- /dev/null +++ b/ansible/roles/tailscale_ci_gateway/templates/tailscale-ci-gateway.plist.j2 @@ -0,0 +1,45 @@ + + + + + Label + mcquack.tailscale-ci-gateway + + ProgramArguments + + /bin/bash + -c + /dev/null || true +docker rm {{ tailscale_ci_gateway_container_name }} 2>/dev/null || true + +# Run the container (foreground so launchd manages lifecycle) +exec docker run --rm \ + --name {{ tailscale_ci_gateway_container_name }} \ + --hostname {{ tailscale_ci_gateway_hostname }} \ + --network {{ tailscale_ci_gateway_network }} \ + --cap-add NET_ADMIN \ + --cap-add NET_RAW \ + -v {{ tailscale_ci_gateway_state_dir }}:/var/lib/tailscale \ + -e TS_AUTHKEY="{{ tailscale_ci_gateway_auth_key }}" \ + -e TS_STATE_DIR=/var/lib/tailscale \ + -e TS_USERSPACE=false \ + -e TS_ACCEPT_DNS=true \ + {{ tailscale_ci_gateway_image }} +]]> + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + {{ ansible_env.HOME }}/Library/Logs/mcquack.tailscale-ci-gateway.out.log + + StandardErrorPath + {{ ansible_env.HOME }}/Library/Logs/mcquack.tailscale-ci-gateway.err.log + +