@shipfox/api-runners 10.0.0 → 10.2.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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +28 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -13
- package/dist/config.js.map +1 -1
- package/dist/core/admin-runner-instances.d.ts +34 -0
- package/dist/core/admin-runner-instances.d.ts.map +1 -0
- package/dist/core/admin-runner-instances.js +58 -0
- package/dist/core/admin-runner-instances.js.map +1 -0
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +12 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/provisioner-tokens.d.ts +33 -0
- package/dist/core/provisioner-tokens.d.ts.map +1 -1
- package/dist/core/provisioner-tokens.js +107 -2
- package/dist/core/provisioner-tokens.js.map +1 -1
- package/dist/db/admin-runner-instances.d.ts +34 -0
- package/dist/db/admin-runner-instances.d.ts.map +1 -0
- package/dist/db/admin-runner-instances.js +62 -0
- package/dist/db/admin-runner-instances.js.map +1 -0
- package/dist/db/db.d.ts +290 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +3 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/job-executions.d.ts.map +1 -1
- package/dist/db/job-executions.js +11 -0
- package/dist/db/job-executions.js.map +1 -1
- package/dist/db/provisioner-tokens.d.ts +38 -0
- package/dist/db/provisioner-tokens.d.ts.map +1 -1
- package/dist/db/provisioner-tokens.js +157 -3
- package/dist/db/provisioner-tokens.js.map +1 -1
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +60 -8
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +134 -0
- package/dist/db/schema/admin-command-results.d.ts.map +1 -0
- package/dist/db/schema/admin-command-results.js +18 -0
- package/dist/db/schema/admin-command-results.js.map +1 -0
- package/dist/db/schema/runner-instances.d.ts +17 -0
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +3 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/dto/admin-provisioner-token.d.ts +16 -0
- package/dist/presentation/dto/admin-provisioner-token.d.ts.map +1 -0
- package/dist/presentation/dto/admin-provisioner-token.js +22 -0
- package/dist/presentation/dto/admin-provisioner-token.js.map +1 -0
- package/dist/presentation/dto/admin-runner-instances.d.ts +23 -0
- package/dist/presentation/dto/admin-runner-instances.d.ts.map +1 -0
- package/dist/presentation/dto/admin-runner-instances.js +19 -0
- package/dist/presentation/dto/admin-runner-instances.js.map +1 -0
- package/dist/presentation/dto/index.d.ts +2 -0
- package/dist/presentation/dto/index.d.ts.map +1 -1
- package/dist/presentation/dto/index.js +2 -0
- package/dist/presentation/dto/index.js.map +1 -1
- package/dist/presentation/routes/admin-provisioner-tokens.d.ts +4 -0
- package/dist/presentation/routes/admin-provisioner-tokens.d.ts.map +1 -0
- package/dist/presentation/routes/admin-provisioner-tokens.js +167 -0
- package/dist/presentation/routes/admin-provisioner-tokens.js.map +1 -0
- package/dist/presentation/routes/admin-runner-instances.d.ts +3 -0
- package/dist/presentation/routes/admin-runner-instances.d.ts.map +1 -0
- package/dist/presentation/routes/admin-runner-instances.js +63 -0
- package/dist/presentation/routes/admin-runner-instances.js.map +1 -0
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +11 -1
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/runner-enrollment.d.ts +1 -0
- package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
- package/dist/presentation/routes/runner-enrollment.js +11 -4
- package/dist/presentation/routes/runner-enrollment.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_special_shadowcat.sql +1 -0
- package/drizzle/0003_flaky_sally_floyd.sql +11 -0
- package/drizzle/meta/0002_snapshot.json +2099 -0
- package/drizzle/meta/0003_snapshot.json +2178 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +10 -9
- package/src/config.test.ts +11 -0
- package/src/config.ts +19 -8
- package/src/core/admin-runner-instances.ts +125 -0
- package/src/core/errors.ts +14 -0
- package/src/core/index.ts +12 -0
- package/src/core/provisioner-tokens.ts +157 -1
- package/src/db/admin-runner-instances.ts +121 -0
- package/src/db/db.ts +2 -0
- package/src/db/index.ts +10 -0
- package/src/db/job-executions.test.ts +17 -0
- package/src/db/job-executions.ts +22 -0
- package/src/db/provisioner-tokens.ts +263 -9
- package/src/db/reservations.test.ts +253 -0
- package/src/db/reservations.ts +170 -15
- package/src/db/schema/admin-command-results.ts +29 -0
- package/src/db/schema/runner-instances.ts +1 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +6 -1
- package/src/presentation/dto/admin-provisioner-token.ts +23 -0
- package/src/presentation/dto/admin-runner-instances.ts +19 -0
- package/src/presentation/dto/index.ts +2 -0
- package/src/presentation/routes/admin-provisioner-tokens.test.ts +227 -0
- package/src/presentation/routes/admin-provisioner-tokens.ts +177 -0
- package/src/presentation/routes/admin-runner-instances.test.ts +671 -0
- package/src/presentation/routes/admin-runner-instances.ts +67 -0
- package/src/presentation/routes/index.ts +8 -0
- package/src/presentation/routes/runner-enrollment.test.ts +60 -2
- package/src/presentation/routes/runner-enrollment.ts +24 -4
- package/test/globalSetup.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {AUTH_USER, requireUserContext} from '@shipfox/api-auth-context';
|
|
2
|
+
import {
|
|
3
|
+
type AuthInterModuleClient,
|
|
4
|
+
authInterModuleContract,
|
|
5
|
+
} from '@shipfox/api-auth-dto/inter-module';
|
|
6
|
+
import {
|
|
7
|
+
listRunnerAdministratorInstancesQuerySchema,
|
|
8
|
+
listRunnerAdministratorInstancesResponseSchema,
|
|
9
|
+
} from '@shipfox/api-runners-dto';
|
|
10
|
+
import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
11
|
+
import {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';
|
|
12
|
+
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
13
|
+
import {listRunnerAdministratorInstances} from '#core/admin-runner-instances.js';
|
|
14
|
+
import {toRunnerAdministratorInstanceDto} from '#presentation/dto/admin-runner-instances.js';
|
|
15
|
+
|
|
16
|
+
function translateAdminRunnerInstancesRouteError(error: unknown): never {
|
|
17
|
+
if (
|
|
18
|
+
isInterModuleKnownError(authInterModuleContract.methods.requireAdminRole, error) &&
|
|
19
|
+
error.code === 'admin-role-required'
|
|
20
|
+
) {
|
|
21
|
+
throw new ClientError('Administrator observer role required', 'forbidden', {
|
|
22
|
+
status: 403,
|
|
23
|
+
details: {required_role: error.details.requiredRole},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createAdminRunnerInstancesRoute(
|
|
31
|
+
auth: Pick<AuthInterModuleClient, 'requireAdminRole'>,
|
|
32
|
+
) {
|
|
33
|
+
return defineRoute({
|
|
34
|
+
method: 'GET',
|
|
35
|
+
path: '/',
|
|
36
|
+
auth: AUTH_USER,
|
|
37
|
+
description: 'List bounded safe installation-managed runner instances for administrators.',
|
|
38
|
+
schema: {
|
|
39
|
+
querystring: listRunnerAdministratorInstancesQuerySchema,
|
|
40
|
+
response: {200: listRunnerAdministratorInstancesResponseSchema},
|
|
41
|
+
},
|
|
42
|
+
errorHandler: translateAdminRunnerInstancesRouteError,
|
|
43
|
+
handler: async (request) => {
|
|
44
|
+
const actor = requireUserContext(request);
|
|
45
|
+
await auth.requireAdminRole({userId: actor.userId, minimumRole: 'admin-observer'});
|
|
46
|
+
|
|
47
|
+
const {state, assignment, label, limit, cursor} = request.query;
|
|
48
|
+
const decodedCursor = decodeTimestampIdCursor(cursor);
|
|
49
|
+
if (cursor !== undefined && !decodedCursor) {
|
|
50
|
+
throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = await listRunnerAdministratorInstances({
|
|
54
|
+
state,
|
|
55
|
+
assignment,
|
|
56
|
+
label,
|
|
57
|
+
limit,
|
|
58
|
+
...(decodedCursor ? {cursor: decodedCursor} : {}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
runners: result.runners.map(toRunnerAdministratorInstanceDto),
|
|
63
|
+
next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
import type {AuthInterModuleClient} from '@shipfox/api-auth-dto/inter-module';
|
|
9
9
|
import type {RouteGroup} from '@shipfox/node-fastify';
|
|
10
10
|
import type {CreateRunnersModuleOptions} from '#installation-provisioning.js';
|
|
11
|
+
import {createAdminProvisionerTokenRoutes} from './admin-provisioner-tokens.js';
|
|
12
|
+
import {createAdminRunnerInstancesRoute} from './admin-runner-instances.js';
|
|
11
13
|
import {assignRunnerInstancesRoute} from './assign-runner-instances.js';
|
|
12
14
|
import {createManualRegistrationTokenRoute} from './create-manual-registration-token.js';
|
|
13
15
|
import {createProvisionerTokenRoute} from './create-provisioner-token.js';
|
|
@@ -77,6 +79,12 @@ function createRunnerOnlyRoutes(auth: AuthInterModuleClient): RouteGroup[] {
|
|
|
77
79
|
reconcileRunnerInstancesRoute,
|
|
78
80
|
],
|
|
79
81
|
},
|
|
82
|
+
{
|
|
83
|
+
prefix: '/admin/runners/instances',
|
|
84
|
+
auth: AUTH_USER,
|
|
85
|
+
routes: [createAdminRunnerInstancesRoute(auth)],
|
|
86
|
+
},
|
|
87
|
+
createAdminProvisionerTokenRoutes(auth),
|
|
80
88
|
];
|
|
81
89
|
}
|
|
82
90
|
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
runnersTestAuthClient,
|
|
24
24
|
} from '#test/index.js';
|
|
25
25
|
import {createRunnerRoutes} from './index.js';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
pollForRunnerActivationToken,
|
|
28
|
+
resolveRunnerAssignmentPollWaitSeconds,
|
|
29
|
+
} from './runner-enrollment.js';
|
|
27
30
|
|
|
28
31
|
const token = 'provisioner-test-token';
|
|
29
32
|
const fakeUserAuth: AuthMethod = {name: AUTH_USER, authenticate: () => Promise.resolve()};
|
|
@@ -65,6 +68,14 @@ describe('runner enrollment control plane', () => {
|
|
|
65
68
|
provisionerId = crypto.randomUUID();
|
|
66
69
|
});
|
|
67
70
|
|
|
71
|
+
it('uses the requested assignment wait below the server cap and caps larger requests', () => {
|
|
72
|
+
expect(resolveRunnerAssignmentPollWaitSeconds(5)).toBe(5);
|
|
73
|
+
expect(resolveRunnerAssignmentPollWaitSeconds(0)).toBe(1);
|
|
74
|
+
expect(resolveRunnerAssignmentPollWaitSeconds(30)).toBe(30);
|
|
75
|
+
expect(resolveRunnerAssignmentPollWaitSeconds(31)).toBe(30);
|
|
76
|
+
expect(resolveRunnerAssignmentPollWaitSeconds(undefined)).toBe(30);
|
|
77
|
+
});
|
|
78
|
+
|
|
68
79
|
it('creates a hashed one-use bootstrap token and restricts its control session to its instance', async () => {
|
|
69
80
|
const created = await app.inject({
|
|
70
81
|
method: 'POST',
|
|
@@ -370,7 +381,7 @@ describe('runner enrollment control plane', () => {
|
|
|
370
381
|
|
|
371
382
|
const assignment = await app.inject({
|
|
372
383
|
method: 'GET',
|
|
373
|
-
url: '/runner-control/assignment',
|
|
384
|
+
url: '/runner-control/assignment?wait_seconds=5',
|
|
374
385
|
headers: {authorization: `Bearer ${controlToken}`},
|
|
375
386
|
});
|
|
376
387
|
expect(assignment.statusCode).toBe(200);
|
|
@@ -394,6 +405,29 @@ describe('runner enrollment control plane', () => {
|
|
|
394
405
|
expect(closed.statusCode).toBe(401);
|
|
395
406
|
});
|
|
396
407
|
|
|
408
|
+
it('rejects a negative assignment wait before entering the control-plane poll', async () => {
|
|
409
|
+
const created = await app.inject({
|
|
410
|
+
method: 'POST',
|
|
411
|
+
url: '/provisioners/runner-instances/batch',
|
|
412
|
+
headers: {authorization: `Bearer ${token}`},
|
|
413
|
+
payload: {runner_instances: [{}]},
|
|
414
|
+
});
|
|
415
|
+
const runner = created.json().runner_instances[0];
|
|
416
|
+
const exchanged = await app.inject({
|
|
417
|
+
method: 'POST',
|
|
418
|
+
url: '/runner-enrollment/exchange',
|
|
419
|
+
payload: {bootstrap_token: runner.bootstrap_token},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const assignment = await app.inject({
|
|
423
|
+
method: 'GET',
|
|
424
|
+
url: '/runner-control/assignment?wait_seconds=-1',
|
|
425
|
+
headers: {authorization: `Bearer ${exchanged.json().control_session_token}`},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(assignment.statusCode).toBe(400);
|
|
429
|
+
});
|
|
430
|
+
|
|
397
431
|
it('keeps polling when an assignment cannot yet mint an activation token', async () => {
|
|
398
432
|
vi.useFakeTimers();
|
|
399
433
|
const abortController = new AbortController();
|
|
@@ -419,6 +453,30 @@ describe('runner enrollment control plane', () => {
|
|
|
419
453
|
vi.useRealTimers();
|
|
420
454
|
}
|
|
421
455
|
});
|
|
456
|
+
|
|
457
|
+
it('does not read or sleep past the assignment poll deadline', async () => {
|
|
458
|
+
vi.useFakeTimers();
|
|
459
|
+
const abortController = new AbortController();
|
|
460
|
+
const getAssignment = vi.fn().mockResolvedValue(null);
|
|
461
|
+
const issueActivationToken = vi.fn();
|
|
462
|
+
try {
|
|
463
|
+
const activationToken = pollForRunnerActivationToken({
|
|
464
|
+
deadline: Date.now() + 10,
|
|
465
|
+
intervalMs: 100,
|
|
466
|
+
signal: abortController.signal,
|
|
467
|
+
getAssignment,
|
|
468
|
+
issueActivationToken,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
472
|
+
|
|
473
|
+
await expect(activationToken).resolves.toBeNull();
|
|
474
|
+
expect(getAssignment).toHaveBeenCalledTimes(1);
|
|
475
|
+
expect(issueActivationToken).not.toHaveBeenCalled();
|
|
476
|
+
} finally {
|
|
477
|
+
vi.useRealTimers();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
422
480
|
it('rejects registration with an expired activation token', async () => {
|
|
423
481
|
const created = await app.inject({
|
|
424
482
|
method: 'POST',
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
attachRunnerControlProviderIdBodySchema,
|
|
7
7
|
createRunnerInstancesBodySchema,
|
|
8
8
|
createRunnerInstancesResponseSchema,
|
|
9
|
+
runnerAssignmentPollQuerySchema,
|
|
9
10
|
runnerAssignmentPollResponseSchema,
|
|
10
11
|
runnerBootstrapExchangeBodySchema,
|
|
11
12
|
runnerBootstrapExchangeResponseSchema,
|
|
@@ -175,17 +176,32 @@ type RunnerAssignmentPollParams = {
|
|
|
175
176
|
getAssignment: () => Promise<unknown | null>;
|
|
176
177
|
issueActivationToken: () => Promise<string | null>;
|
|
177
178
|
};
|
|
179
|
+
|
|
180
|
+
export function resolveRunnerAssignmentPollWaitSeconds(
|
|
181
|
+
requestedWaitSeconds: number | undefined,
|
|
182
|
+
): number {
|
|
183
|
+
return Math.max(
|
|
184
|
+
1,
|
|
185
|
+
Math.min(
|
|
186
|
+
requestedWaitSeconds ?? config.RUNNER_ASSIGNMENT_POLL_MAX_WAIT_SECONDS,
|
|
187
|
+
config.RUNNER_ASSIGNMENT_POLL_MAX_WAIT_SECONDS,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
178
192
|
export async function pollForRunnerActivationToken(
|
|
179
193
|
params: RunnerAssignmentPollParams,
|
|
180
194
|
): Promise<string | null> {
|
|
181
|
-
while (Date.now()
|
|
195
|
+
while (Date.now() < params.deadline) {
|
|
182
196
|
if (params.signal.aborted) return null;
|
|
183
197
|
const assignment = await params.getAssignment();
|
|
184
198
|
if (assignment) {
|
|
185
199
|
const activationToken = await params.issueActivationToken();
|
|
186
200
|
if (activationToken) return activationToken;
|
|
187
201
|
}
|
|
188
|
-
|
|
202
|
+
const remainingMs = params.deadline - Date.now();
|
|
203
|
+
if (remainingMs <= 0 || params.signal.aborted) return null;
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(params.intervalMs, remainingMs)));
|
|
189
205
|
}
|
|
190
206
|
return null;
|
|
191
207
|
}
|
|
@@ -193,11 +209,15 @@ export const runnerAssignmentPollRoute = defineRoute({
|
|
|
193
209
|
method: 'GET',
|
|
194
210
|
path: '/assignment',
|
|
195
211
|
description: 'Wait for only the authenticated runner instance assignment',
|
|
196
|
-
schema: {
|
|
212
|
+
schema: {
|
|
213
|
+
querystring: runnerAssignmentPollQuerySchema,
|
|
214
|
+
response: {200: runnerAssignmentPollResponseSchema},
|
|
215
|
+
},
|
|
197
216
|
preHandler: authenticateRunnerControlSession,
|
|
198
217
|
handler: async (request, reply) => {
|
|
199
218
|
const session = requireRunnerControlSessionContext(request);
|
|
200
|
-
const
|
|
219
|
+
const waitSeconds = resolveRunnerAssignmentPollWaitSeconds(request.query.wait_seconds);
|
|
220
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
201
221
|
const abortController = new AbortController();
|
|
202
222
|
reply.raw.on('close', () => abortController.abort());
|
|
203
223
|
const activationToken = await pollForRunnerActivationToken({
|
package/test/globalSetup.ts
CHANGED
|
@@ -20,6 +20,7 @@ export async function setup() {
|
|
|
20
20
|
await db().execute(sql`TRUNCATE runners_pending_jobs CASCADE`);
|
|
21
21
|
await db().execute(sql`TRUNCATE runners_running_jobs CASCADE`);
|
|
22
22
|
await db().execute(sql`TRUNCATE runners_outbox CASCADE`);
|
|
23
|
+
await db().execute(sql`TRUNCATE runners_admin_command_results CASCADE`);
|
|
23
24
|
|
|
24
25
|
closeDb();
|
|
25
26
|
await closePostgresClient();
|