@andrian.yablonskyy/thub-coordinator 1.0.24 → 1.0.25
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/package.json +1 -1
- package/src/services/jobs.js +13 -6
- package/src/services/registry.js +11 -3
- package/src/services/scheduler.js +6 -1
- package/test/scheduler.test.js +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-coordinator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
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": {
|
package/src/services/jobs.js
CHANGED
|
@@ -145,15 +145,22 @@ function createJobsService(db, { bus, events, registry, artifacts, config }){
|
|
|
145
145
|
spec.target.client = resource.id;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// A job-supplied Docker image needs
|
|
149
|
-
//
|
|
150
|
-
const needs = { jobImage: Boolean(spec.firmware.image) },
|
|
148
|
+
// A job-supplied Docker image or command needs a Client that opted in —
|
|
149
|
+
// say which, rather than the generic "no resource can satisfy".
|
|
150
|
+
const needs = { jobImage: Boolean(spec.firmware.image), jobCommand: Boolean(spec.tests.command) },
|
|
151
151
|
{ type, labels, group, client } = spec.target;
|
|
152
|
-
if (needs.jobImage
|
|
152
|
+
if ((needs.jobImage || needs.jobCommand) && registry.everSatisfiable(type, labels, group, client) &&
|
|
153
|
+
!registry.everSatisfiable(type, labels, group, client, needs)){
|
|
154
|
+
const missing = [
|
|
155
|
+
needs.jobImage && !registry.everSatisfiable(type, labels, group, client, { jobImage: true }) &&
|
|
156
|
+
`job-supplied Docker images (${spec.firmware.image}) — "sw": { "allowJobImages": true }`,
|
|
157
|
+
needs.jobCommand && !registry.everSatisfiable(type, labels, group, client, { jobCommand: true }) &&
|
|
158
|
+
'job-supplied commands (--run) — "allowJobCommands": true'
|
|
159
|
+
].filter(Boolean);
|
|
153
160
|
throw Object.assign(
|
|
154
161
|
new Error(
|
|
155
|
-
`No matching
|
|
156
|
-
'
|
|
162
|
+
`No matching Client runs ${missing.length ? missing.join(', nor ') : 'both a job-supplied Docker image and command together'}` +
|
|
163
|
+
' — enable it in a Client\'s config'
|
|
157
164
|
),
|
|
158
165
|
{ status: 422 }
|
|
159
166
|
);
|
package/src/services/registry.js
CHANGED
|
@@ -67,10 +67,16 @@ const MAX_DEVICES = 8,
|
|
|
67
67
|
present: typeof d?.present === 'boolean' ? d.present : null
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
// Plus `allowJobCommands` (either type): runs a job's own `tests.command`.
|
|
70
71
|
function sanitizeCapabilities(caps){
|
|
71
72
|
if (!caps || typeof caps !== 'object'){
|
|
72
73
|
return null;
|
|
73
74
|
}
|
|
75
|
+
const typed = sanitizeTypedCapabilities(caps);
|
|
76
|
+
return typed ? { ...typed, allowJobCommands: caps.allowJobCommands === true } : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sanitizeTypedCapabilities(caps){
|
|
74
80
|
if (caps.sw && typeof caps.sw === 'object'){
|
|
75
81
|
const sw = caps.sw;
|
|
76
82
|
return {
|
|
@@ -469,13 +475,15 @@ function createRegistryService(db, { bus, events }){
|
|
|
469
475
|
);
|
|
470
476
|
}
|
|
471
477
|
|
|
472
|
-
// `needs
|
|
473
|
-
//
|
|
478
|
+
// `needs`: what a job brings that a Client must have opted in to run —
|
|
479
|
+
// jobImage (firmware.image; sw.allowJobImages) and jobCommand
|
|
480
|
+
// (tests.command; allowJobCommands).
|
|
474
481
|
function matchesTarget(r, labels, groupId, resourceId, needs = {}){
|
|
475
482
|
return labels.every((l) => r.labels.includes(l)) &&
|
|
476
483
|
(!groupId || r.group_ids.includes(groupId)) &&
|
|
477
484
|
(!resourceId || r.id === resourceId) &&
|
|
478
|
-
(!needs.jobImage || r.capabilities?.sw?.allowJobImages === true)
|
|
485
|
+
(!needs.jobImage || r.capabilities?.sw?.allowJobImages === true) &&
|
|
486
|
+
(!needs.jobCommand || r.capabilities?.allowJobCommands === true);
|
|
479
487
|
}
|
|
480
488
|
|
|
481
489
|
// Admin: remove a resource from the registry (dashboard "Remove"). Refused
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
const { JOB_STATES, BUSY_SOURCES } = require('@andrian.yablonskyy/thub-common');
|
|
17
17
|
|
|
18
|
+
// What a job needs a Client to have opted in to (registry matchesTarget).
|
|
19
|
+
function jobNeeds(spec){
|
|
20
|
+
return { jobImage: Boolean(spec.firmware?.image), jobCommand: Boolean(spec.tests?.command) };
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
function rowToJob(row){
|
|
19
24
|
return { ...row, spec: JSON.parse(row.spec) };
|
|
20
25
|
}
|
|
@@ -43,7 +48,7 @@ function createScheduler(db, { bus, events, registry, config }){
|
|
|
43
48
|
for (const job of queued){
|
|
44
49
|
const { type, labels = [], group, client } = job.spec.target,
|
|
45
50
|
candidates = registry
|
|
46
|
-
.findIdleCandidates(type, labels, group, client,
|
|
51
|
+
.findIdleCandidates(type, labels, group, client, jobNeeds(job.spec))
|
|
47
52
|
.filter((r) => !claimedResourceIds.has(r.id));
|
|
48
53
|
if (candidates.length === 0){
|
|
49
54
|
continue;
|
package/test/scheduler.test.js
CHANGED
|
@@ -547,6 +547,24 @@ test('a job-supplied Docker image only goes to SW Clients that allow it; otherwi
|
|
|
547
547
|
assert.equal(jobs.get(fileJob.id).resource_id, strict.id); // firmware URLs still go anywhere
|
|
548
548
|
});
|
|
549
549
|
|
|
550
|
+
test('a job-supplied command only goes to Clients with allowJobCommands; otherwise 422 naming it', () => {
|
|
551
|
+
const { registry, agents, jobs, scheduler } = buildTestServices(),
|
|
552
|
+
{ agent } = agents.create({ name: 'dev', kind: 'cli' }),
|
|
553
|
+
commandJob = makeSpec({ tests: { url: 'https://x/t.tar.gz', command: 'make test' } }),
|
|
554
|
+
plain = registerResource(registry, { name: 'sw-plain', type: 'sw' });
|
|
555
|
+
registry.heartbeat(plain.id, { state: 'idle' });
|
|
556
|
+
|
|
557
|
+
assert.throws(() => jobs.create({ agentId: agent.id, source: 'cli', spec: commandJob }), /allowJobCommands/);
|
|
558
|
+
|
|
559
|
+
const { resourceId } = registry.registerAuto({
|
|
560
|
+
clientId: 'c-cmd', name: 'sw-cmd', type: 'sw', labels: [], capabilities: { sw: { image: 'emu' }, allowJobCommands: true }
|
|
561
|
+
});
|
|
562
|
+
registry.heartbeat(resourceId, { state: 'idle' });
|
|
563
|
+
const job = jobs.create({ agentId: agent.id, source: 'cli', spec: commandJob });
|
|
564
|
+
scheduler.runPass();
|
|
565
|
+
assert.equal(jobs.get(job.id).resource_id, resourceId);
|
|
566
|
+
});
|
|
567
|
+
|
|
550
568
|
test('a resource can belong to several groups at once', () => {
|
|
551
569
|
const { registry, groups } = buildTestServices(),
|
|
552
570
|
g1 = groups.create({ name: 'g1' }),
|