@andrian.yablonskyy/thub-coordinator 1.1.16 → 1.1.17
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 +1 -1
- package/package.json +1 -1
- package/scripts/install-systemd-unit.js +4 -12
- package/src/web/routes.js +8 -4
- package/systemd/thub-coordinator.service +5 -3
- package/test/updates.test.js +20 -4
- package/views/help/_coordinator.pug +8 -9
- package/views/help/_kubernetes.pug +3 -3
- package/views/layout.pug +7 -4
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ The Coordinator's own version (`ver. X.Y.Z`) is shown under the TestHub logo, to
|
|
|
172
172
|
**Versions and self-update.** The Coordinator checks the npm registry for the latest Coordinator, Agent and Client every `updates.checkIntervalMin` (default 15 minutes, `0` disables; the older `checkIntervalHours` is ignored; `updates.registry` for a private mirror) and immediately with the navbar's **check for updates** button (admins; also on the Agents and Resources pages), which reports in a toast whether the Coordinator is current and which connected resources can update their Client. A newer Coordinator shows as an **Update app to X.Y.Z** button in the navbar for admins (a badge under the logo for viewers); Agents and Resources show each one's version with an **update available** or pending **→ vX.Y.Z** badge. Admins request self-updates per agent/resource (**Update**, which becomes **Cancel update**) or with **Update all agents** / **Update all clients**, always to the latest version:
|
|
173
173
|
- Clients get a `self-update` command on their next heartbeat and install it through their root `thub-client-update` helper, once none of the host's instances is busy.
|
|
174
174
|
- Agents install it at the start of their next run (`GET /api/v1/agents/me/update`), then re-run the command on the new version.
|
|
175
|
-
- The Coordinator itself, only when started with `--self-update` (
|
|
175
|
+
- The Coordinator itself, only when started with `--self-update` (a systemd install has it; elsewhere, e.g. in a container, it's off and the dashboard shows nothing about Coordinator updates): the **Update app** button writes `<dataDir>/update-request.json`; the root `thub-coordinator-update.path` unit (installed by `sudo npm i -g`) runs `npm i -g` for it, and the postinstall restarts the service (the dashboard and API are briefly down). Logs: `journalctl -u thub-coordinator-update`. Without that unit, use `thub-admin self-update` on the host.
|
|
176
176
|
|
|
177
177
|
A request is cleared when the Agent/Client reports the new version.
|
|
178
178
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-coordinator",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "TestHub Coordinator — job queue, resource registry, scheduler, heartbeat monitor, log/artifact store and web dashboard",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"engines": {
|
|
@@ -34,23 +34,15 @@ const fs = require('node:fs'),
|
|
|
34
34
|
// The checked-in unit only has placeholders — fill in the target user and
|
|
35
35
|
// wherever *this* install's node and server.js actually are, so it works
|
|
36
36
|
// regardless of npm prefix or an nvm-managed Node.
|
|
37
|
-
// `--self-update` (README §10.2) is off by default; once added to the
|
|
38
|
-
// installed unit's ExecStart, a re-install keeps it there.
|
|
39
|
-
function selfUpdateFlag(existingUnit = UNIT_DEST){
|
|
40
|
-
try {
|
|
41
|
-
return /^ExecStart=.*\s--self-update(\s|$)/m.test(fs.readFileSync(existingUnit, 'utf8')) ? ' --self-update' : '';
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return '';
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
37
|
function renderUnit(user, paths){
|
|
49
38
|
return fs.readFileSync(UNIT_SRC, 'utf8')
|
|
50
39
|
.replace(/^User=.*$/m, `User=${user.name}`)
|
|
51
40
|
.replace(/^Group=.*$/m, `Group=${user.gid}`)
|
|
52
41
|
.replace(/^Environment=THUB_COORDINATOR_CONFIG=.*$/m, `Environment=THUB_COORDINATOR_CONFIG=${paths.configPath}`)
|
|
53
|
-
|
|
42
|
+
// --self-update: the dashboard's "Update app" (README §10.2), through the
|
|
43
|
+
// root thub-coordinator-update units installed below — on for a systemd
|
|
44
|
+
// install. To turn it off, override ExecStart in a drop-in.
|
|
45
|
+
.replace(/^ExecStart=.*$/m, `ExecStart=${process.execPath} ${SERVER_PATH} --self-update`)
|
|
54
46
|
.replace(/^ReadWritePaths=.*$/m, `ReadWritePaths=${[paths.dataDir, paths.dbDir].filter(Boolean).join(' ')}`);
|
|
55
47
|
}
|
|
56
48
|
|
package/src/web/routes.js
CHANGED
|
@@ -807,9 +807,13 @@ function createWebRouter({ services, config }){
|
|
|
807
807
|
return { type: 'danger', text: `Update check failed — the npm registry couldn't be reached (${s.error}). Nothing was checked.` };
|
|
808
808
|
}
|
|
809
809
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
810
|
+
// The Coordinator's own version only when it can update itself
|
|
811
|
+
// (--self-update); Agents and Clients are always reported.
|
|
812
|
+
if (s.selfUpdate){
|
|
813
|
+
parts.push(s.coordinatorUpdate
|
|
814
|
+
? `Coordinator v${s.coordinatorVersion} → v${s.coordinatorUpdate} available.`
|
|
815
|
+
: `Coordinator is up to date (v${s.coordinatorVersion}).`);
|
|
816
|
+
}
|
|
813
817
|
|
|
814
818
|
const connected = services.registry.list().filter((r) => r.status !== RESOURCE_STATES.OUT_OF_SERVICE && r.last_heartbeat_at),
|
|
815
819
|
outdated = connected.filter((r) => r.client_version && isNewer(s.latest.client, r.client_version)),
|
|
@@ -834,7 +838,7 @@ function createWebRouter({ services, config }){
|
|
|
834
838
|
if (s.error){
|
|
835
839
|
parts.push(`Some checks failed: ${s.error}`);
|
|
836
840
|
}
|
|
837
|
-
const behind = s.coordinatorUpdate || outdated.length || unknown.length;
|
|
841
|
+
const behind = (s.selfUpdate && s.coordinatorUpdate) || outdated.length || unknown.length;
|
|
838
842
|
return { type: behind || s.error ? 'warning' : 'success', text: parts.join('\n') };
|
|
839
843
|
}));
|
|
840
844
|
|
|
@@ -3,8 +3,10 @@ Description=TestHub Coordinator
|
|
|
3
3
|
After=network-online.target
|
|
4
4
|
Wants=network-online.target
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# --self-update lets admins update the Coordinator from the dashboard
|
|
7
|
+
# ("Update app", README §10.2) through thub-coordinator-update.path. To turn
|
|
8
|
+
# it off: sudo systemctl edit thub-coordinator, then [Service], ExecStart=
|
|
9
|
+
# (empty) and ExecStart=<the same line without --self-update>.
|
|
8
10
|
# User, Group, THUB_COORDINATOR_CONFIG, ReadWritePaths and ExecStart are
|
|
9
11
|
# rewritten by scripts/install-systemd-unit.js at install time to the user
|
|
10
12
|
# who ran `sudo npm i -g` and this install's real node/server.js paths.
|
|
@@ -13,7 +15,7 @@ Type=simple
|
|
|
13
15
|
User=thub
|
|
14
16
|
Group=thub
|
|
15
17
|
Environment=THUB_COORDINATOR_CONFIG=/var/lib/thub/.config/thub/coordinator.json
|
|
16
|
-
ExecStart=/usr/bin/node /usr/lib/node_modules/@andrian.yablonskyy/thub-coordinator/src/server.js
|
|
18
|
+
ExecStart=/usr/bin/node /usr/lib/node_modules/@andrian.yablonskyy/thub-coordinator/src/server.js --self-update
|
|
17
19
|
Restart=always
|
|
18
20
|
RestartSec=2
|
|
19
21
|
NoNewPrivileges=yes
|
package/test/updates.test.js
CHANGED
|
@@ -105,9 +105,11 @@ test('on startup, a finished update\'s status is cleared; a failure is kept for
|
|
|
105
105
|
assert.equal(fs.existsSync(statusFile), true);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
test('self-update
|
|
108
|
+
test('without --self-update: no request, no Coordinator check or button; Agents and Clients keep theirs', async (t) => {
|
|
109
109
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'thub-updates-')),
|
|
110
|
-
updates = createUpdatesService({
|
|
110
|
+
updates = createUpdatesService({
|
|
111
|
+
config: { dataDir, updates: {} }, pathUnit: path.join(dataDir, 'fake.path'), fetchLatest: async () => '99.0.0'
|
|
112
|
+
});
|
|
111
113
|
fs.writeFileSync(path.join(dataDir, 'fake.path'), '');
|
|
112
114
|
await updates.checkNow();
|
|
113
115
|
assert.equal(updates.status().selfUpdate, false);
|
|
@@ -123,7 +125,9 @@ test('self-update is off unless the Coordinator was started with --self-update:
|
|
|
123
125
|
for (const selfUpdate of [false, true]){
|
|
124
126
|
const config = { ...loadConfig(file), selfUpdate },
|
|
125
127
|
services = buildServices(config);
|
|
126
|
-
services.updates.checkNow = async () => {
|
|
128
|
+
services.updates.checkNow = async () => ({
|
|
129
|
+
fetched: ['coordinator', 'client'], latest: { client: '9.9.9' }, coordinatorVersion: '1.0.0', coordinatorUpdate: '99.0.0', selfUpdate
|
|
130
|
+
});
|
|
127
131
|
Object.assign(services.updates, { status: () => ({ latest: {}, coordinatorVersion: '1.0.0', coordinatorUpdate: '99.0.0', selfUpdate }) });
|
|
128
132
|
services.adminUsers.create({ username: `root${selfUpdate}`, email: `r${selfUpdate}@example.com`, role: 'admin', password: 'pw' }, { by: 't' });
|
|
129
133
|
const server = createApp(config, services).listen(0, '127.0.0.1');
|
|
@@ -137,7 +141,19 @@ test('self-update is off unless the Coordinator was started with --self-update:
|
|
|
137
141
|
method: 'POST', body: new URLSearchParams({ username: `root${selfUpdate}`, password: 'pw' }), redirect: 'manual'
|
|
138
142
|
})).headers.get('set-cookie').split(';')[0],
|
|
139
143
|
html = await (await fetch(`${base}/runners`, { headers: { cookie } })).text();
|
|
144
|
+
// The Coordinator's own: Update app and the navbar's check — only with --self-update.
|
|
140
145
|
assert.equal(/action="\/updates\/coordinator"/.test(html), selfUpdate, `Update app button with selfUpdate=${selfUpdate}`);
|
|
141
|
-
assert.
|
|
146
|
+
assert.equal(/action="\/updates\/check" data-submit-busy/.test(html), selfUpdate, `navbar update check with selfUpdate=${selfUpdate}`);
|
|
147
|
+
assert.equal(/99\.0\.0/.test(html.slice(0, html.indexOf('<main'))), selfUpdate); // nothing about a new Coordinator in the navbar
|
|
148
|
+
// Clients' (and Agents') check and update-all: always.
|
|
149
|
+
assert.match(html, /<form class="d-inline" method="post" action="\/updates\/check">/);
|
|
150
|
+
assert.match(html, /action="\/runners\/update-all"/);
|
|
151
|
+
// "Check for updates" (Runners page): Clients always, the Coordinator only with --self-update.
|
|
152
|
+
const check = await fetch(`${base}/updates/check`, {
|
|
153
|
+
method: 'POST', headers: { cookie }, body: new URLSearchParams({ returnTo: '/runners' }), redirect: 'manual'
|
|
154
|
+
}),
|
|
155
|
+
toast = await (await fetch(`${base}${check.headers.get('location')}`, { headers: { cookie } })).text();
|
|
156
|
+
assert.match(toast, /No connected runners|connected runner/);
|
|
157
|
+
assert.equal(/Coordinator v1\.0\.0 → v99\.0\.0 available/.test(toast), selfUpdate);
|
|
142
158
|
}
|
|
143
159
|
});
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
#[code ~/.config/thub/coordinator.json] (with a random #[code sessionSecret]) and
|
|
14
14
|
#[code ~/var/lib/thub] (database, avatars). It installs, enables and starts
|
|
15
15
|
#[code thub-coordinator.service], plus the root helper behind the navbar's #[strong Update app] button
|
|
16
|
-
(
|
|
16
|
+
(the unit starts the Coordinator with #[code --self-update]; see Updates below).
|
|
17
17
|
The unit uses #[code ProtectSystem=strict] with #[code PrivateTmp=yes], so the data directory and a
|
|
18
18
|
private #[code /tmp] (SQLite's temp files) are the only places the service can write.
|
|
19
19
|
|
|
@@ -176,11 +176,10 @@
|
|
|
176
176
|
h3.h6 Updates
|
|
177
177
|
p.small.mb-0.
|
|
178
178
|
The Coordinator checks npm for new Coordinator, Agent and Client versions. Admins update Agents and Clients
|
|
179
|
-
from the #[a(href="/admin/agents")
|
|
180
|
-
only between jobs.
|
|
181
|
-
|
|
182
|
-
#[code sudo
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
#[code thub self-update] and #[code thub-client self-update].
|
|
179
|
+
from the #[a(href="/admin/agents") CI tokens] and #[a(href="/runners") Runners] pages (always there); Clients install
|
|
180
|
+
an update only between jobs. The #[strong Coordinator]'s own update (the navbar's check for updates and
|
|
181
|
+
#[strong Update app]) is there only when it runs with #[code --self-update]. A systemd install has it on
|
|
182
|
+
(#[code sudo npm i -g] writes it into the unit); to turn it off, override #[code ExecStart] in a drop-in
|
|
183
|
+
(#[code sudo systemctl edit thub-coordinator]). Started any other way, e.g. in a container, it's off and the dashboard
|
|
184
|
+
shows nothing about Coordinator updates: update it by hand (#[code thub-admin self-update], #[code sudo npm i -g …])
|
|
185
|
+
or with a new image. Manual alternatives for the others: #[code thub self-update] and #[code thub-client self-update].
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
p.
|
|
3
3
|
The Coordinator runs as one container: the published package in a Node.js image, its SQLite database on a
|
|
4
4
|
persistent volume, TLS at the ingress. Clients and Agents connect from outside at #[code publicUrl].
|
|
5
|
-
Self-update stays off: the image starts the Coordinator without #[code --self-update],
|
|
6
|
-
deploying a new image tag.
|
|
5
|
+
Self-update stays off: the image starts the Coordinator without #[code --self-update], so the dashboard shows
|
|
6
|
+
nothing about Coordinator updates; you update it by deploying a new image tag.
|
|
7
7
|
|
|
8
8
|
h3.h6 1. Build the image
|
|
9
9
|
+code('Dockerfile').
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
h3.h6 Things to know
|
|
161
161
|
ul.small
|
|
162
162
|
li #[strong One replica.] One SQLite file and an in-process scheduler: no scale-out, no HPA. #[code Recreate] means a few seconds of downtime per upgrade; Clients and Agents reconnect by themselves.
|
|
163
|
-
li #[strong Updating:] deploy a new image tag. Migrations run on the new pod's first start.
|
|
163
|
+
li #[strong Updating:] deploy a new image tag. Migrations run on the new pod's first start. No #[code --self-update], so no Coordinator update check or #[strong Update app] on the dashboard. Agents and Clients still update from it.
|
|
164
164
|
li #[strong Storage:] block storage (ReadWriteOnce), not NFS. A separate database volume: mount it and set #[code THUB_DB_PATH].
|
|
165
165
|
li #[strong /tmp] is an #[code emptyDir]: SQLite needs a writable temp directory and the root filesystem is read-only.
|
|
166
166
|
li #[strong Backups:] #[code kubectl -n thub exec deploy/thub-coordinator -- node -e "new (require('/app/node_modules/better-sqlite3'))('/var/lib/thub/thub.db').backup('/var/lib/thub/backup.db')"], then #[code kubectl cp].
|
package/views/layout.pug
CHANGED
|
@@ -29,12 +29,13 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
29
29
|
data-bs-title=`Coordinator v${coordinatorVersion} · thub-common v${commonVersion} (validates job specs)`
|
|
30
30
|
)
|
|
31
31
|
| ver. #{coordinatorVersion}
|
|
32
|
-
//-
|
|
33
|
-
|
|
32
|
+
//- With --self-update, for those who can't update it themselves.
|
|
33
|
+
//- Without it, nothing about Coordinator updates is shown.
|
|
34
|
+
if user && updates.selfUpdate && updates.coordinatorUpdate && !can.admin
|
|
34
35
|
span.badge.text-bg-warning.ms-1(
|
|
35
36
|
data-bs-toggle="tooltip"
|
|
36
37
|
data-bs-placement="bottom"
|
|
37
|
-
data-bs-title=
|
|
38
|
+
data-bs-title="An admin can update the Coordinator from the navbar"
|
|
38
39
|
)= `v${updates.coordinatorUpdate} available`
|
|
39
40
|
button.navbar-toggler(type="button" data-bs-toggle="collapse" data-bs-target="#nav")
|
|
40
41
|
span.navbar-toggler-icon
|
|
@@ -80,7 +81,9 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
80
81
|
)
|
|
81
82
|
i.bi.bi-question-circle.me-1
|
|
82
83
|
| Help
|
|
83
|
-
|
|
84
|
+
//- The Coordinator's own update check: only with --self-update.
|
|
85
|
+
//- (Agents and Clients are checked from their own pages, always.)
|
|
86
|
+
if user && can.operate && updates.selfUpdate
|
|
84
87
|
li.nav-item.d-flex.align-items-center.me-2
|
|
85
88
|
form(method="post" action="/updates/check" data-submit-busy)
|
|
86
89
|
input(type="hidden" name="returnTo" value=currentPath)
|