@crvouga/dev-fleet 0.5.0 → 0.7.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 +4 -10
- package/ansible/macos/manager.yml +39 -2
- package/ansible/macos/policy.json +6 -1
- package/ansible/monitoring/agent.yml +2 -0
- package/ansible/ubuntu/baseline.yml +8 -0
- package/ansible/ubuntu/recovery.yml +2 -0
- package/ansible/ubuntu/tailscale.yml +2 -0
- package/ansible/ubuntu/tasks/github.yml +50 -0
- 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 +7 -4
- package/docs/github.md +107 -0
- package/docs/manager-recovery.md +78 -0
- package/docs/operations.md +9 -1
- package/docs/runbooks.md +15 -1
- package/docs/state.md +38 -6
- package/docs/topology.md +4 -5
- package/docs/ubuntu-sync.md +5 -5
- package/package.json +2 -2
- package/schema/check-ids.json +51 -3
- package/schema/fleet.schema.json +79 -1
- package/src/auth-checks.ts +14 -6
- package/src/check-owners.ts +163 -0
- package/src/cli/fleet-restore.ts +205 -0
- package/src/cli/fleet-state.ts +57 -5
- package/src/cli/fleet-sync-manager.ts +62 -15
- package/src/cli/fleet-sync-state.ts +18 -8
- package/src/cli/fleet-sync.ts +32 -3
- package/src/cli/github-verify.ts +57 -0
- package/src/cli/register-fleet.ts +33 -0
- package/src/cli/registry.ts +21 -1
- package/src/definition.ts +11 -1
- 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/node-converge-linux.ts +3 -2
- package/src/node-converge-shared.ts +6 -2
- package/src/platform/manager-host.ts +184 -0
- package/src/repair-catalog.ts +66 -2
- package/src/repair-plan.ts +25 -13
- package/src/secrets.ts +58 -13
- package/src/service-auth.ts +2 -2
- package/src/state/check.ts +7 -0
- package/src/state/holder.ts +50 -0
- package/src/state/litestream.ts +6 -3
- package/src/state/migrations/0001-documents.sql +13 -0
- package/src/state/migrations/0002-lease.sql +7 -0
- package/src/state/store.ts +125 -26
- package/src/state/sync.ts +131 -10
- package/src/worker-access-checks.ts +66 -16
- package/src/worker-readiness.ts +1 -1
package/docs/runbooks.md
CHANGED
|
@@ -8,7 +8,7 @@ person (a login, an approval, a choice) is marked **needs a person**.
|
|
|
8
8
|
`features/rules/runbooks.feature` fails when a code in the source has no
|
|
9
9
|
entry here, or a `devfleet` command quoted in the documents does not exist.
|
|
10
10
|
|
|
11
|
-
There are
|
|
11
|
+
There are 244 codes; 31 need a person.
|
|
12
12
|
|
|
13
13
|
## Bootstrap of a new or returning worker
|
|
14
14
|
|
|
@@ -147,6 +147,20 @@ There are 238 codes; 29 need a person.
|
|
|
147
147
|
- `GIT_REMOTE_REFUSED`: git remote refused. **Needs a person.**
|
|
148
148
|
- `GIT_REMOTE_UNREACHABLE`: git remote unreachable.
|
|
149
149
|
|
|
150
|
+
## GitHub sign-in
|
|
151
|
+
|
|
152
|
+
- **Meaning:** A node is not signed in to GitHub from the stored `github-token`, or the token belongs to another login. See [GitHub on every node](github.md).
|
|
153
|
+
- **Confirm:** The `worker:github-sync` and `github:*` checks of `devfleet fleet-status --workers`; on the node, `gh auth status`.
|
|
154
|
+
- **Fix:** Put a valid token for `worker.githubLogin` in the store as `github-token`, then run `devfleet fleet-sync` (a worker also retries every ten minutes).
|
|
155
|
+
- **Escalate:** A missing login or a token issued for another account needs a person to declare the login or issue a new token.
|
|
156
|
+
|
|
157
|
+
- `GITHUB_LOGIN_MISMATCH`: github login mismatch. **Needs a person.**
|
|
158
|
+
- `GITHUB_LOGIN_UNDECLARED`: github login undeclared. **Needs a person.**
|
|
159
|
+
- `GITHUB_SIGN_IN_FAILED`: github sign in failed.
|
|
160
|
+
- `GITHUB_TOKEN_MISSING`: github token missing.
|
|
161
|
+
- `GITHUB_TOKEN_REJECTED`: github token rejected.
|
|
162
|
+
- `GITHUB_UNREACHABLE`: github unreachable.
|
|
163
|
+
|
|
150
164
|
## Tailscale
|
|
151
165
|
|
|
152
166
|
- **Meaning:** A machine is not on the tailnet as the fleet declares it.
|
package/docs/state.md
CHANGED
|
@@ -64,9 +64,9 @@ arguments and never written to a report or a log.
|
|
|
64
64
|
|
|
65
65
|
**A new fleet.** Nothing more: the first `fleet-sync` seeds the bucket.
|
|
66
66
|
|
|
67
|
-
**A replacement manager
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
**A replacement manager.** The bucket already holds the fleet, so its state is
|
|
68
|
+
downloaded before anything is changed. Follow
|
|
69
|
+
[replacing the manager](manager-recovery.md).
|
|
70
70
|
|
|
71
71
|
**Runs the manager starts by itself.** A worker that hands `fleet-sync` to the
|
|
72
72
|
manager starts a command there that gets no environment and no project. Save
|
|
@@ -172,6 +172,7 @@ it records what it applied. Its report carries the result as the
|
|
|
172
172
|
| `local-ahead` | This machine has changes the bucket has not received | The next change, or `--push`, uploads them |
|
|
173
173
|
| `bucket-ahead` | Another manager changed the bucket; this copy is stale | The next change or `fleet-sync` takes the bucket's version |
|
|
174
174
|
| `unreachable` | The bucket did not answer, or did not take an upload | Changes still apply locally and are uploaded later |
|
|
175
|
+
| `corrupt` | The bucket answered with a version that cannot be read | Changes apply locally; `--restore --timestamp` repairs it |
|
|
175
176
|
| `diverged` | Both sides changed | Refused. `--restore --yes` takes the bucket's version |
|
|
176
177
|
|
|
177
178
|
A stale copy never overwrites the bucket. A machine that was offline while
|
|
@@ -210,6 +211,7 @@ once `fleet-sync` has fetched it back.
|
|
|
210
211
|
| `DEVFLEET_CONFIG` | A configuration file, instead of the project's `fleet.json` |
|
|
211
212
|
| `DEVFLEET_STATE` | Another state directory. Earlier files are not read for it |
|
|
212
213
|
| `DEVFLEET_BUCKET` | Another `bucket.json` |
|
|
214
|
+
| `DEVFLEET_MANAGER_HOLDER` | How the manager lease names this machine |
|
|
213
215
|
|
|
214
216
|
How a command reaches the bucket:
|
|
215
217
|
|
|
@@ -254,7 +256,37 @@ mise exec litestream -- litestream restore -no-expand-env \
|
|
|
254
256
|
~/.local/share/dev-fleet/state/fleet.db
|
|
255
257
|
```
|
|
256
258
|
|
|
259
|
+
`devfleet fleet-state --restore --timestamp 2026-11-01T00:00:00Z --yes` does
|
|
260
|
+
the same and then uploads that version as the bucket's current one. It is the
|
|
261
|
+
remedy for a `corrupt` replica: the bucket answers, but its latest version
|
|
262
|
+
cannot be read.
|
|
263
|
+
|
|
257
264
|
Litestream is pinned in `mise.toml` and installed the first time it is used.
|
|
258
|
-
The manager is the only machine that runs it.
|
|
259
|
-
|
|
260
|
-
|
|
265
|
+
The manager is the only machine that runs it.
|
|
266
|
+
|
|
267
|
+
## One manager at a time
|
|
268
|
+
|
|
269
|
+
The fleet state carries a **manager lease**: which machine is the manager,
|
|
270
|
+
and until when. Every change takes or renews it, and it reaches the bucket
|
|
271
|
+
with the change. A second machine (a replacement while the old manager still
|
|
272
|
+
runs, or an old manager that wakes up) reads it from the bucket before it
|
|
273
|
+
writes anything and stops with `FLEET_MANAGER_LEASE_HELD`. `fleet-state
|
|
274
|
+
--init` still restores the state on such a machine; it only writes nothing.
|
|
275
|
+
|
|
276
|
+
The lease runs for seven days after the last change. After that, any machine
|
|
277
|
+
may take it. Before that, `--take-over` (on `fleet-sync`, `fleet-state --init`
|
|
278
|
+
or `fleet-state --import`) makes this machine the manager; use it only once
|
|
279
|
+
the other one is gone ([replacing the manager](manager-recovery.md)).
|
|
280
|
+
|
|
281
|
+
A machine is named in the lease by a digest of its machine ID
|
|
282
|
+
(`/etc/machine-id` on Linux, the hardware UUID on macOS), never by its host
|
|
283
|
+
name, which a replacement shares. `DEVFLEET_MANAGER_HOLDER` names it instead.
|
|
284
|
+
|
|
285
|
+
## Schema version
|
|
286
|
+
|
|
287
|
+
The database records its schema version in `PRAGMA user_version`. The schema
|
|
288
|
+
is a series of numbered migrations (`src/state/migrations/0001-….sql`, …),
|
|
289
|
+
and a command brings an older database forward before it writes. A database a
|
|
290
|
+
newer dev-fleet wrote is refused with `FLEET_STATE_NEWER_THAN_CLI`, read or
|
|
291
|
+
written, and nothing in it changes: install the dev-fleet version the fleet
|
|
292
|
+
runs.
|
package/docs/topology.md
CHANGED
|
@@ -41,7 +41,8 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
|
|
|
41
41
|
manager is asleep, closed or offline, workers have no CLI proxy and no
|
|
42
42
|
dashboard; nothing fails over, because a singleton never runs twice.
|
|
43
43
|
- Its state (proxy OAuth/runtime state, the hub database) is backed up off the
|
|
44
|
-
machine as each service documents, so a replacement manager restores it
|
|
44
|
+
machine as each service documents, so a replacement manager restores it
|
|
45
|
+
([replacing the manager](manager-recovery.md)).
|
|
45
46
|
|
|
46
47
|
## Workers
|
|
47
48
|
|
|
@@ -63,7 +64,5 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
|
|
|
63
64
|
|
|
64
65
|
- **Add or replace a worker:** add it to `workers`, enroll it with
|
|
65
66
|
`fleet-sync`, done. Remove it by deleting the entry.
|
|
66
|
-
- **Replace the manager:**
|
|
67
|
-
|
|
68
|
-
[CLI proxy](../cliproxy/docs/migration.md),
|
|
69
|
-
[Beszel hub](../ansible/monitoring/README.md).
|
|
67
|
+
- **Replace the manager:** follow
|
|
68
|
+
[replacing the manager](manager-recovery.md).
|
package/docs/ubuntu-sync.md
CHANGED
|
@@ -187,7 +187,7 @@ verification and the CLIProxy singleton.
|
|
|
187
187
|
| Disk safety | `personal-fleet-disk-guard` kills only the top user-cgroup writer below 3 GiB free, never deletes files; hourly `personal-fleet-disk-gc.timer` deletes git-ignored build output (`node_modules`, `.cache`, `dist`, ...) in checkouts idle 7d (12h under 15 GiB free) with no process inside, plus unused Docker images and build cache, never source, worktrees or volumes; journal 1G; `/tmp` 25% of RAM, aged 2d hourly, a measured write probe (`TMP_QUOTA_EXHAUSTED`), and T3 agents use disk-backed `/var/tmp` (`TMPDIR`, aged 14d); Docker log rotation | not managed |
|
|
188
188
|
| Tailscale | pinned APT package, `tailscaled` enabled, native login required | Tailscale app, human login |
|
|
189
189
|
| Dashboard agent | pinned native binary, hardened `beszel-agent` systemd unit as user `beszel` | not installed |
|
|
190
|
-
| Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token
|
|
190
|
+
| Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token every 10min; `personal-worker-github` signs it in to GitHub from the stored token ([GitHub](github.md)) | the human's own Vault and GitHub sessions |
|
|
191
191
|
| Noninteractive sudo | validated `/etc/sudoers.d/personal-fleet-USER` | never; the human runs `manager.yml` once with `-K` |
|
|
192
192
|
| T3 worker app | pinned desktop `.deb` is the only T3 process (UI plus embedded server); GDM auto-login, user unit `t3code-desktop.service` bound to the graphical session with `Restart=always`; `personal-worker-recovery` runs a copy of `t3-health.sh` installed in `/usr/local/lib/personal-fleet` every minute: it stops and disables a still-running headless `t3code.service`, reports `T3_NO_GRAPHICAL_SESSION` and restarts the display manager (three times at most) when there is no session, starts a stopped app after `reset-failed`, restarts one whose local API missed three checks, and restarts one on another release than the installed one or with a deferred policy restart once it runs no agent turn; administrator policy | not a worker |
|
|
193
193
|
| T3 providers | the same managed instances (`fleet/agents/t3.json`) through locked T3 administrator policy | reconciled into the human's T3 settings by `fleet-sync` (`t3:providers`) with the ownership-safe `t3-apply` engine, so every T3 offers the identical set; unrelated and unmanaged instances are preserved. `fleet-sync` also applies the same proxy agent profiles (`configuration:apply`) and verifies the shared Vault gateway credential end to end with `provider:*:inference`, exactly as on workers |
|
|
@@ -209,10 +209,10 @@ vault kv get -field=FLEET_WORKER_TOKEN_WORKER_ONE secret/personal/prd \
|
|
|
209
209
|
| ssh worker-one 'vault login -address=https://vault.example.com -no-print -'
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
-
From then on the node's `personal-worker-auth` timer renews that token and
|
|
213
|
-
|
|
214
|
-
provider configuration: agent profiles
|
|
215
|
-
[Authentication and accounts](authentication.md).
|
|
212
|
+
From then on the node's `personal-worker-auth` timer renews that token and its
|
|
213
|
+
`personal-worker-github` timer keeps its GitHub sign-in current
|
|
214
|
+
([GitHub](github.md)). It renders no provider configuration: agent profiles
|
|
215
|
+
are applied by `fleet-sync`. See [Authentication and accounts](authentication.md).
|
|
216
216
|
|
|
217
217
|
## Ubuntu and remote-work safety
|
|
218
218
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crvouga/dev-fleet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Keeps a set of development machines in sync: one manager and many workers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:crvouga/dev-fleet",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=22"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "77efce7ad99a3d521169a5e773ad6b97ca21bc75"
|
|
76
76
|
}
|
package/schema/check-ids.json
CHANGED
|
@@ -50,6 +50,14 @@
|
|
|
50
50
|
"id": "auth:*",
|
|
51
51
|
"meaning": "A required login or capability works in this process context."
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
"id": "auth:copilot",
|
|
55
|
+
"meaning": "Copilot CLI uses the node's GitHub sign-in with a token it accepts."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "auth:github",
|
|
59
|
+
"meaning": "gh answers auth status in this context."
|
|
60
|
+
},
|
|
53
61
|
{
|
|
54
62
|
"id": "auth:toolchain",
|
|
55
63
|
"meaning": "The pinned toolchain, dependencies and Docker could be checked."
|
|
@@ -168,12 +176,52 @@
|
|
|
168
176
|
},
|
|
169
177
|
{
|
|
170
178
|
"id": "git:configuration",
|
|
171
|
-
"meaning": "The
|
|
179
|
+
"meaning": "The declared Git identity and the GitHub credential helper are written in the node's global Git configuration."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "github:expiry",
|
|
183
|
+
"meaning": "The GitHub token does not expire within the warning window."
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "github:git-credential",
|
|
187
|
+
"meaning": "git asks gh for its GitHub credential."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "github:org-access",
|
|
191
|
+
"meaning": "Single sign-on has authorised the GitHub token for each declared organisation."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"id": "github:permissions",
|
|
195
|
+
"meaning": "The GitHub token has every declared scope and can push to every declared repository."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": "github:principal",
|
|
199
|
+
"meaning": "The node is signed in to GitHub as its declared login."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "github:rate-limit",
|
|
203
|
+
"meaning": "The GitHub rate limit left is above the warning floor."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "github:sync",
|
|
207
|
+
"meaning": "fleet-sync signed this node in to GitHub from the stored credential."
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "github:token-current",
|
|
211
|
+
"meaning": "The node uses the GitHub token the store holds now."
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"id": "github:workflow",
|
|
215
|
+
"meaning": "fleet-github-verify completed clone, branch, commit, push, pull request, checks and comment on the sandbox repository."
|
|
172
216
|
},
|
|
173
217
|
{
|
|
174
218
|
"id": "login:*",
|
|
175
219
|
"meaning": "A native per-machine login is in place."
|
|
176
220
|
},
|
|
221
|
+
{
|
|
222
|
+
"id": "manager:boot-recovery",
|
|
223
|
+
"meaning": "The manager comes back by itself after a power cut or reboot."
|
|
224
|
+
},
|
|
177
225
|
{
|
|
178
226
|
"id": "manager:docker",
|
|
179
227
|
"meaning": "The manager runs the pinned Docker Desktop."
|
|
@@ -483,8 +531,8 @@
|
|
|
483
531
|
"meaning": "The worker runs the declared revision."
|
|
484
532
|
},
|
|
485
533
|
{
|
|
486
|
-
"id": "worker:github-
|
|
487
|
-
"meaning": "The worker
|
|
534
|
+
"id": "worker:github-sync",
|
|
535
|
+
"meaning": "The worker is signed in to GitHub from the stored credential as the declared login."
|
|
488
536
|
},
|
|
489
537
|
{
|
|
490
538
|
"id": "worker:package:*",
|
package/schema/fleet.schema.json
CHANGED
|
@@ -611,8 +611,24 @@
|
|
|
611
611
|
}
|
|
612
612
|
},
|
|
613
613
|
"description": "Only with the ntfy notify target: the ntfy topic URL fleet-watch publishes to."
|
|
614
|
+
},
|
|
615
|
+
"github-token": {
|
|
616
|
+
"type": "object",
|
|
617
|
+
"additionalProperties": false,
|
|
618
|
+
"required": ["path", "field"],
|
|
619
|
+
"properties": {
|
|
620
|
+
"path": {
|
|
621
|
+
"type": "string",
|
|
622
|
+
"pattern": "^[A-Za-z0-9._/-]+$"
|
|
623
|
+
},
|
|
624
|
+
"field": {
|
|
625
|
+
"type": "string",
|
|
626
|
+
"pattern": "^[A-Za-z0-9_]+$"
|
|
627
|
+
}
|
|
628
|
+
}
|
|
614
629
|
}
|
|
615
|
-
}
|
|
630
|
+
},
|
|
631
|
+
"description": "Required for every secret except github-token, which defaults to the field GITHUB_TOKEN beside proxy-client-key."
|
|
616
632
|
}
|
|
617
633
|
}
|
|
618
634
|
}
|
|
@@ -662,6 +678,68 @@
|
|
|
662
678
|
"maximum": 720
|
|
663
679
|
}
|
|
664
680
|
}
|
|
681
|
+
},
|
|
682
|
+
"github": {
|
|
683
|
+
"description": "How every node signs in to GitHub. The fleet login is worker.githubLogin.",
|
|
684
|
+
"type": "object",
|
|
685
|
+
"additionalProperties": false,
|
|
686
|
+
"properties": {
|
|
687
|
+
"manager": {
|
|
688
|
+
"description": "fleet: the manager signs in as the fleet login. {login}: a person's own login, checked and never replaced.",
|
|
689
|
+
"oneOf": [
|
|
690
|
+
{
|
|
691
|
+
"const": "fleet"
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
"type": "object",
|
|
695
|
+
"additionalProperties": false,
|
|
696
|
+
"required": ["login"],
|
|
697
|
+
"properties": {
|
|
698
|
+
"login": {
|
|
699
|
+
"type": "string",
|
|
700
|
+
"pattern": "^[a-zA-Z0-9-]{1,39}$"
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
]
|
|
705
|
+
},
|
|
706
|
+
"credential": {
|
|
707
|
+
"description": "shared: one github-token for every node. per-node: github-token-<node> for each node.",
|
|
708
|
+
"enum": ["shared", "per-node"]
|
|
709
|
+
},
|
|
710
|
+
"scopes": {
|
|
711
|
+
"description": "Token scopes every node must have, such as repo and workflow.",
|
|
712
|
+
"type": "array",
|
|
713
|
+
"uniqueItems": true,
|
|
714
|
+
"items": {
|
|
715
|
+
"type": "string",
|
|
716
|
+
"pattern": "^[a-z:_]{1,40}$"
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
"repositories": {
|
|
720
|
+
"description": "owner/name of repositories every node must be able to push to.",
|
|
721
|
+
"type": "array",
|
|
722
|
+
"uniqueItems": true,
|
|
723
|
+
"items": {
|
|
724
|
+
"type": "string",
|
|
725
|
+
"pattern": "^[a-zA-Z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}$"
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
"organizations": {
|
|
729
|
+
"description": "Organisations whose single sign-on must have authorised the token.",
|
|
730
|
+
"type": "array",
|
|
731
|
+
"uniqueItems": true,
|
|
732
|
+
"items": {
|
|
733
|
+
"type": "string",
|
|
734
|
+
"pattern": "^[a-zA-Z0-9-]{1,39}$"
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
"sandbox": {
|
|
738
|
+
"type": "string",
|
|
739
|
+
"pattern": "^[a-zA-Z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}$",
|
|
740
|
+
"description": "owner/name of a repository fleet-github-verify branches, pushes to and opens pull requests on, then cleans up."
|
|
741
|
+
}
|
|
742
|
+
}
|
|
665
743
|
}
|
|
666
744
|
}
|
|
667
745
|
}
|
package/src/auth-checks.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { codingAuth, serviceAuth } from './service-auth';
|
|
|
6
6
|
import { workstationChecks } from './workstation-checks';
|
|
7
7
|
import { toolchainChecks } from './toolchain-checks';
|
|
8
8
|
import { workerSelection } from '../bootstrap/worker-tools';
|
|
9
|
+
import { githubChecks } from './github/checks';
|
|
10
|
+
import { secretsProvider } from './secrets';
|
|
9
11
|
import type { Capability, Check, Runner, CommandResult } from './types';
|
|
10
12
|
|
|
11
13
|
export async function authCheck(
|
|
@@ -20,6 +22,17 @@ export async function authCheck(
|
|
|
20
22
|
return codingAuth(capability, run);
|
|
21
23
|
if (capability === 'services') return serviceAuth(run);
|
|
22
24
|
if (capability === 'workstation') return workstationChecks(root, run);
|
|
25
|
+
if (capability === 'github')
|
|
26
|
+
return githubChecks(run, {
|
|
27
|
+
definition,
|
|
28
|
+
...(nodeId === undefined ? {} : { node: nodeId }),
|
|
29
|
+
secret: secretsProvider(definition.secrets, {
|
|
30
|
+
run: async (argv) => {
|
|
31
|
+
const result = await run(argv, { timeoutMs: 20_000 });
|
|
32
|
+
return result.ok ? result.stdout : null;
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
23
36
|
if (capability === 'toolchain')
|
|
24
37
|
return toolchainChecks(
|
|
25
38
|
root,
|
|
@@ -32,16 +45,11 @@ export async function authCheck(
|
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
async function basicAuth(
|
|
35
|
-
capability: '
|
|
48
|
+
capability: 'vault' | 'vscode',
|
|
36
49
|
run: Runner,
|
|
37
50
|
root: string
|
|
38
51
|
): Promise<Check[]> {
|
|
39
52
|
const spec = {
|
|
40
|
-
github: {
|
|
41
|
-
command: ['gh', 'auth', 'status', '--active'],
|
|
42
|
-
action:
|
|
43
|
-
'Authenticate on this node with gh auth login, then gh auth setup-git.',
|
|
44
|
-
},
|
|
45
53
|
vault: {
|
|
46
54
|
command: [
|
|
47
55
|
'vault',
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/** Who repairs a check: a repair recipe (`repair`), or the reason only a
|
|
2
|
+
* person can (`manual`). Every check ID dev-fleet reports matches one entry,
|
|
3
|
+
* by its exact ID or by a prefix ending in `*` (`beszel:*`). The most
|
|
4
|
+
* specific match wins. `login:*` is the sign-in recipe named after the rest
|
|
5
|
+
* of an `auth:` check's ID.
|
|
6
|
+
*/
|
|
7
|
+
export interface CheckOwner {
|
|
8
|
+
pattern: string;
|
|
9
|
+
repair?: string;
|
|
10
|
+
manual?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const IDENTITY =
|
|
14
|
+
'It needs a person: a sign-in, an approval or a choice only the operator can make.';
|
|
15
|
+
const EVIDENCE =
|
|
16
|
+
'It reports how a command or a run went, not the state of a machine: repeat the command once its remedy is followed.';
|
|
17
|
+
const DECLARATION =
|
|
18
|
+
'The fleet configuration (fleet.json) or the roster is wrong: only the operator edits them.';
|
|
19
|
+
const HARDWARE =
|
|
20
|
+
'It is a property of the hardware, the firmware or the operating system release, which no command changes.';
|
|
21
|
+
|
|
22
|
+
export const CHECK_OWNERS: CheckOwner[] = [
|
|
23
|
+
// Toolchain and workstation: automatic.
|
|
24
|
+
{ pattern: 'tool:*', repair: 'workspace' },
|
|
25
|
+
{ pattern: 'tool:docker-*', repair: 'docker' },
|
|
26
|
+
{ pattern: 'tool:github-auth*', repair: 'login:github' },
|
|
27
|
+
{ pattern: 'dependencies', repair: 'workspace' },
|
|
28
|
+
{ pattern: 'auth:toolchain', repair: 'workspace' },
|
|
29
|
+
{ pattern: 'workstation:*', repair: 'workstation' },
|
|
30
|
+
{ pattern: 'auth:*', repair: 'login:*' },
|
|
31
|
+
{ pattern: 'auth:vscode', manual: IDENTITY },
|
|
32
|
+
{ pattern: 'auth:workstation', manual: IDENTITY },
|
|
33
|
+
{ pattern: 't3:settings', repair: 't3' },
|
|
34
|
+
{ pattern: 't3:api', repair: 't3' },
|
|
35
|
+
// The checks that fail when a machine breaks.
|
|
36
|
+
{ pattern: 'worker:service-recovery', repair: 'recovery' },
|
|
37
|
+
{ pattern: 'worker:capacity', repair: 'disk' },
|
|
38
|
+
{ pattern: 'worker:capacity:*', repair: 'disk' },
|
|
39
|
+
{ pattern: 'beszel:agent', repair: 'monitoring' },
|
|
40
|
+
{ pattern: 'beszel:service', repair: 'monitoring' },
|
|
41
|
+
{ pattern: 'beszel:installation', repair: 'monitoring' },
|
|
42
|
+
{ pattern: 'beszel:protocol', repair: 'monitoring' },
|
|
43
|
+
{ pattern: 'tailscale:configuration', repair: 'network' },
|
|
44
|
+
{ pattern: 'tailscale:online', repair: 'network' },
|
|
45
|
+
{ pattern: 'tailscale:reachability', repair: 'network' },
|
|
46
|
+
{ pattern: 'state:replica', repair: 'state' },
|
|
47
|
+
{ pattern: 'state:*', repair: 'state' },
|
|
48
|
+
// What only a person can do.
|
|
49
|
+
{
|
|
50
|
+
pattern: 'beszel:*',
|
|
51
|
+
manual:
|
|
52
|
+
'The monitoring hub and its accounts are converged by fleet-sync on the manager; follow the remedy, then run fleet-sync.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: 'tailscale:*',
|
|
56
|
+
manual:
|
|
57
|
+
'Signing a machine in to the tailnet and approving it is a person’s; the daemon itself is repaired by the network recipe.',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
pattern: 'state:bucket',
|
|
61
|
+
manual:
|
|
62
|
+
'The bucket and its credentials are the operator’s: name them in fleet.json and pass the credentials.',
|
|
63
|
+
},
|
|
64
|
+
{ pattern: 'state:definition', manual: DECLARATION },
|
|
65
|
+
{
|
|
66
|
+
pattern: 'worker:*',
|
|
67
|
+
manual:
|
|
68
|
+
'Re-converge the worker with fleet-sync: it applies the whole worker policy at the approved revision.',
|
|
69
|
+
},
|
|
70
|
+
{ pattern: 'worker:power-loss-recovery', manual: HARDWARE },
|
|
71
|
+
{ pattern: 'worker:github-principal', manual: IDENTITY },
|
|
72
|
+
{ pattern: 'worker:vault-renewal', manual: IDENTITY },
|
|
73
|
+
{
|
|
74
|
+
pattern: 'manager:*',
|
|
75
|
+
manual:
|
|
76
|
+
'The manager converges itself in fleet-sync; a setting that needs an administrator is applied with ansible-playbook -K ansible/macos/manager.yml.',
|
|
77
|
+
},
|
|
78
|
+
{ pattern: 'manager:identity', manual: DECLARATION },
|
|
79
|
+
{
|
|
80
|
+
pattern: 'manager:docker',
|
|
81
|
+
manual:
|
|
82
|
+
'Docker Desktop is installed and updated by a person; no playbook installs it.',
|
|
83
|
+
},
|
|
84
|
+
{ pattern: 'macos:*', manual: HARDWARE },
|
|
85
|
+
{ pattern: 'ubuntu:release', manual: HARDWARE },
|
|
86
|
+
{
|
|
87
|
+
pattern: 'ubuntu:*',
|
|
88
|
+
manual:
|
|
89
|
+
'Re-converge the worker with fleet-sync: it applies the Ubuntu baseline.',
|
|
90
|
+
},
|
|
91
|
+
{ pattern: 'platform:*', manual: HARDWARE },
|
|
92
|
+
{
|
|
93
|
+
pattern: 'ssh:*',
|
|
94
|
+
manual:
|
|
95
|
+
'SSH trust (host keys, keys in the agent, the alias) is set up by a person once per machine; dev-fleet never accepts a host key for you.',
|
|
96
|
+
},
|
|
97
|
+
{ pattern: 'login:*', manual: IDENTITY },
|
|
98
|
+
{ pattern: 'provider:*', manual: IDENTITY },
|
|
99
|
+
{
|
|
100
|
+
pattern: 'agent:*',
|
|
101
|
+
manual:
|
|
102
|
+
'Re-converge the machine with fleet-sync; an agent that needs a sign-in is a person’s.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
pattern: 't3:*',
|
|
106
|
+
manual:
|
|
107
|
+
'T3 pairing and its private route are approved by a person; fleet-sync applies the rest.',
|
|
108
|
+
},
|
|
109
|
+
{ pattern: 'vscode:*', manual: IDENTITY },
|
|
110
|
+
{
|
|
111
|
+
pattern: 'git:*',
|
|
112
|
+
manual:
|
|
113
|
+
'Uncommitted or unpushed work belongs to a person; dev-fleet never resets a checkout.',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: 'git:configuration',
|
|
117
|
+
manual:
|
|
118
|
+
'fleet-sync writes the Git identity and credential helper with ansible/github/git.yml; run it from the manager.',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
pattern: 'github:*',
|
|
122
|
+
manual:
|
|
123
|
+
'fleet-sync signs every node in to GitHub from the stored github-token (a worker retries every ten minutes); a token that is missing, expired or short of a scope or organisation needs a person to issue a new one.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
pattern: 'github:git-credential',
|
|
127
|
+
manual:
|
|
128
|
+
'fleet-sync writes the GitHub credential helper with ansible/github/git.yml when the git block is declared; run it from the manager.',
|
|
129
|
+
},
|
|
130
|
+
{ pattern: 'github:rate-limit', manual: EVIDENCE },
|
|
131
|
+
{ pattern: 'github:workflow', manual: EVIDENCE },
|
|
132
|
+
{ pattern: 'fleet:*', manual: DECLARATION },
|
|
133
|
+
{ pattern: 'enrollment:*', manual: DECLARATION },
|
|
134
|
+
{ pattern: 'notify:*', manual: DECLARATION },
|
|
135
|
+
{
|
|
136
|
+
pattern: 'proxy:*',
|
|
137
|
+
manual:
|
|
138
|
+
'The CLI proxy is installed with devfleet cliproxy install and its accounts are signed in by a person.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
pattern: 'accounts:roster-current',
|
|
142
|
+
manual:
|
|
143
|
+
'The roster reaches every node with devfleet fleet-sync --only accounts, run from the manager.',
|
|
144
|
+
},
|
|
145
|
+
{ pattern: 'accounts:native-login', manual: IDENTITY },
|
|
146
|
+
{ pattern: 'configuration:*', manual: EVIDENCE },
|
|
147
|
+
{ pattern: 'source:*', manual: EVIDENCE },
|
|
148
|
+
{ pattern: 'bootstrap:*', manual: EVIDENCE },
|
|
149
|
+
{ pattern: 'rollout:*', manual: EVIDENCE },
|
|
150
|
+
{ pattern: 'repair:*', manual: EVIDENCE },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const matches = (pattern: string, id: string): boolean =>
|
|
154
|
+
pattern.endsWith('*') ? id.startsWith(pattern.slice(0, -1)) : id === pattern;
|
|
155
|
+
|
|
156
|
+
/** The entry that owns a check, or null for an ID nobody registered. */
|
|
157
|
+
export function checkOwner(id: string): CheckOwner | null {
|
|
158
|
+
return (
|
|
159
|
+
CHECK_OWNERS.filter((owner) => matches(owner.pattern, id)).sort(
|
|
160
|
+
(a, b) => b.pattern.length - a.pattern.length
|
|
161
|
+
)[0] ?? null
|
|
162
|
+
);
|
|
163
|
+
}
|