Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions roles/github_runner_macos_host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ A MacBook with its lid closed still sleeps with `sleep 0`, because that setting

## Tailscale

The role installs the Homebrew formula `tailscale` and runs `tailscaled install-system-daemon`, which copies `tailscaled` to `/usr/local/bin`, writes the launchd daemon `/Library/LaunchDaemons/com.tailscale.tailscaled.plist` and starts it, so the daemon runs from boot, before anyone logs in. The standalone Tailscale app and this daemon are meant to run side by side on a Mac (the app's network extension and the daemon are both connected on the fleet's Macs), so the app being installed never skips the daemon. A host that already has that file is not installed on again, whether this role or a person installed it, and a daemon without the Homebrew formula behind it (one built by hand) is never replaced; what the role does for a host that has the formula is in Keeping Tailscale current below.
The role installs the Homebrew formula `tailscale` and runs `tailscaled install-system-daemon`, which copies `tailscaled` to `/usr/local/bin`, writes the launchd daemon `/Library/LaunchDaemons/com.tailscale.tailscaled.plist` and starts it, so the daemon runs from boot, before anyone logs in. The standalone Tailscale app and this daemon are meant to run side by side on a Mac (the app's network extension and the daemon are both connected on the fleet's Macs), so the app being installed never skips the daemon. The role runs the formula's binaries from the formula's keg (`<prefix>/opt/tailscale/bin`, Homebrew's stable path for the newest keg) and never from the prefix's `bin` directory, because Homebrew links the formula there only when nothing else holds the names, and the standalone app's wrapper script (`#!/bin/sh` then a call of the app's own binary) holds `/usr/local/bin/tailscale` on an Intel Mac that has the app: Homebrew then installs the keg, removes every link it made and exits non-zero, which the role does not count as a failure while the formula is installed (and current, when it is being kept current). That wrapper also talks to the app and not to the daemon, which is why the role asks the daemon through the formula's own `tailscale`. A host that already has that file is not installed on again, whether this role or a person installed it, and a daemon without the Homebrew formula behind it (one built by hand) is never replaced; what the role does for a host that has the formula is in Keeping Tailscale current below.

