@andrian.yablonskyy/thub-coordinator 1.1.8 → 1.1.10
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 -3
- package/package.json +1 -1
- package/public/js/remove-resource.js +2 -2
- package/src/services/jobs.js +7 -0
- package/src/services/list-prefs.js +1 -1
- package/src/services/registry.js +42 -3
- package/src/web/routes.js +63 -40
- package/test/groups-tab.test.js +74 -9
- package/test/live.test.js +2 -2
- package/test/scheduler.test.js +12 -0
- package/test/users.test.js +2 -2
- package/views/admin/agents.pug +2 -4
- package/views/groups/list.pug +3 -3
- package/views/help/_agent-cli.pug +4 -5
- package/views/help/_agent-setup.pug +1 -1
- package/views/help/_ci.pug +1 -1
- package/views/help/_client-machines.pug +5 -5
- package/views/help/_client-setup.pug +4 -4
- package/views/help/_coordinator.pug +3 -3
- package/views/help/_docker.pug +1 -1
- package/views/help/_env.pug +2 -3
- package/views/help/_overview.pug +3 -3
- package/views/help/_quick-start.pug +1 -1
- package/views/help/_troubleshooting.pug +6 -6
- package/views/index.pug +1 -1
- package/views/jobs/list.pug +3 -3
- package/views/jobs/show.pug +1 -1
- package/views/layout.pug +2 -2
- package/views/mixins/client-config.pug +50 -2
- package/views/mixins/group-select.pug +2 -2
- package/views/mixins/resource.pug +21 -19
- package/views/mixins/updates.pug +1 -1
- package/views/resources/list.pug +11 -8
package/test/users.test.js
CHANGED
|
@@ -185,7 +185,7 @@ test('dashboard: only maintainers and admins sign in; security pages are admins\
|
|
|
185
185
|
assert.match(dev.text, /no dashboard access/);
|
|
186
186
|
|
|
187
187
|
const mo = await signIn('mo', 'pw');
|
|
188
|
-
assert.equal((await get(mo.cookie, '/
|
|
188
|
+
assert.equal((await get(mo.cookie, '/runners')).status, 200);
|
|
189
189
|
assert.equal((await get(mo.cookie, '/admin/settings')).status, 403); // admins' only
|
|
190
190
|
for (const url of ['/admin/users', '/admin/agents']){
|
|
191
191
|
assert.equal((await get(mo.cookie, url)).status, 200, url); // within limits — see below
|
|
@@ -195,7 +195,7 @@ test('dashboard: only maintainers and admins sign in; security pages are admins\
|
|
|
195
195
|
|
|
196
196
|
// Blocked while signed in: the next click goes to the sign-in page.
|
|
197
197
|
u.setBlocked(maint.id, true, { actorId: admin.id });
|
|
198
|
-
const after = await get(mo.cookie, '/
|
|
198
|
+
const after = await get(mo.cookie, '/runners');
|
|
199
199
|
assert.equal(after.status, 302);
|
|
200
200
|
assert.equal(after.headers.get('location'), '/login');
|
|
201
201
|
assert.match((await signIn('mo', 'pw')).text, /blocked/);
|
package/views/admin/agents.pug
CHANGED
|
@@ -10,7 +10,7 @@ block content
|
|
|
10
10
|
.d-flex.flex-wrap.align-items-center.gap-2.mb-3
|
|
11
11
|
+updatesToolbar('agent', 'agents', list.pageUrl())
|
|
12
12
|
.d-flex.flex-wrap.align-items-center.gap-2.mb-3
|
|
13
|
-
+searchForm(q, 'Search
|
|
13
|
+
+searchForm(q, 'Search CI tokens: name, group, version, status', list.urlFor({ q: '' }))
|
|
14
14
|
.ms-auto
|
|
15
15
|
+pageSizePicker(list)
|
|
16
16
|
|
|
@@ -30,7 +30,6 @@ block content
|
|
|
30
30
|
thead
|
|
31
31
|
tr
|
|
32
32
|
+sortHeader(list, 'name', 'Name')
|
|
33
|
-
+sortHeader(list, 'kind', 'Kind')
|
|
34
33
|
+sortHeader(list, 'group', 'Group')
|
|
35
34
|
+sortHeader(list, 'version', 'Version', 'desc')
|
|
36
35
|
+sortHeader(list, 'created', 'Created', 'desc')
|
|
@@ -43,7 +42,6 @@ block content
|
|
|
43
42
|
//- The name opens Edit (name and group).
|
|
44
43
|
td
|
|
45
44
|
a.fw-medium(href=`#editAgentModal-${a.id}` data-bs-toggle="modal" role="button")= a.name
|
|
46
|
-
td.text-uppercase= a.kind
|
|
47
45
|
td
|
|
48
46
|
if a.group_id && groupsById[a.group_id]
|
|
49
47
|
span.badge.text-bg-info= groupsById[a.group_id].name
|
|
@@ -108,7 +106,7 @@ block content
|
|
|
108
106
|
button.btn.btn-primary(type="submit") Save
|
|
109
107
|
else
|
|
110
108
|
tr
|
|
111
|
-
td.text-body-secondary(colspan="
|
|
109
|
+
td.text-body-secondary(colspan="7")
|
|
112
110
|
if q
|
|
113
111
|
| No agent matches “#{q}”.
|
|
114
112
|
a(href=list.urlFor({ q: '' })) Show all agents
|
package/views/groups/list.pug
CHANGED
|
@@ -9,7 +9,7 @@ block content
|
|
|
9
9
|
|
|
10
10
|
p.text-body-secondary.small: i.
|
|
11
11
|
Give an agent a group — a user on #[a(href="/admin/users") Users], a CI token on #[a(href="/admin/agents") CI tokens] —
|
|
12
|
-
and its jobs run only on
|
|
12
|
+
and its jobs run only on runners in that group (§13.1). Which groups a Client is in is set on its runner card (Groups tab).
|
|
13
13
|
|
|
14
14
|
if totalCount
|
|
15
15
|
.d-flex.flex-wrap.align-items-center.gap-2.mb-2
|
|
@@ -25,7 +25,7 @@ block content
|
|
|
25
25
|
tr
|
|
26
26
|
th Name
|
|
27
27
|
th Comment
|
|
28
|
-
th
|
|
28
|
+
th Runners
|
|
29
29
|
th Agents
|
|
30
30
|
th Created
|
|
31
31
|
if canManage
|
|
@@ -44,7 +44,7 @@ block content
|
|
|
44
44
|
form.d-inline.ms-1(
|
|
45
45
|
method="post"
|
|
46
46
|
action=`/groups/${g.id}/delete`
|
|
47
|
-
data-confirm=`Delete group "${g.name}"?
|
|
47
|
+
data-confirm=`Delete group "${g.name}"? Runners listing it just stop matching on it, and its agents go back to running their jobs on any runner.`
|
|
48
48
|
data-confirm-title="Delete group?"
|
|
49
49
|
data-confirm-ok="Delete group"
|
|
50
50
|
)
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
-
|
|
19
19
|
const groups = [
|
|
20
20
|
{ title: 'Where to run', rows: [
|
|
21
|
-
['--type hw|sw', 'Required.
|
|
22
|
-
['--
|
|
23
|
-
['--label <l>', 'Required label (repeatable). The Client must have all of them.', '--label uart --label stlink'],
|
|
21
|
+
['--type hw|sw', 'Required. Runner type.', '--type hw'],
|
|
22
|
+
['--label <l>', 'Required label (repeatable). The runner must have all of them. 1–64 characters, no spaces, commas or semicolons.', '--label uart --label stlink'],
|
|
24
23
|
['--client <name|id>', 'Only this Client; waits for it even if others are idle.', '--client lab-hw-01']
|
|
25
24
|
] },
|
|
26
25
|
{ title: 'What to run', rows: [
|
|
@@ -73,13 +72,13 @@
|
|
|
73
72
|
|
|
74
73
|
h3.h6 Examples
|
|
75
74
|
+code('Flash and test a board, fail CI on failure').
|
|
76
|
-
thub run --type hw --board
|
|
75
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
77
76
|
--download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
|
|
78
77
|
--git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
|
|
79
78
|
--command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
|
|
80
79
|
--arg --junit --timeout 45m --wait
|
|
81
80
|
+code('Reproduce a failure on the exact bench, labeled with your name').
|
|
82
|
-
thub run --type hw --board
|
|
81
|
+
thub run --type hw --label board:nucleo-f401re --client lab-hw-01 --user "$USER" \
|
|
83
82
|
--download-file "$IMAGE_URL" --git-repo "$TESTS_REPO" --command ./ci/test.sh
|
|
84
83
|
# Ctrl-C detaches; the job keeps running. Re-attach:
|
|
85
84
|
thub status M-00126
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
thub config set key thk_…
|
|
45
45
|
thub config set user "Your Name" # optional: job owner label
|
|
46
46
|
p.small.
|
|
47
|
-
These are saved in #[code ~/.config/thub/agent.json]. Which
|
|
47
|
+
These are saved in #[code ~/.config/thub/agent.json]. Which runner group your jobs run in isn't an Agent
|
|
48
48
|
setting: an admin or maintainer sets it for your user on the dashboard (#[code thub whoami] shows it). Order of precedence: flags (#[code --url], #[code --key]) →
|
|
49
49
|
environment → config file.
|
|
50
50
|
table.table.table-sm.small
|
package/views/help/_ci.pug
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
steps:
|
|
32
32
|
- run: |
|
|
33
33
|
npx -y @andrian.yablonskyy/thub-agent run \
|
|
34
|
-
--type hw --board
|
|
34
|
+
--type hw --label board:nucleo-f401re \
|
|
35
35
|
--download-file "${{ needs.build.outputs.image_url }}" \
|
|
36
36
|
--git-repo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$GITHUB_SHA" \
|
|
37
37
|
--command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh' \
|
|
@@ -40,17 +40,17 @@
|
|
|
40
40
|
lsusb -t # USB tree
|
|
41
41
|
udevadm info -a -n /dev/ttyUSB0 | grep -m1 'ATTRS{devpath}'
|
|
42
42
|
st-info --probe # ST-Link serials
|
|
43
|
-
| Or open the
|
|
43
|
+
| Or open the runner card → #[strong Connected USB devices] → #[strong Refresh], then #[strong Import to config].
|
|
44
44
|
li
|
|
45
45
|
| Put the devpaths into #[code hw-devices] and restart. On every start the Client writes
|
|
46
46
|
| #[code /etc/udev/rules.d/99-thub-<instance>.rules], so the devices appear as #[code /dev/thub/dut<N>-stlink|uart|usb]:
|
|
47
47
|
+code('HW machine').
|
|
48
48
|
sudo systemctl restart thub-client@client
|
|
49
49
|
ls -l /dev/thub/
|
|
50
|
-
li Check the
|
|
50
|
+
li Check the runner card's #[strong Capabilities]: each configured device must be present (a missing one is flagged).
|
|
51
51
|
li Your job's #[code --command] flashes the board. The Client never flashes by itself; it passes the ST-Link serials and the downloaded files in the environment (#[a(href="#env") Environment variables]).
|
|
52
52
|
+code('A typical HW job').
|
|
53
|
-
thub run --type hw --board
|
|
53
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
54
54
|
--download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
|
|
55
55
|
--git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
|
|
56
56
|
--command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' \
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
p.small.
|
|
72
72
|
To scale out, clone the VM template. In the clone, #[strong before its first start], run
|
|
73
73
|
#[code rm -f ~/var/lib/thub/client/*/.client-id ~/var/lib/thub/client/*.token] and give it its own #[code name].
|
|
74
|
-
Otherwise both VMs register as the same
|
|
74
|
+
Otherwise both VMs register as the same runner.
|
|
75
75
|
+code('A typical SW job').
|
|
76
76
|
thub run --type sw \
|
|
77
77
|
--docker-image registry.lab.local:5000/dut-emulator:2026.08 \
|
|
@@ -84,4 +84,4 @@
|
|
|
84
84
|
li Outbound HTTPS to the Coordinator; test it with #[code curl -sI #{coordinatorUrl}/login].
|
|
85
85
|
li The Client's service user has SSH keys and git credentials for private repos (#[a(href="#git") Git repositories]) and Docker logins for private registries (#[a(href="#docker") Docker registry]).
|
|
86
86
|
li Under systemd, the service can only write to its state directory. Jobs write to #[code $THUB_WORK_DIR], not to #[code ~].
|
|
87
|
-
li Optional: a nightly reboot from the
|
|
87
|
+
li Optional: a nightly reboot from the runner card's #[strong Reboot] tab (cron, host-local time). It waits for running jobs.
|
|
@@ -74,15 +74,15 @@
|
|
|
74
74
|
tr
|
|
75
75
|
td: code name
|
|
76
76
|
td No
|
|
77
|
-
td
|
|
77
|
+
td Runner name. Under systemd, the instance name (#[code thub-client@dut1] → #[code dut1]) is used unless an admin renames it on the dashboard.
|
|
78
78
|
tr
|
|
79
79
|
td: code labels
|
|
80
80
|
td No
|
|
81
|
-
td Matched against a job's #[code --
|
|
81
|
+
td Matched against a job's #[code --label]s. Use #[code board:<name>] for the board (#[code --label board:<name>]). Also set on its runner card (Labels tab), which writes them here.
|
|
82
82
|
tr
|
|
83
83
|
td: code groups
|
|
84
84
|
td No
|
|
85
|
-
td The
|
|
85
|
+
td The runner groups this Client belongs to. Set them on its runner card (Groups tab), which writes them here.
|
|
86
86
|
tr
|
|
87
87
|
td: code varDir
|
|
88
88
|
td No
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
td An explicit identity UUID (or #[code THUB_CLIENT_ID]). Normally generated once and kept in #[code .client-id].
|
|
102
102
|
p.small.
|
|
103
103
|
SW Clients have no settings of their own: they run whatever DUT image a job brings (#[code --docker-image]).
|
|
104
|
-
Admins can also edit an HW Client's devices from its
|
|
104
|
+
Admins can also edit an HW Client's devices from its runner card (#[strong ST-Link | UART | DUT USB]
|
|
105
105
|
tabs). The tabs show the config the Client runs, as it reported it at its last start, or a saved
|
|
106
106
|
change it hasn't applied yet. #[strong Connected USB devices → Import to config] fills them from the host's #[code lsusb].
|
|
107
107
|
The card's #[strong Export] and #[strong Import] icons download a Client's config file (never its
|
|
@@ -63,13 +63,13 @@
|
|
|
63
63
|
td The shared secret Clients self-register with. Unset or #[code null] turns registration off (#[code 503]).
|
|
64
64
|
tr
|
|
65
65
|
td: code heartbeat.*
|
|
66
|
-
td Clients heartbeat every #[code intervalSec] — the Coordinator tells them, so it's set here only (and on Settings). After #[code missedLimit] misses a
|
|
66
|
+
td Clients heartbeat every #[code intervalSec] — the Coordinator tells them, so it's set here only (and on Settings). After #[code missedLimit] misses a runner goes #[code OUT_OF_SERVICE] and its job becomes #[code LOST]; #[code sweepIntervalSec] is how often that's checked.
|
|
67
67
|
tr
|
|
68
68
|
td: code scheduler.*
|
|
69
69
|
td #[code assignAckTimeoutSec]: how long an assignment waits for the Client to accept. #[code requeueOnLost]: retry a #[code LOST] job once. #[code maxQueuedPerAgent]: queue limit per agent.
|
|
70
70
|
tr
|
|
71
71
|
td: code rateLimit.*
|
|
72
|
-
td Requests per minute per caller (agent/
|
|
72
|
+
td Requests per minute per caller (agent/runner token, dashboard user, else IP; default 500), and sign-in attempts per IP (default 10). Past it: #[code 429] with #[code Retry-After]. #[code 0] turns a limit off.
|
|
73
73
|
tr
|
|
74
74
|
td: code jobs.*
|
|
75
75
|
td Default and maximum job timeout, in seconds.
|
|
@@ -152,5 +152,5 @@
|
|
|
152
152
|
p.small.mb-0.
|
|
153
153
|
The Coordinator checks npm for new Coordinator, Agent and Client versions. Admins update the Coordinator
|
|
154
154
|
with #[strong Update app] in the navbar, and Agents and Clients from the #[a(href="/admin/agents") Agents]
|
|
155
|
-
and #[a(href="/
|
|
155
|
+
and #[a(href="/runners") Runners] pages. Clients install an update only between jobs.
|
|
156
156
|
Manual alternatives: #[code thub-admin self-update], #[code thub self-update] and #[code thub-client self-update].
|
package/views/help/_docker.pug
CHANGED
|
@@ -121,4 +121,4 @@
|
|
|
121
121
|
td Files written to #[code /work] stay owned by the Client user, so the workspace can be cleaned up.
|
|
122
122
|
+note('info').
|
|
123
123
|
Docker access is effectively root access on that host. The Client host must have Docker and the service user must be in the
|
|
124
|
-
#[code docker] group. The
|
|
124
|
+
#[code docker] group. The runner card's #[strong Capabilities] show whether a Client can use Docker, and why not.
|
package/views/help/_env.pug
CHANGED
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
-
|
|
36
36
|
const jobVars = [
|
|
37
37
|
['JOB_TYPE', '--type', 'hw or sw'],
|
|
38
|
-
['
|
|
39
|
-
['JOB_LABEL / JOB_LABEL_<n>', '--board, --label', 'all labels, comma-separated / each'],
|
|
38
|
+
['JOB_LABEL / JOB_LABEL_<n>', '--label', 'all labels, comma-separated / each'],
|
|
40
39
|
['JOB_GROUP', '(dashboard)', 'the submitting agent\'s group, if it has one'],
|
|
41
40
|
['JOB_CLIENT', '--client', 'this Client\'s name'],
|
|
42
41
|
['JOB_USER', '--user', 'as given'],
|
|
@@ -118,7 +117,7 @@
|
|
|
118
117
|
python:3.14 ./run-tests.sh
|
|
119
118
|
+code('Submitting the job that feeds these').
|
|
120
119
|
export API_TOKEN=…
|
|
121
|
-
thub run --type hw --board
|
|
120
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
122
121
|
--download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
|
|
123
122
|
--git-repo git@github.com:yourorg/firmware-tests.git main \
|
|
124
123
|
--env TARGET=production --env API_TOKEN \
|
package/views/help/_overview.pug
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
i.bi.bi-hdd-network.me-1
|
|
13
13
|
| Coordinator
|
|
14
14
|
p.card-text.small.mb-0.
|
|
15
|
-
This service. It holds the job queue and the
|
|
15
|
+
This service. It holds the job queue and the runner registry, and runs the scheduler and
|
|
16
16
|
the heartbeat monitor. It stores job state and logs in SQLite,
|
|
17
17
|
and serves this dashboard. It's the only thing that needs a public HTTPS port.
|
|
18
18
|
.col-md-4
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
li #[strong The Coordinator is the single source of truth.] It assigns a job inside a single SQLite transaction, so a job can never be assigned twice.
|
|
52
52
|
li #[strong A job is a command, not a script upload.] The Client runs your #[code --command] as its service user, never as root. Issue agent tokens only to trusted pipelines and people.
|
|
53
53
|
|
|
54
|
-
h3.h6
|
|
54
|
+
h3.h6 Runner and job states
|
|
55
55
|
.row.g-3.small
|
|
56
56
|
.col-md-6
|
|
57
57
|
table.table.table-sm.mb-0
|
|
58
58
|
thead
|
|
59
59
|
tr
|
|
60
|
-
th
|
|
60
|
+
th Runner
|
|
61
61
|
th Meaning
|
|
62
62
|
tbody
|
|
63
63
|
tr
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
sudo npm i -g @andrian.yablonskyy/thub-client
|
|
19
19
|
nano ~/.config/thub/client.json # coordinatorUrl, type, joinKey
|
|
20
20
|
sudo systemctl enable --now thub-client@client
|
|
21
|
-
| It shows up on #[a(href="/
|
|
21
|
+
| It shows up on #[a(href="/runners") Runners] by itself, usually within 10 s.
|
|
22
22
|
li
|
|
23
23
|
strong Create users and their access keys
|
|
24
24
|
| on #[a(href="/admin/users") Users] (admins and maintainers), or on the Coordinator host with
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
//- <code>, with everything else HTML-escaped first (so `<instance>` shows as text).
|
|
4
4
|
-
|
|
5
5
|
const faq = [
|
|
6
|
-
['A Client doesn\'t appear on
|
|
7
|
-
['A Client flaps or two machines show as one
|
|
8
|
-
['Name already in use (`409`)', 'Another live Client holds that name. Rename one of them in its config or from the
|
|
9
|
-
['The job is rejected with `422`', 'No registered Client can ever match its type, labels, group or client. Compare `thub resources` with your `--type` and `--
|
|
6
|
+
['A Client doesn\'t appear on Runners', 'Check `journalctl -u thub-client@<instance> -f`. A `503` at registration means the Coordinator has no `clientJoinKey`; a `401` means the `joinKey` differs. Also check that `coordinatorUrl` is reachable from the lab (`curl -sI <url>/login`).'],
|
|
7
|
+
['A Client flaps or two machines show as one runner', 'Both use the same `.client-id` (a cloned VM or a shared config file). Delete `<varDir>/<instance>/.client-id` on one of them and restart it, or set a distinct `clientId`.'],
|
|
8
|
+
['Name already in use (`409`)', 'Another live Client holds that name. Rename one of them in its config or from the runner card. A name held by an `OUT_OF_SERVICE` runner is reclaimed automatically.'],
|
|
9
|
+
['The job is rejected with `422`', 'No registered Client can ever match its type, labels, group or client. Compare `thub resources` with your `--type` and `--label`s; your group (set on the dashboard, see `thub whoami`) must have a Client of that kind.'],
|
|
10
10
|
['The job stays `QUEUED`', 'Matching Clients are all `BUSY`, locked (`local`), in `MAINTENANCE` or offline, or the job is pinned with `--client`. It times out after `--timeout`.'],
|
|
11
11
|
['`ERROR` during prepare', 'The clone, download or DUT image pull failed; the job log names the reason. For git, check the Client user\'s keys and `known_hosts` (`ssh -T git@<host>` as that user). For images, check the Client user\'s `docker login`.'],
|
|
12
12
|
['Permission denied on `/dev/ttyUSB*` or `docker.sock`', 'Docker or the device groups were added after the Client was installed. Re-run `sudo npm i -g @andrian.yablonskyy/thub-client` so the unit gets the `dialout`/`plugdev`/`docker` groups, then restart.'],
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
['Job `LOST`', 'The Client missed 3 heartbeats (network, reboot, crash). With `requeueOnLost` the job is retried once on another Client.'],
|
|
15
15
|
['A job can\'t write to `~` (read-only file system)', 'Under systemd the Client can only write to its state directory. Write to `$THUB_WORK_DIR`, and set `DOCKER_CONFIG` there for `docker login`.'],
|
|
16
16
|
['Live logs don\'t stream through the proxy', 'Turn off response buffering for the Coordinator (nginx: `proxy_buffering off;`, Caddy: `flush_interval -1`).'],
|
|
17
|
-
['Finding a job,
|
|
17
|
+
['Finding a job, runner, group or agent', 'Use the search box on its list page (press `/` to jump to it). Every word must match, e.g. `lab-hw-01 FAILED` or a git ref. Job search covers ids, users, runners, agents, commands, repos and `--meta`, but not `--env` values (secrets). On a job page, search the log above the log viewer: `Enter`/`Shift+Enter` step through matches.'],
|
|
18
18
|
['Lists don\'t update by themselves (navbar shows Offline or Paused)', '`Paused`: you\'re editing something on the page (a field, the inline rename, a dialog); it resumes when you\'re done. `Offline`: the browser can\'t hold the `/live` stream open, often a proxy buffering it (nginx: `proxy_buffering off;`); reloading the page always shows current data. `Session ended`: sign in again. Background updates never extend the session\'s idle timeout.'],
|
|
19
19
|
['Sign-in says it needs HTTPS', 'The session cookie is HTTPS-only because `publicUrl` is `https://`, but the request arrived as plain HTTP. Open the dashboard through its https:// address; behind a reverse proxy, make it send `X-Forwarded-Proto: https` (nginx: `proxy_set_header X-Forwarded-Proto $scheme;`) and check `trustProxy`. To allow plain HTTP, set `session.secureCookie` to `false`.'],
|
|
20
20
|
['Where are a job\'s test reports and output files?', 'The Coordinator keeps only the log and the result (verdict, exit code, JUnit test counts). Files the job creates stay in its workspace on the Client and are deleted when the job ends. To keep them, publish them from `--command`, e.g. `curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T results/report.xml "https://artifactory.example.com/qa/$THUB_JOB_ID/report.xml"` with `--env ART_TOKEN`. To show them on the job page and in `thub status`, list them in `$THUB_ARTIFACTS_FILE` as `[{"name", "size", "link", "timestamp"}]` — only that metadata is kept. The full log is always available from the job page\'s Raw button.'],
|
|
21
|
-
['429 Too Many Requests', 'A caller went over `rateLimit.requestsPerMinute` (per agent or
|
|
21
|
+
['429 Too Many Requests', 'A caller went over `rateLimit.requestsPerMinute` (per agent or runner token, per dashboard user, else per IP; default 500). The Agent and Client wait for `Retry-After` and retry by themselves; a script calling the API should do the same. Raise the limit in the config file or on Settings if your normal traffic needs more (a Client streaming logs makes up to ~240 requests a minute). Too many sign-in attempts from one address: wait a minute (`rateLimit.loginPerMinute`).'],
|
|
22
22
|
['Sign-in says “Too many failed sign-ins”', 'After repeated wrong passwords the Coordinator makes each further attempt wait, doubling each time: per address from the 5th failure (up to 15 min), per username from the 10th from any address (up to 5 min). Wait the time shown, or sign in from another address; a successful sign-in clears it, and an hour without failures forgets it. The Coordinator never tells whether a username exists.'],
|
|
23
23
|
['Agent says “Invalid or revoked access key” or “… is blocked”', 'The key was replaced (`thub key rotate`, an admin, your profile) or revoked, or your user is blocked. Ask an admin for a new key (Users), or create one on your profile if you can sign in. Then `thub config set key <key>`; `thub whoami` checks it.'],
|
|
24
24
|
['The Coordinator won\'t start after an upgrade: `SqliteError: disk I/O error` (`SQLITE_IOERR_GETTEMPPATH`)', 'Its systemd unit is older than 1.1.1: it has `ProtectSystem=strict` without `PrivateTmp=yes`, so SQLite has no writable temp directory for a large migration. The migration rolls back, so no data is lost. Install the latest version with `sudo npm i -g @andrian.yablonskyy/thub-coordinator` (the dashboard can\'t update a Coordinator that\'s down), or run `sudo systemctl edit thub-coordinator`, add `[Service]` and `PrivateTmp=yes`, then `sudo systemctl restart thub-coordinator`.'],
|
package/views/index.pug
CHANGED
|
@@ -21,7 +21,7 @@ block content
|
|
|
21
21
|
.col-md-4
|
|
22
22
|
.card
|
|
23
23
|
.card-body
|
|
24
|
-
h6.card-subtitle.mb-2.text-body-secondary
|
|
24
|
+
h6.card-subtitle.mb-2.text-body-secondary Runners online
|
|
25
25
|
p.display-6= `${onlineCount} / ${resources.length}`
|
|
26
26
|
|
|
27
27
|
.row.g-3(data-live="overview-cards")
|
package/views/jobs/list.pug
CHANGED
|
@@ -13,7 +13,7 @@ block content
|
|
|
13
13
|
|
|
14
14
|
form.row.g-2.mb-3(method="get" role="search")
|
|
15
15
|
.col-12.col-md-5.col-lg-4
|
|
16
|
-
+searchBox(filters.q, 'Search jobs: id, user,
|
|
16
|
+
+searchBox(filters.q, 'Search jobs: id, user, runner, command, repo…', list.urlFor({ q: '' }))
|
|
17
17
|
.col-auto
|
|
18
18
|
select.form-select.form-select-sm(name="state" data-autosubmit)
|
|
19
19
|
option(value="") any state
|
|
@@ -37,7 +37,7 @@ block content
|
|
|
37
37
|
+sortHeader(list, 'source', 'Source')
|
|
38
38
|
+sortHeader(list, 'user', 'User')
|
|
39
39
|
+sortHeader(list, 'state', 'State')
|
|
40
|
-
+sortHeader(list, 'resource', '
|
|
40
|
+
+sortHeader(list, 'resource', 'Runner')
|
|
41
41
|
+sortHeader(list, 'created', 'Created', 'desc')
|
|
42
42
|
+sortHeader(list, 'duration', 'Duration', 'desc')
|
|
43
43
|
if canManage
|
|
@@ -132,7 +132,7 @@ block content
|
|
|
132
132
|
data-value=preset.value
|
|
133
133
|
)= preset.label
|
|
134
134
|
p
|
|
135
|
-
| Deletes finished jobs that ended before then, with their logs, and the
|
|
135
|
+
| Deletes finished jobs that ended before then, with their logs, and the runner, agent and group history logged before then — then compacts the database file.
|
|
136
136
|
|
|
|
137
137
|
strong This cannot be undone.
|
|
138
138
|
| Active jobs are never touched.
|
package/views/jobs/show.pug
CHANGED
|
@@ -29,7 +29,7 @@ block content
|
|
|
29
29
|
if job.spec.user
|
|
30
30
|
dt.col-sm-2 User
|
|
31
31
|
dd.col-sm-10= job.spec.user
|
|
32
|
-
dt.col-sm-2
|
|
32
|
+
dt.col-sm-2 Runner
|
|
33
33
|
dd.col-sm-10= job.resource ? job.resource.name : job.resource_name ? `${job.resource_name} (removed)` : '—'
|
|
34
34
|
dt.col-sm-2 Target
|
|
35
35
|
dd.col-sm-10= `${job.spec.target.type} — ${(job.spec.target.labels || []).join(', ')}`
|
package/views/layout.pug
CHANGED
|
@@ -53,7 +53,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
53
53
|
//- Below lg the menu drops down over the page (thub.css .thub-nav,
|
|
54
54
|
//- closed by public/js/actions.js): pages as a two-column grid with
|
|
55
55
|
//- icons, then the tools in a row, then the account row.
|
|
56
|
-
- const pages = user ? [['/', 'overview', 'Overview', 'speedometer2'], ['/
|
|
56
|
+
- const pages = user ? [['/', 'overview', 'Overview', 'speedometer2'], ['/runners', 'resources', 'Runners', 'hdd-rack'], ['/groups', 'groups', 'Groups', 'collection'], ['/jobs', 'jobs', 'Jobs', 'list-task'], ['/admin/users', 'users', 'Users', 'people'], ['/admin/agents', 'agents', 'CI tokens', 'key']].concat(user.role === 'admin' ? [['/admin/settings', 'settings', 'Settings', 'gear']] : []) : []
|
|
57
57
|
ul.navbar-nav.me-auto.thub-nav-pages
|
|
58
58
|
each pg in pages
|
|
59
59
|
li.nav-item
|
|
@@ -88,7 +88,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
88
88
|
aria-label="Check for updates"
|
|
89
89
|
data-bs-toggle="tooltip"
|
|
90
90
|
data-bs-placement="bottom"
|
|
91
|
-
data-bs-title=updates.checkedAt ? `Check for updates now — the Coordinator and connected
|
|
91
|
+
data-bs-title=updates.checkedAt ? `Check for updates now — the Coordinator and connected runners (last checked ${fmtDate(updates.checkedAt)})` : 'Check for updates now — the Coordinator and connected runners'
|
|
92
92
|
)
|
|
93
93
|
i.bi.bi-arrow-repeat
|
|
94
94
|
if user && can.admin && updates.coordinatorUpdate
|
|
@@ -96,9 +96,56 @@ mixin configTabItems(r)
|
|
|
96
96
|
if r.type === 'hw'
|
|
97
97
|
each t in [['stlinks', 'ST-Link'], ['uarts', 'UART'], ['usbs', 'DUT USB']]
|
|
98
98
|
+configTab(p, t[0], t[1])
|
|
99
|
+
+configTab(p, 'labels', 'Labels')
|
|
99
100
|
+configTab(p, 'groups', 'Groups')
|
|
100
101
|
+configTab(p, 'reboot', 'Reboot')
|
|
101
102
|
|
|
103
|
+
//- Labels tab: what a job's --label / --label board:must find on this Client,
|
|
104
|
+
//- one per line. Saved like the Groups tab (registry.setClientLabels): in
|
|
105
|
+
//- effect on the Coordinator at once, written to the config file's
|
|
106
|
+
//- `labels` by the Client.
|
|
107
|
+
mixin labelsPane(r, returnTo)
|
|
108
|
+
- const p = `cfg-${r.id}`
|
|
109
|
+
- const canEdit = can.operate && r.config_support === 'file'
|
|
110
|
+
- const pending = r.config_import && r.config_import.revision > r.config_applied_revision && r.config_import.fields && r.config_import.fields.labels
|
|
111
|
+
+configPane(p, 'labels')
|
|
112
|
+
if pending || r.config_error
|
|
113
|
+
.d-flex.justify-content-end.gap-1.mb-2
|
|
114
|
+
if pending
|
|
115
|
+
span.badge.text-bg-warning(
|
|
116
|
+
data-bs-toggle="tooltip"
|
|
117
|
+
data-bs-title="In effect on the Coordinator; the Client writes them to its config file on its next heartbeat, then restarts once idle"
|
|
118
|
+
) not written to the Client yet
|
|
119
|
+
if r.config_error
|
|
120
|
+
span.badge.text-bg-danger(data-bs-toggle="tooltip" data-bs-title=r.config_error) refused by the Client
|
|
121
|
+
if r.config_support !== 'file'
|
|
122
|
+
p.small.text-body-secondary Labels can't be set here: #{noConfigReport} Or set #[code labels] in its config file.
|
|
123
|
+
form(
|
|
124
|
+
method="post"
|
|
125
|
+
action=`/runners/${r.id}/labels`
|
|
126
|
+
data-confirm=`Save the labels of ${r.name}? They apply at once; the Client also writes them to its config file and restarts once it has no job running.`
|
|
127
|
+
data-confirm-title="Save runner labels?"
|
|
128
|
+
data-confirm-ok="Save"
|
|
129
|
+
data-confirm-tone="primary"
|
|
130
|
+
)
|
|
131
|
+
input(type="hidden" name="returnTo" value=returnTo)
|
|
132
|
+
fieldset(disabled=!canEdit)
|
|
133
|
+
label.form-label.small(for=`${p}-labels-input`) One label per line
|
|
134
|
+
textarea.form-control.font-monospace(
|
|
135
|
+
id=`${p}-labels-input`
|
|
136
|
+
name="labels"
|
|
137
|
+
rows="6"
|
|
138
|
+
spellcheck="false"
|
|
139
|
+
autocapitalize="off"
|
|
140
|
+
placeholder="board:nucleo-f401re\nuart\nstlink"
|
|
141
|
+
)= r.labels.join('\n')
|
|
142
|
+
.form-text
|
|
143
|
+
| A job runs here only if this runner has every label it asks for (#[code thub run --label uart --label board:nucleo-f401re]).
|
|
144
|
+
| Each up to 64 characters, no spaces, commas or semicolons; up to 64 labels.
|
|
145
|
+
if canEdit
|
|
146
|
+
.d-flex.justify-content-end.mt-3
|
|
147
|
+
button.btn.btn-sm.btn-primary(type="submit" data-bs-toggle="tooltip" data-bs-title="Save the labels — in effect at once; the Client writes them to its config file") Save labels
|
|
148
|
+
|
|
102
149
|
//- Groups tab: which resource groups this Client belongs to (thub run
|
|
103
150
|
//- --group). Saved as a config revision the Client writes to its config
|
|
104
151
|
//- file's `groups` (registry.setClientGroups); in effect on the Coordinator
|
|
@@ -129,7 +176,7 @@ mixin groupsPane(r, returnTo, groupsById)
|
|
|
129
176
|
else
|
|
130
177
|
form(
|
|
131
178
|
method="post"
|
|
132
|
-
action=`/
|
|
179
|
+
action=`/runners/${r.id}/groups`
|
|
133
180
|
data-confirm=`Save the groups of ${r.name}? They apply at once; the Client also writes them to its config file and restarts once it has no job running.`
|
|
134
181
|
data-confirm-title="Save Client groups?"
|
|
135
182
|
data-confirm-ok="Save"
|
|
@@ -173,7 +220,7 @@ mixin configPanes(r, returnTo, groupsById)
|
|
|
173
220
|
else
|
|
174
221
|
form(
|
|
175
222
|
method="post"
|
|
176
|
-
action=`/
|
|
223
|
+
action=`/runners/${r.id}/config`
|
|
177
224
|
data-client-config="hw"
|
|
178
225
|
data-confirm=`Apply these devices to ${r.name}? The Client writes them to its config file on its next heartbeat and restarts once it has no job running.`
|
|
179
226
|
data-confirm-title="Save Client devices?"
|
|
@@ -198,6 +245,7 @@ mixin configPanes(r, returnTo, groupsById)
|
|
|
198
245
|
+deviceTable('usbs', 'DUT USB devices', current.usbs || [], ['Device', 'udev devpath'])
|
|
199
246
|
.form-text Device: a udev index 1–8 (→ /dev/thub/dut<N>-usb) or a /dev path. udev devpath: the USB port it's plugged into.
|
|
200
247
|
+saveCapabilities(canEdit)
|
|
248
|
+
+labelsPane(r, returnTo)
|
|
201
249
|
+groupsPane(r, returnTo, groupsById || {})
|
|
202
250
|
+configPane(p, 'reboot')
|
|
203
251
|
dl.row.mb-0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//- --group. Groups by name; none = any resource.
|
|
4
4
|
mixin groupSelect(id, selected, groups)
|
|
5
5
|
select.form-select(id=id name="groupId")
|
|
6
|
-
option(value="" selected=!selected) Any
|
|
6
|
+
option(value="" selected=!selected) Any runner (no group)
|
|
7
7
|
each g in groups
|
|
8
8
|
option(value=g.id selected=g.id === selected)= g.name
|
|
9
|
-
.form-text Its jobs run only on
|
|
9
|
+
.form-text Its jobs run only on runners in this group. Manage groups on #[a(href="/groups") Groups].
|