@crvouga/dev-fleet 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/ansible/macos/policy.json +1 -0
- package/ansible/platforms.json +111 -0
- package/ansible/ubuntu/authorize-sudo.sh +7 -16
- package/ansible/ubuntu/baseline.yml +6 -28
- package/ansible/ubuntu/disk-gc.py +186 -4
- package/ansible/ubuntu/policy.json +18 -3
- package/ansible/ubuntu/t3-desktop.yml +2 -2
- package/ansible/ubuntu/tailscale.yml +20 -9
- package/ansible/ubuntu/tasks/dpkg.yml +46 -0
- package/ansible/ubuntu/tasks/github.yml +50 -0
- package/ansible/ubuntu/tasks/install.yml +38 -0
- package/ansible/ubuntu/tasks/packages.yml +4 -55
- package/ansible/ubuntu/tasks/release.yml +25 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -5
- package/ansible/ubuntu/tasks/snapshot.yml +50 -0
- package/ansible/ubuntu/tasks/unattended-upgrades.yml +24 -0
- package/bootstrap/fleet-remote-bootstrap.sh +18 -41
- package/bootstrap/worker-auth.ts +6 -7
- package/bootstrap/worker-github.ts +55 -44
- package/docs/authentication.md +4 -1
- package/docs/consumer-setup.md +37 -30
- package/docs/declared.md +4 -5
- package/docs/github.md +107 -0
- package/docs/operations.md +1 -1
- package/docs/runbooks.md +17 -6
- package/docs/support-matrix.md +36 -0
- package/docs/ubuntu-sync.md +14 -12
- package/docs/worker-contract.md +28 -1
- package/package.json +2 -2
- package/schema/check-ids.json +55 -7
- package/schema/fleet.schema.json +86 -4
- package/src/auth-checks.ts +14 -6
- package/src/cli/fleet-sync-manager.ts +9 -0
- package/src/cli/fleet-sync-remote.ts +1 -1
- package/src/cli/github-verify.ts +57 -0
- package/src/cli/register-fleet.ts +2 -0
- package/src/cli/registry.ts +7 -0
- package/src/definition.ts +29 -8
- package/src/github/checks.ts +412 -0
- package/src/github/converge.ts +62 -0
- package/src/github/copilot.ts +54 -0
- package/src/github/credential.ts +21 -0
- package/src/github/policy.ts +141 -0
- package/src/github/sync.ts +123 -0
- package/src/github/workflow.ts +223 -0
- package/src/machine/platform.ts +48 -28
- package/src/machine/support-matrix.ts +76 -0
- package/src/manager-sync.ts +15 -7
- package/src/network-definition.ts +1 -1
- package/src/node-converge-linux.ts +3 -2
- package/src/node-converge-shared.ts +6 -2
- package/src/node-converge.ts +1 -1
- package/src/platform/debian-version.ts +62 -0
- package/src/platform/ubuntu-audit.ts +11 -4
- package/src/platform/ubuntu-release.ts +41 -0
- package/src/remote-bootstrap.ts +2 -0
- package/src/repair-catalog.ts +2 -2
- package/src/secrets.ts +58 -13
- package/src/service-auth.ts +2 -2
- package/src/worker-access-checks.ts +66 -16
- package/src/worker-host-checks.ts +36 -10
- package/src/worker-readiness.ts +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The one way a pinned Ubuntu package is installed: from the snapshot,
|
|
3
|
+
# waiting for the APT lock, and never downgrading. A pin is a minimum: a
|
|
4
|
+
# package Ubuntu's security updates moved past its pin is left as it is and
|
|
5
|
+
# reported as a warning (docs/worker-contract.md). `install_pins` maps each
|
|
6
|
+
# package to its pinned version.
|
|
7
|
+
- ansible.builtin.import_tasks: snapshot.yml
|
|
8
|
+
- name: Read the installed package versions
|
|
9
|
+
ansible.builtin.package_facts:
|
|
10
|
+
manager: apt
|
|
11
|
+
- name: Compare each installed pinned package with its pin
|
|
12
|
+
ansible.builtin.command:
|
|
13
|
+
argv:
|
|
14
|
+
- dpkg
|
|
15
|
+
- --compare-versions
|
|
16
|
+
- '{{ ansible_facts.packages[item][0].version }}'
|
|
17
|
+
- ge
|
|
18
|
+
- '{{ install_pins[item] }}'
|
|
19
|
+
loop: "{{ install_pins.keys() | select('in', ansible_facts.packages) | list }}"
|
|
20
|
+
register: pin_comparison
|
|
21
|
+
changed_when: false
|
|
22
|
+
failed_when: pin_comparison.rc not in [0, 1]
|
|
23
|
+
check_mode: false
|
|
24
|
+
- name: Select the pinned packages that are absent or older than their pin
|
|
25
|
+
ansible.builtin.set_fact:
|
|
26
|
+
below_pin: "{{ (install_pins.keys() | reject('in', ansible_facts.packages) | list) + (pin_comparison.results | selectattr('rc', 'equalto', 1) | map(attribute='item') | list) }}"
|
|
27
|
+
- name: Install the pinned version of each package below its pin, without restarting services
|
|
28
|
+
ansible.builtin.apt:
|
|
29
|
+
name: '{{ item }}={{ install_pins[item] }}'
|
|
30
|
+
state: present
|
|
31
|
+
lock_timeout: '{{ aptLockTimeoutSeconds }}'
|
|
32
|
+
allow_downgrade: false
|
|
33
|
+
allow_unauthenticated: false
|
|
34
|
+
fail_on_autoremove: true
|
|
35
|
+
policy_rc_d: 101
|
|
36
|
+
environment:
|
|
37
|
+
APT_CONFIG: /etc/personal-fleet/apt.conf
|
|
38
|
+
loop: '{{ below_pin }}'
|
|
@@ -1,56 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
owner: root
|
|
7
|
-
group: root
|
|
8
|
-
mode: '0755'
|
|
9
|
-
- name: Declare snapshot sources used only for managed package transactions
|
|
10
|
-
ansible.builtin.copy:
|
|
11
|
-
dest: /etc/personal-fleet/ubuntu.sources
|
|
12
|
-
owner: root
|
|
13
|
-
group: root
|
|
14
|
-
mode: '0644'
|
|
15
|
-
backup: true
|
|
16
|
-
content: |
|
|
17
|
-
Types: deb
|
|
18
|
-
URIs: https://snapshot.ubuntu.com/ubuntu/{{ snapshot }}/
|
|
19
|
-
Suites: resolute resolute-updates resolute-security
|
|
20
|
-
Components: main universe restricted multiverse
|
|
21
|
-
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
|
22
|
-
register: snapshot_sources
|
|
23
|
-
- name: Isolate snapshot selection from unrelated APT operations
|
|
24
|
-
ansible.builtin.copy:
|
|
25
|
-
dest: /etc/personal-fleet/apt.conf
|
|
26
|
-
owner: root
|
|
27
|
-
group: root
|
|
28
|
-
mode: '0644'
|
|
29
|
-
content: |
|
|
30
|
-
Dir::Etc::sourcelist "/etc/personal-fleet/ubuntu.sources";
|
|
31
|
-
Dir::Etc::sourceparts "-";
|
|
32
|
-
DPkg::Lock::Timeout "{{ aptLockTimeoutSeconds }}";
|
|
33
|
-
- name: Inspect immutable snapshot package index
|
|
34
|
-
ansible.builtin.stat:
|
|
35
|
-
path: '/var/lib/apt/lists/snapshot.ubuntu.com_ubuntu_{{ snapshot }}_dists_resolute_InRelease'
|
|
36
|
-
register: snapshot_index
|
|
37
|
-
- name: Fetch signed snapshot indexes after source change or interrupted setup
|
|
38
|
-
ansible.builtin.apt:
|
|
39
|
-
update_cache: true
|
|
40
|
-
cache_valid_time: 0
|
|
41
|
-
lock_timeout: '{{ aptLockTimeoutSeconds }}'
|
|
42
|
-
environment:
|
|
43
|
-
APT_CONFIG: /etc/personal-fleet/apt.conf
|
|
44
|
-
when: snapshot_sources.changed or not snapshot_index.stat.exists
|
|
45
|
-
- name: Install exact managed packages without downgrading or restarting services
|
|
46
|
-
ansible.builtin.apt:
|
|
47
|
-
name: '{{ item.key }}={{ item.value }}'
|
|
48
|
-
state: present
|
|
49
|
-
lock_timeout: '{{ aptLockTimeoutSeconds }}'
|
|
50
|
-
allow_downgrade: false
|
|
51
|
-
allow_unauthenticated: false
|
|
52
|
-
fail_on_autoremove: true
|
|
53
|
-
policy_rc_d: 101
|
|
54
|
-
environment:
|
|
55
|
-
APT_CONFIG: /etc/personal-fleet/apt.conf
|
|
56
|
-
loop: '{{ packages | dict2items }}'
|
|
2
|
+
- ansible.builtin.include_tasks: install.yml
|
|
3
|
+
vars:
|
|
4
|
+
install_pins: '{{ packages }}'
|
|
5
|
+
- ansible.builtin.import_tasks: unattended-upgrades.yml
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The accepted releases are named once, in policy.json. This selects the
|
|
3
|
+
# entry of the running release and refuses everything else.
|
|
4
|
+
- name: Select the accepted release this machine runs
|
|
5
|
+
ansible.builtin.set_fact:
|
|
6
|
+
ubuntu_release: "{{ releases | selectattr('release', 'equalto', ansible_facts.distribution_version) | first | default({}) }}"
|
|
7
|
+
- name: Require an accepted release, its codename and one of its architectures
|
|
8
|
+
ansible.builtin.assert:
|
|
9
|
+
that:
|
|
10
|
+
- ansible_facts.distribution == 'Ubuntu'
|
|
11
|
+
- ubuntu_release.release is defined
|
|
12
|
+
- ansible_facts.distribution_release == ubuntu_release.codename
|
|
13
|
+
- "(ansible_facts.architecture | replace('x86_64', 'amd64') | replace('aarch64', 'arm64')) in ubuntu_release.architectures"
|
|
14
|
+
fail_msg: UBUNTU_BASELINE_MISMATCH; workers run a release docs/support-matrix.md lists. No distribution upgrade is permitted.
|
|
15
|
+
- name: Pin the packages of this release, its own pins over the common ones
|
|
16
|
+
ansible.builtin.set_fact:
|
|
17
|
+
packages: '{{ packages | combine(ubuntu_release.packages | default({})) }}'
|
|
18
|
+
- name: Read the kernel release to tell WSL2 apart from a machine of its own
|
|
19
|
+
ansible.builtin.slurp:
|
|
20
|
+
src: /proc/sys/kernel/osrelease
|
|
21
|
+
register: kernel_release
|
|
22
|
+
- name: Refuse WSL2 by name
|
|
23
|
+
ansible.builtin.assert:
|
|
24
|
+
that: not (kernel_release.content | b64decode | lower is search('microsoft|wsl'))
|
|
25
|
+
fail_msg: WORKER_ENVIRONMENT_UNSUPPORTED; WSL2 is not a worker. Run the worker on a physical or virtual machine of its own (docs/support-matrix.md).
|
|
@@ -49,10 +49,11 @@
|
|
|
49
49
|
line: /swap.fleet none swap sw 0 0
|
|
50
50
|
when: active_swap.stdout | trim == ''
|
|
51
51
|
|
|
52
|
-
- name: Install the systemd userspace OOM killer
|
|
53
|
-
ansible.builtin.
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
- name: Install the systemd userspace OOM killer at its pin
|
|
53
|
+
ansible.builtin.include_tasks: install.yml
|
|
54
|
+
vars:
|
|
55
|
+
install_pins:
|
|
56
|
+
systemd-oomd: "{{ packages['systemd-oomd'] }}"
|
|
56
57
|
- name: Create memory and disk policy directories
|
|
57
58
|
ansible.builtin.file:
|
|
58
59
|
path: '{{ item }}'
|
|
@@ -276,7 +277,7 @@
|
|
|
276
277
|
After=docker.service
|
|
277
278
|
[Service]
|
|
278
279
|
Type=oneshot
|
|
279
|
-
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-inode-percent {{ resourceSafety.inodeWarnFreePercent }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}
|
|
280
|
+
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-inode-percent {{ resourceSafety.inodeWarnFreePercent }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}{% for root in resourceSafety.diskGcWorktreeRoots %} --worktree-root {{ root }}{% endfor %} --worktree-idle-days {{ resourceSafety.diskGcWorktreeIdleDays }}{% for cache, mib in resourceSafety.diskGcCacheCapsMiB.items() %} --cache-cap {{ cache }}={{ mib }}{% endfor %} --keep-newest /var/cache/personal-fleet={{ resourceSafety.diskGcPackagesKept }} --mise {{ development_home }}/.local/bin/mise --user {{ development_user }}
|
|
280
281
|
|
|
281
282
|
TimeoutStartSec=2h
|
|
282
283
|
Nice=19
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The snapshot every managed package comes from, separate from the system's
|
|
3
|
+
# own sources. Included before any pinned package is installed; a second
|
|
4
|
+
# inclusion in one run changes nothing.
|
|
5
|
+
- name: Create owned APT configuration directory
|
|
6
|
+
ansible.builtin.file:
|
|
7
|
+
path: /etc/personal-fleet
|
|
8
|
+
state: directory
|
|
9
|
+
owner: root
|
|
10
|
+
group: root
|
|
11
|
+
mode: '0755'
|
|
12
|
+
- name: Declare snapshot sources used only for managed package transactions
|
|
13
|
+
ansible.builtin.copy:
|
|
14
|
+
dest: /etc/personal-fleet/ubuntu.sources
|
|
15
|
+
owner: root
|
|
16
|
+
group: root
|
|
17
|
+
mode: '0644'
|
|
18
|
+
backup: true
|
|
19
|
+
content: |
|
|
20
|
+
Types: deb
|
|
21
|
+
URIs: https://snapshot.ubuntu.com/ubuntu/{{ snapshot }}/
|
|
22
|
+
Suites: {{ ubuntu_release.codename }} {{ ubuntu_release.codename }}-updates {{ ubuntu_release.codename }}-security
|
|
23
|
+
Components: main universe restricted multiverse
|
|
24
|
+
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
|
25
|
+
register: snapshot_sources
|
|
26
|
+
# Fetching the snapshot indexes must leave the indexes of the system's own
|
|
27
|
+
# sources in place: the system's APT and unattended upgrades still use them.
|
|
28
|
+
- name: Isolate snapshot selection from unrelated APT operations
|
|
29
|
+
ansible.builtin.copy:
|
|
30
|
+
dest: /etc/personal-fleet/apt.conf
|
|
31
|
+
owner: root
|
|
32
|
+
group: root
|
|
33
|
+
mode: '0644'
|
|
34
|
+
content: |
|
|
35
|
+
Dir::Etc::sourcelist "/etc/personal-fleet/ubuntu.sources";
|
|
36
|
+
Dir::Etc::sourceparts "-";
|
|
37
|
+
APT::Get::List-Cleanup "false";
|
|
38
|
+
DPkg::Lock::Timeout "{{ aptLockTimeoutSeconds }}";
|
|
39
|
+
- name: Inspect immutable snapshot package index
|
|
40
|
+
ansible.builtin.stat:
|
|
41
|
+
path: '/var/lib/apt/lists/snapshot.ubuntu.com_ubuntu_{{ snapshot }}_dists_{{ ubuntu_release.codename }}_InRelease'
|
|
42
|
+
register: snapshot_index
|
|
43
|
+
- name: Fetch signed snapshot indexes after source change or interrupted setup
|
|
44
|
+
ansible.builtin.apt:
|
|
45
|
+
update_cache: true
|
|
46
|
+
cache_valid_time: 0
|
|
47
|
+
lock_timeout: '{{ aptLockTimeoutSeconds }}'
|
|
48
|
+
environment:
|
|
49
|
+
APT_CONFIG: /etc/personal-fleet/apt.conf
|
|
50
|
+
when: snapshot_sources.changed or not snapshot_index.stat.exists
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Ubuntu installs security updates by itself, and they keep flowing: a pin is
|
|
3
|
+
# a minimum, never a hold. What runs unattended is declared here so it is
|
|
4
|
+
# known: security updates only, never a reboot, never a removal. The fleet's
|
|
5
|
+
# own file sorts after the package's 50unattended-upgrades and replaces its
|
|
6
|
+
# origins.
|
|
7
|
+
- name: Declare which updates Ubuntu installs unattended
|
|
8
|
+
ansible.builtin.copy:
|
|
9
|
+
dest: /etc/apt/apt.conf.d/52personal-fleet-unattended-upgrades
|
|
10
|
+
owner: root
|
|
11
|
+
group: root
|
|
12
|
+
mode: '0644'
|
|
13
|
+
content: |
|
|
14
|
+
// Managed by personal Fleet: unattended upgrades
|
|
15
|
+
APT::Periodic::Update-Package-Lists "1";
|
|
16
|
+
APT::Periodic::Unattended-Upgrade "1";
|
|
17
|
+
#clear Unattended-Upgrade::Allowed-Origins;
|
|
18
|
+
#clear Unattended-Upgrade::Origins-Pattern;
|
|
19
|
+
Unattended-Upgrade::Allowed-Origins {
|
|
20
|
+
"${distro_id}:${distro_codename}-security";
|
|
21
|
+
};
|
|
22
|
+
Unattended-Upgrade::Automatic-Reboot "false";
|
|
23
|
+
Unattended-Upgrade::Remove-Unused-Dependencies "false";
|
|
24
|
+
Unattended-Upgrade::Remove-New-Unused-Dependencies "false";
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
# Executed over strict SSH from the selected verified Git revision. stdin is
|
|
3
3
|
# one line holding the base64 fleet configuration (empty when there is none),
|
|
4
4
|
# then the base64 Git bundle (nothing in cached mode). All executable content
|
|
5
|
-
# remains in this tracked source.
|
|
5
|
+
# remains in this tracked source. The Ubuntu releases workers may run are
|
|
6
|
+
# named once, in ansible/ubuntu/policy.json; the manager passes them in.
|
|
6
7
|
set -eu
|
|
7
8
|
umask 077
|
|
8
9
|
revision=$1
|
|
9
10
|
bundle_sha=$2
|
|
10
11
|
node=$3
|
|
11
12
|
source_mode=${4:-transfer}
|
|
13
|
+
releases=${5:-}
|
|
12
14
|
case "$source_mode" in cached|transfer) ;; *) exit 2;; esac
|
|
15
|
+
case "$releases" in *[!0-9.\ ]*|'') exit 2;; esac
|
|
13
16
|
case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
|
|
14
17
|
[ "${#revision}" = 40 ] || exit 2
|
|
15
18
|
case "$bundle_sha" in *[!a-f0-9]*|'') exit 2;; esac
|
|
@@ -26,7 +29,8 @@ remedy() {
|
|
|
26
29
|
case "$1" in
|
|
27
30
|
FLEET_REMOTE_LOCKED) echo 'Another fleet operation holds this machine. Wait for it to finish; rerun fleet-sync afterwards. Never remove the lock while its holder runs.';;
|
|
28
31
|
ROOT_DEVELOPMENT_USER_REJECTED) echo 'Connect as the non-root developer account the fleet configuration names.';;
|
|
29
|
-
UBUNTU_BASELINE_MISMATCH|UBUNTU_ARCHITECTURE_UNSUPPORTED|
|
|
32
|
+
UBUNTU_BASELINE_MISMATCH|UBUNTU_ARCHITECTURE_UNSUPPORTED|PLATFORM_UNSUPPORTED) echo 'Workers run an Ubuntu release docs/support-matrix.md lists, on x86_64 or aarch64. Reinstall the machine; fleet-sync never upgrades an operating system.';;
|
|
33
|
+
WORKER_ENVIRONMENT_UNSUPPORTED) echo 'WSL2 is not a worker. Run the worker on a physical or virtual machine of its own (docs/support-matrix.md).';;
|
|
30
34
|
FLEET_STATE_PATH_UNSAFE) echo 'A fleet state directory under ~/.local/share/personal is a symbolic link. Replace it with a real directory.';;
|
|
31
35
|
BOOTSTRAP_CAPACITY_UNVERIFIED|BOOTSTRAP_DISK_CAPACITY_REQUIRED) echo 'Free at least 2 GiB and 50000 inodes on the home file system, then rerun fleet-sync.';;
|
|
32
36
|
SOURCE_DIGEST_MISMATCH) echo 'The bundle changed in transit. Rerun fleet-sync; if it repeats, check the SSH route.';;
|
|
@@ -37,8 +41,6 @@ remedy() {
|
|
|
37
41
|
APT_DISK_FULL) echo 'Package installation ran out of disk space. Free space under /var and /usr, then rerun fleet-sync.';;
|
|
38
42
|
DPKG_INTERRUPTED) echo 'An earlier package installation that fleet-sync did not start was interrupted. Run sudo dpkg --configure -a once no package process is running, then rerun fleet-sync.';;
|
|
39
43
|
APT_UPDATE_FAILED|APT_INSTALL_FAILED) echo 'Run sudo apt-get update and install the bootstrap packages by hand to see the error, then rerun fleet-sync.';;
|
|
40
|
-
XCODE_COMMAND_LINE_TOOLS_REQUIRED) echo 'Install the Xcode Command Line Tools (xcode-select --install), then rerun fleet-sync.';;
|
|
41
|
-
BOOTSTRAP_LOCK_TOOL_REQUIRED) echo 'Install python3 so the operation lock can be taken, then rerun fleet-sync.';;
|
|
42
44
|
SOURCE_CACHE_CHANGED|SOURCE_STATUS_FAILED|SOURCE_CHECKOUT_CONFLICT) echo 'The fleet-owned checkout of this revision holds changes fleet-sync did not make. Inspect and remove that revisions directory, then rerun fleet-sync.';;
|
|
43
45
|
CONFIGURATION_INSTALL_FAILED) echo 'The fleet configuration could not be written under ~/.config/dev-fleet. Check that directory, then rerun fleet-sync.';;
|
|
44
46
|
TOOLCHAIN_*) echo 'The pinned toolchain did not converge. Rerun fleet-sync; it resumes where it stopped. If it repeats, run devfleet fleet-status --node for this worker.';;
|
|
@@ -64,22 +66,14 @@ trap '' HUP
|
|
|
64
66
|
trap 'exit 1' INT TERM
|
|
65
67
|
# No shell startup assumptions, and no implicit root development user.
|
|
66
68
|
[ "$(id -u)" != 0 ] || die ROOT_DEVELOPMENT_USER_REJECTED
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac ;;
|
|
76
|
-
Darwin)
|
|
77
|
-
major=$(sw_vers -productVersion | cut -d. -f1)
|
|
78
|
-
case "$major" in ''|*[!0-9]*) die MACOS_BASELINE_MISMATCH;; esac
|
|
79
|
-
[ "$major" -ge 14 ] || die MACOS_BASELINE_MISMATCH
|
|
80
|
-
case "$(uname -m)" in arm64|x86_64) ;; *) die MACOS_ARCHITECTURE_UNSUPPORTED;; esac ;;
|
|
81
|
-
*) die PLATFORM_UNSUPPORTED;;
|
|
82
|
-
esac
|
|
69
|
+
# Workers are the Ubuntu releases the policy accepts (docs/support-matrix.md).
|
|
70
|
+
[ "$(uname -s)" = Linux ] || die PLATFORM_UNSUPPORTED
|
|
71
|
+
# shellcheck source=/dev/null
|
|
72
|
+
. /etc/os-release
|
|
73
|
+
[ "$ID" = ubuntu ] || die UBUNTU_BASELINE_MISMATCH
|
|
74
|
+
case " $releases " in *" $VERSION_ID "*) ;; *) die UBUNTU_BASELINE_MISMATCH;; esac
|
|
75
|
+
case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac
|
|
76
|
+
! grep -qiE 'microsoft|wsl' /proc/sys/kernel/osrelease 2>/dev/null || die WORKER_ENVIRONMENT_UNSUPPORTED
|
|
83
77
|
root="$HOME/.local/share/personal/fleet"
|
|
84
78
|
# Refuse symlinked state roots rather than allowing writes through them.
|
|
85
79
|
for path in "$HOME/.local" "$HOME/.local/share" "$HOME/.local/share/personal" "$root"; do
|
|
@@ -89,14 +83,8 @@ mkdir -p "$root"
|
|
|
89
83
|
phase=lock
|
|
90
84
|
[ ! -L "$root/remote-operation.lock" ] || exit 1
|
|
91
85
|
exec 9>"$root/remote-operation.lock"
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
if command -v flock >/dev/null 2>&1; then
|
|
95
|
-
flock -n 9 || die FLEET_REMOTE_LOCKED
|
|
96
|
-
else
|
|
97
|
-
command -v python3 >/dev/null 2>&1 || die BOOTSTRAP_LOCK_TOOL_REQUIRED
|
|
98
|
-
python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || die FLEET_REMOTE_LOCKED
|
|
99
|
-
fi
|
|
86
|
+
# The lock follows this shell's open descriptor and is released when it exits.
|
|
87
|
+
flock -n 9 || die FLEET_REMOTE_LOCKED
|
|
100
88
|
# The operation record: a manager that lost its session reads it to follow
|
|
101
89
|
# this operation to its end instead of starting another (see
|
|
102
90
|
# src/remote-operation.ts). The result of the last operation is beside it.
|
|
@@ -132,14 +120,9 @@ if [ "$source_mode" = transfer ]; then
|
|
|
132
120
|
bundle=$(mktemp "$root/source.XXXXXX")
|
|
133
121
|
# Leave no executable payload copies; this temporary file is only Git data.
|
|
134
122
|
base64 -d >"$bundle"
|
|
135
|
-
|
|
136
|
-
printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
|
|
137
|
-
else
|
|
138
|
-
[ "$(shasum -a 256 "$bundle" | cut -d' ' -f1)" = "$bundle_sha" ] || die SOURCE_DIGEST_MISMATCH
|
|
139
|
-
fi
|
|
123
|
+
printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
|
|
140
124
|
fi
|
|
141
125
|
phase=prerequisites
|
|
142
|
-
if [ "$platform" = Linux ]; then
|
|
143
126
|
missing=''
|
|
144
127
|
for package in git curl ca-certificates unzip xz-utils build-essential python3 ansible-core; do
|
|
145
128
|
dpkg-query -W -f='${db:Status-Status}' "$package" 2>/dev/null | grep -qx installed || missing="$missing $package"
|
|
@@ -171,12 +154,6 @@ if [ -n "$missing" ]; then
|
|
|
171
154
|
rm -f -- "$pending" "$apt_log"
|
|
172
155
|
apt_log=''
|
|
173
156
|
fi
|
|
174
|
-
else
|
|
175
|
-
# Apple Command Line Tools provide git and python3; Ansible runs through the
|
|
176
|
-
# pinned uv tool runner installed by the shared mise toolchain below.
|
|
177
|
-
xcode-select -p >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
|
|
178
|
-
command -v git >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
|
|
179
|
-
fi
|
|
180
157
|
phase=source-verification
|
|
181
158
|
deployment="$root/revisions/$revision"
|
|
182
159
|
[ ! -L "$root/revisions" ] && [ ! -L "$deployment" ] || exit 1
|
|
@@ -235,7 +212,7 @@ else
|
|
|
235
212
|
toolchain_warm=''
|
|
236
213
|
rm -f -- "$status_file" "$ready_file"
|
|
237
214
|
fi
|
|
238
|
-
|
|
215
|
+
bounded='timeout 1800'
|
|
239
216
|
# shellcheck disable=SC2086
|
|
240
217
|
if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --node "$node" --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
|
|
241
218
|
tool_phase=$(sed -n 's/.*"phase":"\([a-z0-9-]*\)".*/\1/p' "$status_file" 2>/dev/null) || tool_phase=''
|
package/bootstrap/worker-auth.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** Private scheduled job: renews this worker's own Vault token, and nothing
|
|
2
|
+
* else. GitHub sign-in is its own job (worker-github.ts), so a failure of
|
|
3
|
+
* one never stops or hides the other. Raw auth output is never logged.
|
|
4
|
+
*/
|
|
3
5
|
import { spawnSync } from 'node:child_process';
|
|
4
6
|
import { resolveVaultConfig, usesVault } from '../src/vault-config';
|
|
5
7
|
|
|
@@ -49,14 +51,11 @@ if (import.meta.main) {
|
|
|
49
51
|
jobId: 'vault-self-renewal',
|
|
50
52
|
operation: 'vault.token.renew',
|
|
51
53
|
});
|
|
52
|
-
const
|
|
53
|
-
const github = vault && syncWorkerGithub();
|
|
54
|
-
const ok = vault && github;
|
|
54
|
+
const ok = renewWorkerVault();
|
|
55
55
|
log.info('job_finished', {
|
|
56
56
|
jobId: 'vault-self-renewal',
|
|
57
57
|
operation: 'vault.token.renew',
|
|
58
|
-
vaultAuthenticated:
|
|
59
|
-
githubAuthenticated: github,
|
|
58
|
+
vaultAuthenticated: ok,
|
|
60
59
|
outcome: ok ? 'success' : 'authentication_required',
|
|
61
60
|
durationMs: Date.now() - started,
|
|
62
61
|
retryDelayMs: 600000,
|
|
@@ -1,47 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/** GitHub sign-in of this node from the stored credential; never copies a
|
|
2
|
+
* login directory. Runs from the timer (`personal-worker-github`) and from
|
|
3
|
+
* every sync. One JSON line, never a token or raw output.
|
|
4
|
+
*/
|
|
5
|
+
import { hostname } from 'node:os';
|
|
6
|
+
import { fleet } from '../src/fleet-config';
|
|
7
|
+
import { secretsProvider } from '../src/secrets';
|
|
8
|
+
import { runCommand } from '../src/runner';
|
|
9
|
+
import { hostId } from '../src/topology';
|
|
10
|
+
import { githubCredential } from '../src/github/credential';
|
|
11
|
+
import { githubNode } from '../src/github/policy';
|
|
12
|
+
import { syncGithub, type GithubSyncResult } from '../src/github/sync';
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// The
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
14
|
+
/** The exit status of each outcome, so the unit's result names it too. */
|
|
15
|
+
export const EXIT_CODES: Record<GithubSyncResult['code'], number> = {
|
|
16
|
+
GITHUB_OK: 0,
|
|
17
|
+
GITHUB_TOKEN_MISSING: 10,
|
|
18
|
+
GITHUB_TOKEN_REJECTED: 11,
|
|
19
|
+
GITHUB_LOGIN_MISMATCH: 12,
|
|
20
|
+
GITHUB_UNREACHABLE: 13,
|
|
21
|
+
GITHUB_SIGN_IN_FAILED: 14,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const GITHUB_JOB = 'dev-fleet-github';
|
|
25
|
+
|
|
26
|
+
if (import.meta.main) {
|
|
27
|
+
const started = Date.now();
|
|
28
|
+
// The manager is never a declared worker; `--node` names a worker whose
|
|
29
|
+
// host name is not its id.
|
|
30
|
+
const flag = process.argv.indexOf('--node');
|
|
31
|
+
const id = flag > 0 ? process.argv[flag + 1] : hostId(hostname());
|
|
32
|
+
const target = githubNode(fleet, id);
|
|
33
|
+
const result: GithubSyncResult =
|
|
34
|
+
target.login === null
|
|
35
|
+
? { code: 'GITHUB_LOGIN_MISMATCH', adopted: false }
|
|
36
|
+
: await syncGithub({
|
|
37
|
+
run: runCommand,
|
|
38
|
+
token: githubCredential(
|
|
39
|
+
fleet,
|
|
40
|
+
target.node,
|
|
41
|
+
secretsProvider(fleet.secrets)
|
|
42
|
+
),
|
|
43
|
+
login: target.login,
|
|
44
|
+
// A person's own login on the manager is measured, never replaced.
|
|
45
|
+
apply: target.managed,
|
|
46
|
+
});
|
|
47
|
+
process.stdout.write(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
name: GITHUB_JOB,
|
|
50
|
+
event: 'job_finished',
|
|
51
|
+
role: target.role,
|
|
52
|
+
code: result.code,
|
|
53
|
+
adopted: result.adopted,
|
|
54
|
+
durationMs: Date.now() - started,
|
|
55
|
+
}) + '\n'
|
|
45
56
|
);
|
|
46
|
-
|
|
57
|
+
process.exitCode = EXIT_CODES[result.code];
|
|
47
58
|
}
|
package/docs/authentication.md
CHANGED
|
@@ -54,6 +54,7 @@ Every failure code is in [the runbooks](runbooks.md).
|
|
|
54
54
|
| `monitoring-account-password` | yes | `DEVFLEET_SECRET_MONITORING_ACCOUNT_PASSWORD` | `~/.config/dev-fleet/secrets/monitoring-account-password` |
|
|
55
55
|
| `monitoring-tunnel-token` | yes | `DEVFLEET_SECRET_MONITORING_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/monitoring-tunnel-token` |
|
|
56
56
|
| `proxy-tunnel-token` | yes | `DEVFLEET_SECRET_PROXY_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/proxy-tunnel-token` |
|
|
57
|
+
| `github-token` | when its feature is declared | `DEVFLEET_SECRET_GITHUB_TOKEN` | `~/.config/dev-fleet/secrets/github-token` |
|
|
57
58
|
| `monitoring-backup-access-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_ACCESS_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-access-key` |
|
|
58
59
|
| `monitoring-backup-secret-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_SECRET_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-secret-key` |
|
|
59
60
|
| `notify-webhook-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_WEBHOOK_URL` | `~/.config/dev-fleet/secrets/notify-webhook-url` |
|
|
@@ -67,10 +68,11 @@ Every failure code is in [the runbooks](runbooks.md).
|
|
|
67
68
|
|
|
68
69
|
| Check | Measured in |
|
|
69
70
|
| ----------------------------- | ------------------------------------------ |
|
|
71
|
+
| `auth:github` | `src/github/checks.ts` |
|
|
70
72
|
| `auth:vault` | `src/cli/fleet-sync-vault.ts` |
|
|
71
73
|
| `worker:git-identity` | `src/git-identity.ts` |
|
|
72
74
|
| `worker:git-push-auth` | `src/definition.ts`, `src/git-identity.ts` |
|
|
73
|
-
| `worker:github-
|
|
75
|
+
| `worker:github-sync` | `src/worker-access-checks.ts` |
|
|
74
76
|
| `worker:proxy-authentication` | `src/worker-access-checks.ts` |
|
|
75
77
|
| `worker:vault-renewal` | `src/worker-access-checks.ts` |
|
|
76
78
|
|
|
@@ -84,6 +86,7 @@ Every failure code is in [the runbooks](runbooks.md).
|
|
|
84
86
|
| -------------------------------- | -------------------------------------- | ------------------------------------- |
|
|
85
87
|
| `personal-fleet-disk-gc.timer` | `ansible/ubuntu/tasks/resources.yml` | OnBootSec=10min, OnCalendar=hourly |
|
|
86
88
|
| `personal-worker-auth.timer` | `ansible/ubuntu/tasks/credentials.yml` | OnBootSec=2min, OnUnitActiveSec=10min |
|
|
89
|
+
| `personal-worker-github.timer` | `ansible/ubuntu/tasks/github.yml` | OnBootSec=2min, OnUnitActiveSec=10min |
|
|
87
90
|
| `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=1min, OnUnitActiveSec=1min |
|
|
88
91
|
| `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=5min, OnUnitActiveSec=15min |
|
|
89
92
|
|
package/docs/consumer-setup.md
CHANGED
|
@@ -153,35 +153,36 @@ npx devfleet fleet-state --example > fleet.json
|
|
|
153
153
|
Then replace every placeholder. Each field, from
|
|
154
154
|
[`schema/fleet.schema.json`](../schema/fleet.schema.json):
|
|
155
155
|
|
|
156
|
-
| Field | Value
|
|
157
|
-
| ----------------------------------------------------- |
|
|
158
|
-
| `version` | `3`
|
|
159
|
-
| `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it.
|
|
160
|
-
| `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials.
|
|
161
|
-
| `secrets` | See [secrets](#secrets).
|
|
162
|
-
| `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`.
|
|
163
|
-
| `manager.os` | `"macOS"`
|
|
164
|
-
| `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot.
|
|
165
|
-
| `manager.network.user` | The manager's login name (`id -un`).
|
|
166
|
-
| `manager.neverSleepOnPower` | `true` (workers depend on the manager's services).
|
|
167
|
-
| `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it.
|
|
168
|
-
| `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`.
|
|
169
|
-
| `workers[].os` | `"linux"`
|
|
170
|
-
| `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager).
|
|
171
|
-
| `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere.
|
|
172
|
-
| `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`).
|
|
173
|
-
| `workers[].network.t3Port` | `3773`
|
|
174
|
-
| `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`.
|
|
175
|
-
| `services.monitoring.hubPort` | `8443`
|
|
176
|
-
| `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard.
|
|
177
|
-
| `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with.
|
|
178
|
-
| `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as.
|
|
179
|
-
| `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }
|
|
180
|
-
| `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`.
|
|
181
|
-
| `worker.githubLogin` | The GitHub login every worker must be signed in as.
|
|
182
|
-
| `
|
|
183
|
-
| `worker.
|
|
184
|
-
| `worker.
|
|
156
|
+
| Field | Value |
|
|
157
|
+
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `version` | `3` |
|
|
159
|
+
| `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it. |
|
|
160
|
+
| `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials. |
|
|
161
|
+
| `secrets` | See [secrets](#secrets). |
|
|
162
|
+
| `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`. |
|
|
163
|
+
| `manager.os` | `"macOS"` |
|
|
164
|
+
| `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot. |
|
|
165
|
+
| `manager.network.user` | The manager's login name (`id -un`). |
|
|
166
|
+
| `manager.neverSleepOnPower` | `true` (workers depend on the manager's services). |
|
|
167
|
+
| `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it. |
|
|
168
|
+
| `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`. |
|
|
169
|
+
| `workers[].os` | `"linux"` |
|
|
170
|
+
| `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager). |
|
|
171
|
+
| `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere. |
|
|
172
|
+
| `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`). |
|
|
173
|
+
| `workers[].network.t3Port` | `3773` |
|
|
174
|
+
| `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`. |
|
|
175
|
+
| `services.monitoring.hubPort` | `8443` |
|
|
176
|
+
| `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard. |
|
|
177
|
+
| `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with. |
|
|
178
|
+
| `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as. |
|
|
179
|
+
| `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }`: a release of the [support matrix](support-matrix.md). |
|
|
180
|
+
| `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`. |
|
|
181
|
+
| `worker.githubLogin` | The GitHub login every worker must be signed in as. |
|
|
182
|
+
| `github` (optional) | Who the manager signs in to GitHub as, per-node tokens, required scopes; see [GitHub](github.md). |
|
|
183
|
+
| `worker.vaultUser` | The Vault user each worker's token is issued for. |
|
|
184
|
+
| `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed). |
|
|
185
|
+
| `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy). |
|
|
185
186
|
|
|
186
187
|
Use the same values in every place they repeat. Validate before going on:
|
|
187
188
|
|
|
@@ -206,6 +207,10 @@ dev-fleet asks for four secrets by purpose: `proxy-client-key`,
|
|
|
206
207
|
| `file` | `{ "provider": "file" }` (optional `"directory"`) | One private file per name, default `~/.config/dev-fleet/secrets/NAME`. |
|
|
207
208
|
| `env` | `{ "provider": "env" }` | `DEVFLEET_SECRET_PROXY_CLIENT_KEY` and so on (upper case, `_` for `-`). |
|
|
208
209
|
|
|
210
|
+
A fifth, `github-token`, is the GitHub token of `worker.githubLogin`: every node
|
|
211
|
+
is signed in with it ([GitHub](github.md)). With `vault` it needs no location
|
|
212
|
+
of its own: it defaults to the field `GITHUB_TOKEN` beside `proxy-client-key`.
|
|
213
|
+
|
|
209
214
|
Only `vault` gives each worker its own login and renewal and gives the CLI
|
|
210
215
|
proxy its encrypted backup; with the other three those two features are not
|
|
211
216
|
available yet. You never write a secret value into the project; the human puts
|
|
@@ -352,7 +357,9 @@ Read `summary` and `remedy.action` of the failing check. The common ones:
|
|
|
352
357
|
| `TAILSCALE_NODE_OFFLINE` | The worker is off or off the tailnet. It is listed and skipped; others continue. |
|
|
353
358
|
| `TAILSCALE_DEVICE_REPLACED` | The tailnet device behind a worker changed. Nothing is written to it until the human confirms the replacement. |
|
|
354
359
|
| `SUDO_AUTHORIZATION_REQUIRED` | The worker account cannot use sudo without a password. Run the first sync interactively. |
|
|
355
|
-
| `UBUNTU_BASELINE_MISMATCH` | The worker is not Ubuntu
|
|
360
|
+
| `UBUNTU_BASELINE_MISMATCH` | The worker is not an Ubuntu release the [support matrix](support-matrix.md) lists. |
|
|
361
|
+
| `WORKER_ENVIRONMENT_UNSUPPORTED` | The worker is WSL2 or a container. Use a physical or virtual machine of its own. |
|
|
362
|
+
| `PACKAGE_NEWER_THAN_PIN` (warning) | Ubuntu's security updates moved a pinned package past its pin. It is kept; the pin is a minimum. |
|
|
356
363
|
| `ROOT_DEVELOPMENT_USER_REJECTED` | The worker's SSH user is root. Use a normal account with sudo. |
|
|
357
364
|
| `BOOTSTRAP_DISK_CAPACITY_REQUIRED` | Less than 2 GiB free (or too few inodes) where the fleet keeps its files on the worker. |
|
|
358
365
|
| `FLEET_REMOTE_LOCKED` | Another operation is running on that worker. Wait and run again. |
|