Repository navigation
Description
A native node (github_runner_cluster_node_mode: native) can only be an agent today: plugins/filter/cluster_topology.py forces it to agent and refuses github_runner_cluster_node_role: server and github_runner_cluster_bootstrap: true. I want a native host to run as a k3s server under systemd, beside the agent path, so a Linux host with a good path and a real disk can hold an etcd vote without a Docker VM in front of its fsync (a voter in a Colima VM has lost unsynced raft entries at a power loss). Part of the dynamic regrading epic.
Topology. A native host becomes a server only when it sets github_runner_cluster_node_role: server or is marked eligible for the role controller; it still never counts towards the automatic server number, so an existing inventory whose native hosts set nothing keeps every one of them an agent (with max_servers: 5 and native hosts no longer forced to agents, a fleet's native agents would otherwise become servers silently). A native bootstrap host is allowed, and refused under headscale_in_cluster, whose bootstrap supervises k3s for a static Headscale pod. The guard that refuses role: server is replaced by these rules, and the README's Native nodes section says so.
Units. Two units, as k3s's installer names them: k3s.service (k3s server --config /etc/rancher/k3s/server.yaml) and the existing k3s-agent.service (switched to --config /etc/rancher/k3s/agent.yaml, so neither reads the other's flags; the old config.yaml is removed on the first run). A role file, /var/lib/rancher/k3s/fleet-role, holds server or agent, and each unit has an ExecCondition= that skips the start unless the file names its role, so the two can never run together and Restart=always can never start a server that has been demoted. Same Type=exec, KillMode=process, Delegate=yes and limits as the agent unit, and the same PATH wrapper and resolver helper.
Server configuration, matching what every Docker server passes (k3s requires the network, --disable and --disable-network-policy flags to match on every server): server (a current voter's URL when joining), cluster-init only when bootstrapping a cluster that does not exist yet, token-file, vpn-auth-file, node-name, node-ip set to the host's Tailscale IPv4 (read by an ExecStartPre helper and written to server.yaml.d/, so etcd's peer URL is the mesh address and never the LAN address k3s would otherwise take), disable: [traefik], disable-network-policy, write-kubeconfig-mode, https-listen-port, tls-san for every server and every eligible host, the etcd timing as etcd-arg when set, resolv-conf and node-taint. k3s reads <config>.d/*.yaml beside the file it is given (dotDFiles in pkg/configfilearg/parser.go), which is where the regrade level and the join URL go without Ansible.
Data and snapshots. /var/lib/rancher/k3s/server on the host's disk; scheduled snapshots stay k3s's default (every 12 hours, five kept per server). The role's etcd_timing checks extend to native servers.
Upgrades and restarts. The version check reads the cluster through native servers (k3s kubectl on the host) as well as through Docker containers, and decides the server and agent rules from each node's live role labels, not the inventory, since the role controller can change them. A native server restarts only through the same quorum guard and one-at-a-time turns as a recreated Docker server (tasks/server_recreate_check.yml, tasks/server_ready.yml), servers first.
Tailscale. Unchanged: the host's own client is the node's device, logged in by k3s's --vpn-auth. The tailnet policy the role writes already grants all ports between node tags, so etcd's 2379 and 2380 and the API port reach a native server over the mesh. The API and kubelet ports also answer on the host's LAN addresses; the github_runner_linux_host role gains an opt-in rule admitting the API, etcd and kubelet ports only on tailscale0 and loopback, since a laptop server joins networks the fleet does not control.
Converting an existing native agent in place. Same node name, password file and Tailscale device: the role writes the server configuration and role file, cordons and drains the node (waiting for runner pods to finish), stops the agent unit and starts the server unit, which joins as a learner. Whether k3s adds the control-plane, etcd and master labels to a Node that registered as an agent, and accepts the existing node password, is the first thing the mesh test establishes.
Tests: tests/unit for the topology filter (native server, native bootstrap, native hosts kept out of the automatic count, headscale_in_cluster refusal), the units and templates. A mesh scenario that adds a native node as a privileged systemd container (real systemd, the real pinned k3s binary, Tailscale in the container) to the three Docker servers: it joins as a server, is a learner first and a voter after promotion (etcdctl member list through a server, EtcdIsVoter on its Node), its peer URL is its mesh address, writes keep succeeding throughout, a restart rejoins as the same member, and both units never run together. On a real native host the role then runs in --check and with github_runner_cluster_native_start: false, staging the server without starting it, before any live conversion.
Activity
github-actions commentedon Nov 3, 2026
🎉 This issue has been resolved in version 2.24.0 🎉
The release is available on:
v2.24.0Your semantic-release bot 📦🚀