diff --git a/AGENTS.md b/AGENTS.md index ae76154..5b86be6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is ## Architecture - **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible -- **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup +- **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup with pasta networking (IPv6 support) - **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications - **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits diff --git a/ansible/roles/gitea-runner/defaults/main.yml b/ansible/roles/gitea-runner/defaults/main.yml index f346fcb..75a6ae0 100644 --- a/ansible/roles/gitea-runner/defaults/main.yml +++ b/ansible/roles/gitea-runner/defaults/main.yml @@ -53,3 +53,10 @@ docker_apt_source_line: >- {{ ansible_facts['distribution_release'] }} stable # Set to false in CI/molecule to skip rootless daemon startup (needs kernel userns) docker_rootless_setup: true + +# Rootless Docker network driver: "pasta" (IPv6 support) or "slirp4netns" (IPv4 only) +# pasta has proper outgoing IPv6 support; slirp4netns does not (known limitation). +docker_rootless_net_driver: "pasta" + +# IPv6 subnet for rootless Docker containers (ULA range, not routable on internet) +docker_ipv6_cidr: "fd00:dead:beef::/48" diff --git a/ansible/roles/gitea-runner/tasks/rootless_docker.yml b/ansible/roles/gitea-runner/tasks/rootless_docker.yml index d94a678..bf2778c 100644 --- a/ansible/roles/gitea-runner/tasks/rootless_docker.yml +++ b/ansible/roles/gitea-runner/tasks/rootless_docker.yml @@ -35,6 +35,7 @@ name: - uidmap - slirp4netns + - passt - fuse-overlayfs - docker-ce - docker-ce-cli @@ -57,6 +58,7 @@ - docker - docker-compose - slirp4netns + - passt - fuse-overlayfs - rsync state: present @@ -106,12 +108,48 @@ group: "{{ gitea_runner_service_user }}" when: docker_rootless_setup -- name: Configure rootless Docker to disable IPv6 +- name: Ensure systemd user override directory exists + ansible.builtin.file: + path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d" + state: directory + mode: "0755" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + when: docker_rootless_setup + +- name: Configure rootless Docker to use pasta with IPv6 + ansible.builtin.copy: + dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf" + content: | + [Service] + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ docker_rootless_net_driver }}" + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit" + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6" + mode: "0644" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + register: docker_network_override + when: docker_rootless_setup + +- name: Reload systemd user daemon if network config changed + ansible.builtin.command: systemctl --user daemon-reload + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true + when: + - docker_rootless_setup + - docker_network_override is changed + +- name: Configure rootless Docker daemon with IPv6 enabled ansible.builtin.copy: dest: "{{ gitea_runner_home }}/.config/docker/daemon.json" content: | { - "ipv6": false + "ipv6": true, + "ip6tables": true, + "fixed-cidr-v6": "{{ docker_ipv6_cidr }}" } mode: "0644" owner: "{{ gitea_runner_service_user }}" @@ -128,7 +166,7 @@ changed_when: true when: - docker_rootless_setup - - docker_ipv6_config is changed + - docker_ipv6_config is changed or docker_network_override is changed - name: Wait for rootless Docker daemon to be ready ansible.builtin.command: docker version diff --git a/docs/tech/architecture.md b/docs/tech/architecture.md index 701be21..8ff30e9 100644 --- a/docs/tech/architecture.md +++ b/docs/tech/architecture.md @@ -173,9 +173,21 @@ Each runner operates under a dedicated unprivileged system user. The Docker daem - User namespace mapping via `/etc/subuid` and `/etc/subgid` (range: 100000-165535) - Rootless Docker socket at `/run/user//docker.sock` -- `slirp4netns` for user-mode networking +- `pasta` for user-mode networking with IPv6 support (replaces `slirp4netns`, which lacks outgoing IPv6) - `fuse-overlayfs` for rootless container storage +The rootless Docker daemon is configured via a systemd user override +(`docker.service.d/override.conf`) that sets: + +- `DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta` — use pasta as the network driver +- `DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit` — pasta's native port forwarding +- `DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6` — enable IPv6 routing + +The daemon.json enables IPv6 with a ULA subnet (`fd00:dead:beef::/48`) +for container addressing. This ensures runner containers can reach +both IPv4 and IPv6 services (e.g., the Gitea registry) without +per-workaround DNS hacks. + Containers launched by the runner never have root access to the host. The rootless Docker daemon is started as a systemd user service and persists via lingering. ### Secret handling