The role never joins or re-authenticates a tailnet on its own, because joining needs an auth key, a secret only the tailnet's owner can create. With `github_runner_macos_host_tailscale_auth_key` set, the role reads the daemon's login state and, for a daemon that needs a login, runs `tailscale up` with the key on standard input (so it is in no process listing and, with `no_log`, in no output), plus `github_runner_macos_host_tailscale_up_args`. A daemon that is already logged in is not touched, and a join that failed is retried by the next run. It applies only to a host whose Homebrew formula the role installed (the formula's `tailscale` is in the Homebrew prefix), so a hand-built daemon is never joined or logged in again.
The role never joins or re-authenticates a tailnet on its own, because joining needs an auth key, a secret only the tailnet's owner can create. With `github_runner_macos_host_tailscale_auth_key` set, the role reads the daemon's login state and, for a daemon that needs a login, runs `tailscale up` with the key on standard input (so it is in no process listing and, with `no_log`, in no output), plus `github_runner_macos_host_tailscale_up_args`. A daemon that is already logged in is not touched, and a join that failed is retried by the next run. It applies only to a host whose Homebrew formula the role installed (the formula's `tailscale` is in its keg), so a hand-built daemon is never joined or logged in again.

Without the key, join the host by hand once: `sudo tailscale up`, or run the role again with a key.

### Keeping Tailscale current

A Tailscale that was installed once and never updated falls behind: the formula is only installed when absent, and `tailscaled install-system-daemon` copies the formula's `tailscaled` to `/usr/local/bin` (or, on a prefix of `/usr/local` where that path is a link to the formula, starts it once), so upgrading the formula alone leaves the old release running. With `github_runner_macos_host_tailscale_update` (default true) the role, after the install above and before any join, does three things for what the host already has, and installs nothing: it refreshes Homebrew's index (`brew update`) and brings the `tailscale` formula, and no other formula, to its latest release; it runs `tailscaled install-system-daemon` again, once, when the installed daemon's release differs from the formula's or the running daemon's from the formula's, which removes the installed daemon and its launchd definition and installs and starts the new ones ([`cmd/tailscaled/install_darwin.go`](https://github.com/tailscale/tailscale/blob/main/cmd/tailscaled/install_darwin.go), whose uninstall step runs first), and the daemon keeps its login because that lives in its state directory; and when Tailscale's standalone app is installed as the Homebrew cask `tailscale-app`, it keeps that current too (`greedy`, because the app updates itself and Homebrew then lists it as outdated only when asked to be), never installing it.
A Tailscale that was installed once and never updated falls behind: the formula is only installed when absent, and `tailscaled install-system-daemon` copies the formula's `tailscaled` to `/usr/local/bin` (or, on a prefix of `/usr/local` where that path is a link to the formula, starts it once), so upgrading the formula alone leaves the old release running. With `github_runner_macos_host_tailscale_update` (default true) the role, after the install above and before any join, does three things for what the host already has, and installs nothing: it refreshes Homebrew's index (`brew update`) and brings the `tailscale` formula, and no other formula, to its latest release (only when Homebrew reports it behind, and an upgrade whose link step failed but which left the formula installed and current is a success); it runs `tailscaled install-system-daemon` again, once, when the installed daemon's release differs from the formula's or the running daemon's from the formula's, which removes the installed daemon and its launchd definition and installs and starts the new ones ([`cmd/tailscaled/install_darwin.go`](https://github.com/tailscale/tailscale/blob/main/cmd/tailscaled/install_darwin.go), whose uninstall step runs first), and the daemon keeps its login because that lives in its state directory; and when Tailscale's standalone app is installed as the Homebrew cask `tailscale-app`, it keeps that current too, never installing it: the app updates itself, so Homebrew's record of the cask lags and `brew outdated --greedy` lists it although the app is current, and upgrading it then runs the cask's installer over the current app, which fails and is reverted. The role therefore upgrades the cask only when the release in the app bundle (`CFBundleShortVersionString`) is behind the cask's, and leaves an app that has updated itself, or is ahead of the cask, alone.

Replacing the daemon restarts it, so connections over the host's tailnet, an SSH session included, drop for a moment; upgrading the app runs its installer, which uses `sudo`, and restarts its network extension. A host with neither the formula behind its daemon nor the cask is skipped with a message, as is one without Homebrew, and a daemon built by hand is left as it is. Under `--check` Homebrew's index is not refreshed, so the answer is the one the index the host already has gives, nothing that changes the host runs, and the upgrade and the refresh are reported as changes. The role never joins or logs in to a tailnet, whatever it updates.

Expand All @@ -53,11 +53,11 @@ Replacing the daemon restarts it, so connections over the host's tailnet, an SSH
- `github_runner_macos_host_power_ac_sleep`, `github_runner_macos_host_power_ac_displaysleep`, `github_runner_macos_host_power_ac_disksleep` (default 0 each): minutes of inactivity before the system sleeps, the display sleeps and the disks spin down on AC power; 0 never.
- `github_runner_macos_host_power_ac_womp`, `github_runner_macos_host_power_ac_standby`, `github_runner_macos_host_power_ac_powernap`, `github_runner_macos_host_power_ac_tcpkeepalive` (default 1 each): wake for network access, standby, Power Nap and keeping TCP connections alive during sleep; 1 on, 0 off.
- `github_runner_macos_host_disable_sleep` (default true): keep the Mac awake with its lid closed, with `pmset -a disablesleep 1`. False leaves the setting alone and never turns it off. See Power settings above.
- `github_runner_macos_host_homebrew_prefix` (default empty): the Homebrew prefix, whose `bin` holds the formula's `tailscaled` and `tailscale`. Empty takes `/opt/homebrew` on Apple silicon and `/usr/local` on Intel.
- `github_runner_macos_host_homebrew_prefix` (default empty): the Homebrew prefix, whose `opt/tailscale/bin` holds the formula's `tailscaled` and `tailscale`. Empty takes `/opt/homebrew` on Apple silicon and `/usr/local` on Intel.
- `github_runner_macos_host_tailscale_auth_key` (default empty), `github_runner_macos_host_tailscale_up_args` (default empty list): see Tailscale above.
- `github_runner_macos_host_tailscale_plist`: the launchd definition `tailscaled install-system-daemon` writes. Not a setting; a variable only so the tests can point it at a stand-in.
- `github_runner_macos_host_tailscale_update` (default true): keep the formula, its daemon and the standalone app current, as under Keeping Tailscale current above. Part of the Tailscale part, so `github_runner_macos_host_tailscale` false skips it too.
- `github_runner_macos_host_tailscale_daemon_binary`, `github_runner_macos_host_tailscale_app_cask`: where `tailscaled install-system-daemon` puts the daemon the launchd definition starts, and the Homebrew cask of the standalone app. Not settings; variables only so the tests can point them at stand-ins.
- `github_runner_macos_host_tailscale_daemon_binary`, `github_runner_macos_host_tailscale_app_cask`, `github_runner_macos_host_tailscale_app_bundle`: where `tailscaled install-system-daemon` puts the daemon the launchd definition starts, the Homebrew cask of the standalone app and where the cask puts the app, whose bundle says which release is installed. Not settings; variables only so the tests can point them at stand-ins.
- `github_runner_macos_host_path`: the `PATH` the Homebrew tasks run with, both Homebrew prefixes and the system directories.

`meta/argument_specs.yml` checks every variable when the role runs, and `defaults/main.yml` holds each default with its reasoning.
Expand All @@ -79,4 +79,4 @@ github_runner_macos_host_tailscale_up_args: [--hostname=example]

## Testing

`tests/unit/test_macos_host.py` covers the filter in `plugins/filter/macos_host.py` that compares `pmset -g custom` with the wanted values, the defaults against the argument specs and this file, the sudoers file's rendered content, ownership and `visudo` validation, the wiring in `playbooks/site.yml` (the role runs before the fleet play, and `become` appears nowhere else), and the role's tasks (`tests/macos_host/run.yml`) against stand-ins for `pmset`, `brew`, `tailscaled`, `tailscale` and `sudo` on `PATH`. The stand-ins that change the host refuse to run without the `sudo` stand-in, as the real ones do for a user who is not root, so the tests check that exactly the tasks that need it use it. They check that a Mac that sleeps gets only the settings that differ, `disablesleep` is set once and then left alone (and not read or set at all when its variable is false), a second run and `--check` change nothing, a host that already has the daemon is left alone, an older formula and daemon get one upgrade of the formula alone and one refresh of the daemon (a current one gets neither, and `--check` makes neither), the standalone app is upgraded only where it is installed, and the auth key reaches `tailscale up` on standard input and nowhere else.
`tests/unit/test_macos_host.py` covers the filter in `plugins/filter/macos_host.py` that compares `pmset -g custom` with the wanted values, the defaults against the argument specs and this file, the sudoers file's rendered content, ownership and `visudo` validation, the wiring in `playbooks/site.yml` (the role runs before the fleet play, and `become` appears nowhere else), and the role's tasks (`tests/macos_host/run.yml`) against stand-ins for `pmset`, `brew`, `tailscaled`, `tailscale` and `sudo` on `PATH`. The stand-ins that change the host refuse to run without the `sudo` stand-in, as the real ones do for a user who is not root, so the tests check that exactly the tasks that need it use it. They check that a Mac that sleeps gets only the settings that differ, `disablesleep` is set once and then left alone (and not read or set at all when its variable is false), a second run and `--check` change nothing, a host that already has the daemon is left alone, an older formula and daemon get one upgrade of the formula alone and one refresh of the daemon (a current one gets neither, and `--check` makes neither), a formula whose link step fails on the app's wrapper script is still installed, upgraded and run from its keg while a formula that did not install fails the run with Homebrew's output, the standalone app is upgraded only where it is installed and its bundle is behind the cask (one that has updated itself is left alone, and a cask installer that fails for an app that is behind fails the run), and the auth key reaches `tailscale up` on standard input and nowhere else.
6 changes: 4 additions & 2 deletions roles/github_runner_macos_host/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ github_runner_macos_host_disable_sleep: true

# The host's own Tailscale client, installed from the Homebrew formula and run as the tailscaled system daemon (a launchd daemon that starts at boot, before anyone logs in, so the host stays reachable on its tailnet). The role installs and starts the daemon and never joins a tailnet unless github_runner_macos_host_tailscale_auth_key is set. A host whose daemon is already installed, by this role or by hand, is left entirely alone.
github_runner_macos_host_tailscale: true
# The Homebrew prefix, whose bin directory holds the formula's tailscaled and tailscale. Empty takes /opt/homebrew on Apple silicon and /usr/local on Intel, from the host's machine fact.
# The Homebrew prefix, whose opt directory holds the formula's keg and so its tailscaled and tailscale, which the role runs from there whether or not Homebrew linked them into the prefix's bin directory. Empty takes /opt/homebrew on Apple silicon and /usr/local on Intel, from the host's machine fact.
github_runner_macos_host_homebrew_prefix: ""
# Not a setting: tailscaled install-system-daemon writes exactly this file, and its presence is how the role knows the daemon is installed. It is a variable only so the tests can point it at a stand-in.
github_runner_macos_host_tailscale_plist: /Library/LaunchDaemons/com.tailscale.tailscaled.plist
# Whether the role keeps the Tailscale software it finds current. The Homebrew formula is brought to its latest release (the formula and no other), and when the installed daemon differs from the formula's release, or the running daemon from the installed one, tailscaled install-system-daemon is run again so the daemon runs the new release: the daemon is a copy of the formula's binary (or a link to it, started once), so upgrading the formula alone leaves the old one running. Tailscale's standalone app, when it is installed as the Homebrew cask, is kept current as well; it is never installed. A host with none of them is skipped with a message, and the role never joins or logs in to a tailnet. Replacing the daemon restarts it, which drops the host's tailnet connections for a moment (its login is kept), and upgrading the app restarts its network extension. Part of the Tailscale part, so github_runner_macos_host_tailscale false skips it too.
# Whether the role keeps the Tailscale software it finds current. The Homebrew formula is brought to its latest release (the formula and no other; a keg Homebrew installed but could not link, because the standalone app's wrapper script holds /usr/local/bin/tailscale, is installed all the same, and the role runs the formula from its keg), and when the installed daemon differs from the formula's release, or the running daemon from the installed one, tailscaled install-system-daemon is run again so the daemon runs the new release: the daemon is a copy of the formula's binary (or a link to it, started once), so upgrading the formula alone leaves the old one running. Tailscale's standalone app, when it is installed as the Homebrew cask, is kept current as well, but only when its own bundle is behind the cask's release, because the app updates itself and Homebrew's record of it then lags; it is never installed. A host with none of them is skipped with a message, and the role never joins or logs in to a tailnet. Replacing the daemon restarts it, which drops the host's tailnet connections for a moment (its login is kept), and upgrading the app restarts its network extension. Part of the Tailscale part, so github_runner_macos_host_tailscale false skips it too.
github_runner_macos_host_tailscale_update: true
# Not settings: where tailscaled install-system-daemon puts the daemon that the launchd definition starts, and the Homebrew cask of Tailscale's standalone app. Variables only so the tests can point them at stand-ins.
github_runner_macos_host_tailscale_daemon_binary: /usr/local/bin/tailscaled
github_runner_macos_host_tailscale_app_cask: tailscale-app
# Not a setting: where the cask puts the standalone app, whose bundle says which release is installed (the app updates itself, so Homebrew's own record can be older). A variable only so the tests can point it at a stand-in.
github_runner_macos_host_tailscale_app_bundle: /Applications/Tailscale.app
# A Tailscale auth key (a secret only the tailnet's owner can create, from any source Ansible reads), empty by default. Set, the role runs tailscale up with it on a host whose daemon the role manages (the Homebrew formula's tailscale is installed) and whose tailnet login is still needed; empty, the role never joins or re-authenticates a tailnet.
github_runner_macos_host_tailscale_auth_key: ""
# Further arguments for tailscale up, such as --hostname=example or --advertise-tags=tag:example. Used only when the auth key is set.
Expand Down
Loading
Loading