@shipfox/api-runners 7.0.2 → 8.0.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 +11 -13
- package/CHANGELOG.md +23 -0
- package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.js +15 -1
- package/dist/presentation/routes/poll-demand.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +17164 -3306
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -8
- package/src/core/runner-sessions.test.ts +3 -3
- package/src/db/job-executions.test.ts +7 -3
- package/src/db/runner-instances.test.ts +47 -0
- package/src/presentation/routes/heartbeat.test.ts +18 -17
- package/src/presentation/routes/manual-registration-tokens.test.ts +8 -4
- package/src/presentation/routes/poll-demand.ts +13 -1
- package/src/presentation/routes/register.test.ts +7 -9
- package/src/presentation/routes/request-job.test.ts +7 -23
- package/src/presentation/routes/runner-enrollment.test.ts +7 -4
- package/test/fixtures/auth-inter-module.ts +1 -30
- package/test/fixtures/auth.ts +85 -0
- package/test/index.ts +8 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-runners",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "8.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -25,21 +25,22 @@
|
|
|
25
25
|
"@temporalio/workflow": "1.18.1",
|
|
26
26
|
"drizzle-orm": "^0.45.2",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
|
-
"@shipfox/api-auth-context": "
|
|
29
|
-
"@shipfox/api-auth-dto": "
|
|
28
|
+
"@shipfox/api-auth-context": "7.1.0",
|
|
29
|
+
"@shipfox/api-auth-dto": "7.1.0",
|
|
30
30
|
"@shipfox/api-runners-dto": "7.0.1",
|
|
31
|
-
"@shipfox/api-workflows-dto": "
|
|
31
|
+
"@shipfox/api-workflows-dto": "8.0.0",
|
|
32
32
|
"@shipfox/config": "1.2.2",
|
|
33
33
|
"@shipfox/inter-module": "0.2.0",
|
|
34
34
|
"@shipfox/node-drizzle": "0.3.2",
|
|
35
|
+
"@shipfox/node-error-monitoring": "0.2.0",
|
|
35
36
|
"@shipfox/node-auth-root-key": "0.2.1",
|
|
36
|
-
"@shipfox/node-fastify": "0.
|
|
37
|
-
"@shipfox/node-module": "0.
|
|
38
|
-
"@shipfox/node-opentelemetry": "0.
|
|
37
|
+
"@shipfox/node-fastify": "0.3.0",
|
|
38
|
+
"@shipfox/node-module": "0.5.0",
|
|
39
|
+
"@shipfox/node-opentelemetry": "0.6.0",
|
|
39
40
|
"@shipfox/node-outbox": "0.2.4",
|
|
40
41
|
"@shipfox/node-postgres": "0.4.2",
|
|
41
42
|
"@shipfox/node-rate-limit": "0.3.0",
|
|
42
|
-
"@shipfox/node-temporal": "0.
|
|
43
|
+
"@shipfox/node-temporal": "0.4.0",
|
|
43
44
|
"@shipfox/node-tokens": "0.3.0",
|
|
44
45
|
"@shipfox/runner-labels": "0.1.1"
|
|
45
46
|
},
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {verifyRunnerSessionToken} from '@shipfox/api-auth';
|
|
2
1
|
import {and, eq} from 'drizzle-orm';
|
|
3
2
|
import {db} from '#db/db.js';
|
|
4
3
|
import {ephemeralRegistrationTokens} from '#db/schema/ephemeral-registration-tokens.js';
|
|
@@ -7,6 +6,7 @@ import {providerRunners} from '#db/schema/runner-instances.js';
|
|
|
7
6
|
import {runnerSessions} from '#db/schema/runner-sessions.js';
|
|
8
7
|
import {
|
|
9
8
|
ephemeralRegistrationTokenFactory,
|
|
9
|
+
getRunnerSessionTokenClaims,
|
|
10
10
|
manualRegistrationTokenFactory,
|
|
11
11
|
providerRunnerFactory,
|
|
12
12
|
runnersTestAuthClient,
|
|
@@ -51,7 +51,7 @@ describe('registerRunnerSession', () => {
|
|
|
51
51
|
.where(eq(runnerSessions.id, result.session.id));
|
|
52
52
|
expect(rows[0]?.labels).toEqual(['linux', 'x64']);
|
|
53
53
|
|
|
54
|
-
const claims =
|
|
54
|
+
const claims = getRunnerSessionTokenClaims(result.sessionToken);
|
|
55
55
|
expect(claims?.labels).toEqual(['linux', 'x64']);
|
|
56
56
|
expect(claims?.maxClaims).toBeNull();
|
|
57
57
|
});
|
|
@@ -104,7 +104,7 @@ describe('registerRunnerSession', () => {
|
|
|
104
104
|
expect(session?.provisionerId).toBe(token.provisionerId);
|
|
105
105
|
expect(session?.providerRunnerId).toBe(token.providerRunnerId);
|
|
106
106
|
|
|
107
|
-
const claims =
|
|
107
|
+
const claims = getRunnerSessionTokenClaims(result.sessionToken);
|
|
108
108
|
expect(claims?.maxClaims).toBe(1);
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {verifyJobLeaseToken} from '@shipfox/api-auth';
|
|
2
1
|
import {
|
|
3
2
|
RUNNER_JOB_CLAIMED,
|
|
4
3
|
RUNNER_JOB_LEASE_EXPIRED,
|
|
@@ -8,7 +7,12 @@ import {eq, sql} from 'drizzle-orm';
|
|
|
8
7
|
import {EmptyRequiredLabelsError, RunnerSessionExhaustedError} from '#core/errors.js';
|
|
9
8
|
import {claimJobExecution} from '#core/job-executions.js';
|
|
10
9
|
import {detectAndExpireStuckJobs} from '#core/maintenance.js';
|
|
11
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getLeaseTokenClaims,
|
|
12
|
+
pendingJobFactory,
|
|
13
|
+
runnerSessionFactory,
|
|
14
|
+
runnersTestAuthClient,
|
|
15
|
+
} from '#test/index.js';
|
|
12
16
|
import {db} from './db.js';
|
|
13
17
|
import {
|
|
14
18
|
cancelRunnerJobs,
|
|
@@ -770,7 +774,7 @@ describe('claimJobExecution', () => {
|
|
|
770
774
|
expect(claimed?.workflowRunAttemptId).toBe(created.workflowRunAttemptId);
|
|
771
775
|
expect(claimed).not.toHaveProperty('steps');
|
|
772
776
|
|
|
773
|
-
const claims =
|
|
777
|
+
const claims = getLeaseTokenClaims(claimed?.leaseToken as string);
|
|
774
778
|
expect(claims).toMatchObject({
|
|
775
779
|
jobId: created.jobId,
|
|
776
780
|
workflowRunAttemptId: created.workflowRunAttemptId,
|
|
@@ -1822,6 +1822,53 @@ describe('reconcileRunnerInstances', () => {
|
|
|
1822
1822
|
});
|
|
1823
1823
|
|
|
1824
1824
|
describe('runner instance provider attachment', () => {
|
|
1825
|
+
it('updates the pre-created runner instance before its first lifecycle report', async () => {
|
|
1826
|
+
const provisionerId = crypto.randomUUID();
|
|
1827
|
+
const providerRunnerId = 'ec2-runner-1';
|
|
1828
|
+
const [instance] = await db()
|
|
1829
|
+
.insert(providerRunners)
|
|
1830
|
+
.values({
|
|
1831
|
+
provisionerId,
|
|
1832
|
+
providerKind: 'ec2',
|
|
1833
|
+
templateKey: 'linux',
|
|
1834
|
+
labels: ['linux'],
|
|
1835
|
+
state: 'starting',
|
|
1836
|
+
reportedAt: new Date(),
|
|
1837
|
+
})
|
|
1838
|
+
.returning({id: providerRunners.id});
|
|
1839
|
+
if (!instance) throw new Error('Runner instance insert returned no row');
|
|
1840
|
+
|
|
1841
|
+
const attached = await attachRunnerInstanceProviderId({
|
|
1842
|
+
runnerInstanceId: instance.id,
|
|
1843
|
+
provisionerId,
|
|
1844
|
+
providerRunnerId,
|
|
1845
|
+
});
|
|
1846
|
+
await reportRunnerInstances({
|
|
1847
|
+
workspaceId: null,
|
|
1848
|
+
provisionerId,
|
|
1849
|
+
events: [
|
|
1850
|
+
{
|
|
1851
|
+
providerRunnerId,
|
|
1852
|
+
reservationId: null,
|
|
1853
|
+
templateKey: 'linux',
|
|
1854
|
+
labels: ['linux'],
|
|
1855
|
+
state: 'starting',
|
|
1856
|
+
reason: null,
|
|
1857
|
+
runnerSessionId: null,
|
|
1858
|
+
providerKind: 'ec2',
|
|
1859
|
+
reportedAt: new Date(),
|
|
1860
|
+
},
|
|
1861
|
+
],
|
|
1862
|
+
});
|
|
1863
|
+
const rows = await db()
|
|
1864
|
+
.select()
|
|
1865
|
+
.from(providerRunners)
|
|
1866
|
+
.where(eq(providerRunners.provisionerId, provisionerId));
|
|
1867
|
+
|
|
1868
|
+
expect(attached).toBe(true);
|
|
1869
|
+
expect(rows).toMatchObject([{id: instance.id, providerRunnerId, state: 'starting'}]);
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1825
1872
|
it('belongs to its provisioner until it receives one provider runner identity', async () => {
|
|
1826
1873
|
const provisionerId = crypto.randomUUID();
|
|
1827
1874
|
const [instance] = await db()
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createLeaseTokenAuthMethod,
|
|
3
|
-
createRunnerSessionAuthMethod,
|
|
4
|
-
issueJobLeaseToken,
|
|
5
|
-
jobLeaseParamsFrom,
|
|
6
|
-
verifyJobLeaseToken,
|
|
7
|
-
} from '@shipfox/api-auth';
|
|
8
1
|
import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
|
|
9
2
|
import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
|
|
10
3
|
import type {AuthMethod} from '@shipfox/node-fastify';
|
|
@@ -17,7 +10,15 @@ import {requestJobExecutionCancellation} from '#db/job-executions.js';
|
|
|
17
10
|
import {runnerSessions} from '#db/schema/runner-sessions.js';
|
|
18
11
|
import {runningJobExecutions} from '#db/schema/running-job-executions.js';
|
|
19
12
|
import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
|
|
20
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
fakeLeaseTokenAuthMethod,
|
|
15
|
+
fakeRunnerSessionAuthMethod,
|
|
16
|
+
getLeaseTokenClaims,
|
|
17
|
+
mintLeaseToken,
|
|
18
|
+
pendingJobFactory,
|
|
19
|
+
runnerSessionFactory,
|
|
20
|
+
runnersTestAuthClient,
|
|
21
|
+
} from '#test/index.js';
|
|
21
22
|
import {createRunnerRoutes} from './index.js';
|
|
22
23
|
|
|
23
24
|
const fakeUserAuth: AuthMethod = {
|
|
@@ -53,8 +54,8 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
|
|
|
53
54
|
auth: [
|
|
54
55
|
fakeUserAuth,
|
|
55
56
|
createRunnerRegistrationTokenAuthMethod(),
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
fakeRunnerSessionAuthMethod,
|
|
58
|
+
fakeLeaseTokenAuthMethod,
|
|
58
59
|
fakeProvisionerAuth,
|
|
59
60
|
],
|
|
60
61
|
routes: createRunnerRoutes(runnersTestAuthClient),
|
|
@@ -120,7 +121,7 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
|
|
|
120
121
|
expect(res.statusCode).toBe(200);
|
|
121
122
|
const body = res.json<{cancel: boolean; lease_token: string}>();
|
|
122
123
|
expect(body).toEqual({cancel: false, lease_token: expect.any(String)});
|
|
123
|
-
const refreshedLease =
|
|
124
|
+
const refreshedLease = getLeaseTokenClaims(body.lease_token);
|
|
124
125
|
expect(refreshedLease).toMatchObject({
|
|
125
126
|
jobId,
|
|
126
127
|
jobExecutionId,
|
|
@@ -236,7 +237,7 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
|
|
|
236
237
|
expect(res.statusCode).toBe(200);
|
|
237
238
|
const body = res.json<{cancel: boolean; lease_token: string}>();
|
|
238
239
|
expect(body).toEqual({cancel: true, lease_token: expect.any(String)});
|
|
239
|
-
const refreshedLease =
|
|
240
|
+
const refreshedLease = getLeaseTokenClaims(body.lease_token);
|
|
240
241
|
expect(refreshedLease).toMatchObject({
|
|
241
242
|
jobId,
|
|
242
243
|
jobExecutionId,
|
|
@@ -248,10 +249,10 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
|
|
|
248
249
|
|
|
249
250
|
it('preserves current step scope when renewing a step-scoped lease', async () => {
|
|
250
251
|
const {jobId, leaseToken} = await claimAvailableJob();
|
|
251
|
-
const lease =
|
|
252
|
+
const lease = getLeaseTokenClaims(leaseToken);
|
|
252
253
|
if (!lease) throw new Error('Expected valid lease token');
|
|
253
254
|
const stepScope = {currentStepId: crypto.randomUUID(), currentStepAttempt: 3};
|
|
254
|
-
const stepScopedLeaseToken =
|
|
255
|
+
const stepScopedLeaseToken = mintLeaseToken({...lease, ...stepScope});
|
|
255
256
|
|
|
256
257
|
const res = await app.inject({
|
|
257
258
|
method: 'POST',
|
|
@@ -261,14 +262,14 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
|
|
|
261
262
|
|
|
262
263
|
expect(res.statusCode).toBe(200);
|
|
263
264
|
const body = res.json<{cancel: boolean; lease_token: string}>();
|
|
264
|
-
const refreshedLease =
|
|
265
|
+
const refreshedLease = getLeaseTokenClaims(body.lease_token);
|
|
265
266
|
expect(refreshedLease).toMatchObject(stepScope);
|
|
266
267
|
});
|
|
267
268
|
|
|
268
269
|
it('returns 404 when the jobId is unknown', async () => {
|
|
269
270
|
const jobId = crypto.randomUUID();
|
|
270
271
|
const jobExecutionId = crypto.randomUUID();
|
|
271
|
-
const leaseToken =
|
|
272
|
+
const leaseToken = mintLeaseToken({
|
|
272
273
|
jobId,
|
|
273
274
|
jobExecutionId,
|
|
274
275
|
workflowRunId: crypto.randomUUID(),
|
|
@@ -291,7 +292,7 @@ describe('POST /runners/jobs/:jobId/heartbeat', () => {
|
|
|
291
292
|
it('returns 404 when the job belongs to a different session', async () => {
|
|
292
293
|
const {jobId, jobExecutionId} = await claimAvailableJob();
|
|
293
294
|
const otherSession = await runnerSessionFactory.create({workspaceId});
|
|
294
|
-
const leaseToken =
|
|
295
|
+
const leaseToken = mintLeaseToken({
|
|
295
296
|
jobId,
|
|
296
297
|
jobExecutionId,
|
|
297
298
|
workflowRunId: crypto.randomUUID(),
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {createLeaseTokenAuthMethod, createRunnerSessionAuthMethod} from '@shipfox/api-auth';
|
|
2
1
|
import {
|
|
3
2
|
AUTH_LEASED_JOB,
|
|
4
3
|
AUTH_PROVISIONER_TOKEN,
|
|
@@ -18,7 +17,12 @@ import {db} from '#db/db.js';
|
|
|
18
17
|
import {revokeManualRegistrationToken} from '#db/manual-registration-tokens.js';
|
|
19
18
|
import {manualRegistrationTokens} from '#db/schema/manual-registration-tokens.js';
|
|
20
19
|
import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
|
|
21
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
fakeLeaseTokenAuthMethod,
|
|
22
|
+
fakeRunnerSessionAuthMethod,
|
|
23
|
+
manualRegistrationTokenFactory,
|
|
24
|
+
runnersTestAuthClient,
|
|
25
|
+
} from '#test/index.js';
|
|
22
26
|
import {createRunnerRoutes} from './index.js';
|
|
23
27
|
|
|
24
28
|
let authenticatedMemberships: ReadonlyArray<UserContextMembership> = [];
|
|
@@ -59,8 +63,8 @@ describe('manual registration token routes', () => {
|
|
|
59
63
|
auth: [
|
|
60
64
|
fakeUserAuth,
|
|
61
65
|
createRunnerRegistrationTokenAuthMethod(),
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
fakeRunnerSessionAuthMethod,
|
|
67
|
+
fakeLeaseTokenAuthMethod,
|
|
64
68
|
fakeProvisionerAuth,
|
|
65
69
|
],
|
|
66
70
|
routes: createRunnerRoutes(runnersTestAuthClient),
|
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
} from '@shipfox/api-auth-context';
|
|
5
5
|
import type {PollDemandTemplateDto} from '@shipfox/api-runners-dto';
|
|
6
6
|
import {pollDemandBodySchema, pollDemandResponseSchema} from '@shipfox/api-runners-dto';
|
|
7
|
+
import {reportError} from '@shipfox/node-error-monitoring';
|
|
7
8
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
9
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
8
10
|
import {config} from '#config.js';
|
|
9
11
|
import {pollDemand, releaseReservationGrants} from '#core/demand.js';
|
|
10
12
|
import {publishWorkspaceProvisionerCapabilitySnapshot} from '#db/provisioner-capability-snapshots.js';
|
|
@@ -50,7 +52,17 @@ export function createPollDemandRoute(options: CreateRunnersModuleOptions = {})
|
|
|
50
52
|
reply.raw.on('close', () => {
|
|
51
53
|
if (!responseFinished) {
|
|
52
54
|
abortController.abort();
|
|
53
|
-
void releaseReservationGrants(responseReservations).catch(() =>
|
|
55
|
+
void releaseReservationGrants(responseReservations).catch((error) => {
|
|
56
|
+
logger().error(
|
|
57
|
+
{err: error, reservationCount: responseReservations.length},
|
|
58
|
+
'Failed to release reservations after provisioner disconnect',
|
|
59
|
+
);
|
|
60
|
+
reportError(error, {
|
|
61
|
+
boundary: 'runners.cleanup',
|
|
62
|
+
operation: 'release-disconnected-reservations',
|
|
63
|
+
extra: {reservationCount: responseReservations.length},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
54
66
|
}
|
|
55
67
|
});
|
|
56
68
|
const provisionerContext = requireProvisionerContext(request);
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createLeaseTokenAuthMethod,
|
|
3
|
-
createRunnerSessionAuthMethod,
|
|
4
|
-
verifyRunnerSessionToken,
|
|
5
|
-
} from '@shipfox/api-auth';
|
|
6
1
|
import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
|
|
7
2
|
import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
|
|
8
3
|
import type {AuthMethod} from '@shipfox/node-fastify';
|
|
@@ -20,6 +15,9 @@ import {runnerSessions} from '#db/schema/runner-sessions.js';
|
|
|
20
15
|
import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
|
|
21
16
|
import {
|
|
22
17
|
ephemeralRegistrationTokenFactory,
|
|
18
|
+
fakeLeaseTokenAuthMethod,
|
|
19
|
+
fakeRunnerSessionAuthMethod,
|
|
20
|
+
getRunnerSessionTokenClaims,
|
|
23
21
|
manualRegistrationTokenFactory,
|
|
24
22
|
runnersTestAuthClient,
|
|
25
23
|
} from '#test/index.js';
|
|
@@ -52,8 +50,8 @@ describe('POST /runners/register', () => {
|
|
|
52
50
|
auth: [
|
|
53
51
|
fakeUserAuth,
|
|
54
52
|
createRunnerRegistrationTokenAuthMethod(),
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
fakeRunnerSessionAuthMethod,
|
|
54
|
+
fakeLeaseTokenAuthMethod,
|
|
57
55
|
fakeProvisionerAuth,
|
|
58
56
|
],
|
|
59
57
|
routes: createRunnerRoutes(runnersTestAuthClient),
|
|
@@ -87,7 +85,7 @@ describe('POST /runners/register', () => {
|
|
|
87
85
|
expect(body.mode).toBe('manual');
|
|
88
86
|
expect(body.max_claims).toBeNull();
|
|
89
87
|
|
|
90
|
-
const claims =
|
|
88
|
+
const claims = getRunnerSessionTokenClaims(body.session_token);
|
|
91
89
|
expect(claims).toMatchObject({
|
|
92
90
|
runnerSessionId: body.session_id,
|
|
93
91
|
workspaceId,
|
|
@@ -144,7 +142,7 @@ describe('POST /runners/register', () => {
|
|
|
144
142
|
expect(body.mode).toBe('ephemeral');
|
|
145
143
|
expect(body.max_claims).toBe(1);
|
|
146
144
|
|
|
147
|
-
const claims =
|
|
145
|
+
const claims = getRunnerSessionTokenClaims(body.session_token);
|
|
148
146
|
expect(claims?.maxClaims).toBe(1);
|
|
149
147
|
|
|
150
148
|
const [session] = await db()
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createLeaseTokenAuthMethod,
|
|
3
|
-
createRunnerSessionAuthMethod,
|
|
4
|
-
verifyJobLeaseToken,
|
|
5
|
-
} from '@shipfox/api-auth';
|
|
6
1
|
import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
|
|
7
|
-
import {RUNNER_SESSION_TOKEN_AUDIENCE} from '@shipfox/api-auth-dto';
|
|
8
|
-
import {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
|
|
9
2
|
import type {AuthMethod} from '@shipfox/node-fastify';
|
|
10
3
|
import {closeApp, createApp} from '@shipfox/node-fastify';
|
|
11
|
-
import {signHs256} from '@shipfox/node-jwt';
|
|
12
4
|
import {generateOpaqueToken} from '@shipfox/node-tokens';
|
|
13
5
|
import type {FastifyInstance} from 'fastify';
|
|
14
6
|
import {createRunnerRegistrationTokenAuthMethod} from '#presentation/auth/index.js';
|
|
15
7
|
import {
|
|
16
8
|
ephemeralRegistrationTokenFactory,
|
|
9
|
+
fakeLeaseTokenAuthMethod,
|
|
10
|
+
fakeRunnerSessionAuthMethod,
|
|
11
|
+
getLeaseTokenClaims,
|
|
17
12
|
manualRegistrationTokenFactory,
|
|
18
13
|
pendingJobFactory,
|
|
19
14
|
runnersTestAuthClient,
|
|
@@ -42,8 +37,8 @@ describe('POST /runners/jobs/request', () => {
|
|
|
42
37
|
auth: [
|
|
43
38
|
fakeUserAuth,
|
|
44
39
|
createRunnerRegistrationTokenAuthMethod(),
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
fakeRunnerSessionAuthMethod,
|
|
41
|
+
fakeLeaseTokenAuthMethod,
|
|
47
42
|
fakeProvisionerAuth,
|
|
48
43
|
],
|
|
49
44
|
routes: createRunnerRoutes(runnersTestAuthClient),
|
|
@@ -116,7 +111,7 @@ describe('POST /runners/jobs/request', () => {
|
|
|
116
111
|
expect(body.job_name).toBeUndefined();
|
|
117
112
|
expect(body.steps).toBeUndefined();
|
|
118
113
|
|
|
119
|
-
const claims =
|
|
114
|
+
const claims = getLeaseTokenClaims(body.lease_token);
|
|
120
115
|
expect(claims).toMatchObject({
|
|
121
116
|
jobId: created.jobId,
|
|
122
117
|
workflowRunId: created.workflowRunId,
|
|
@@ -200,18 +195,7 @@ describe('POST /runners/jobs/request', () => {
|
|
|
200
195
|
});
|
|
201
196
|
|
|
202
197
|
it('returns 401 when the runner session token is expired', async () => {
|
|
203
|
-
const expiredSessionToken =
|
|
204
|
-
payload: {
|
|
205
|
-
runnerSessionId,
|
|
206
|
-
workspaceId,
|
|
207
|
-
scope: 'workspace',
|
|
208
|
-
labels: ['linux', 'x64'],
|
|
209
|
-
maxClaims: null,
|
|
210
|
-
},
|
|
211
|
-
secret: runnerSessionTokenKey(),
|
|
212
|
-
expiresIn: '-1s',
|
|
213
|
-
audience: RUNNER_SESSION_TOKEN_AUDIENCE,
|
|
214
|
-
});
|
|
198
|
+
const expiredSessionToken = 'invalid';
|
|
215
199
|
|
|
216
200
|
const res = await app.inject({
|
|
217
201
|
method: 'POST',
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {createLeaseTokenAuthMethod, createRunnerSessionAuthMethod} from '@shipfox/api-auth';
|
|
2
1
|
import {AUTH_PROVISIONER_TOKEN, AUTH_USER, setProvisionerContext} from '@shipfox/api-auth-context';
|
|
3
2
|
import {
|
|
4
3
|
type AuthMethod,
|
|
@@ -18,7 +17,11 @@ import {
|
|
|
18
17
|
createRunnerControlSessionAuthMethod,
|
|
19
18
|
createRunnerRegistrationTokenAuthMethod,
|
|
20
19
|
} from '#presentation/auth/index.js';
|
|
21
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
fakeLeaseTokenAuthMethod,
|
|
22
|
+
fakeRunnerSessionAuthMethod,
|
|
23
|
+
runnersTestAuthClient,
|
|
24
|
+
} from '#test/index.js';
|
|
22
25
|
import {createRunnerRoutes} from './index.js';
|
|
23
26
|
|
|
24
27
|
const token = 'provisioner-test-token';
|
|
@@ -44,8 +47,8 @@ describe('runner enrollment control plane', () => {
|
|
|
44
47
|
provisionerAuth,
|
|
45
48
|
createRunnerRegistrationTokenAuthMethod(),
|
|
46
49
|
createRunnerControlSessionAuthMethod(),
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
fakeRunnerSessionAuthMethod,
|
|
51
|
+
fakeLeaseTokenAuthMethod,
|
|
49
52
|
],
|
|
50
53
|
routes: createRunnerRoutes(runnersTestAuthClient),
|
|
51
54
|
swagger: false,
|
|
@@ -1,30 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type {AuthInterModuleClient} from '@shipfox/api-auth-dto/inter-module';
|
|
3
|
-
import {jobLeaseTokenKey, runnerSessionTokenKey} from '@shipfox/node-auth-root-key';
|
|
4
|
-
import {signHs256} from '@shipfox/node-jwt';
|
|
5
|
-
|
|
6
|
-
const leaseSecret = jobLeaseTokenKey();
|
|
7
|
-
const runnerSessionSecret = runnerSessionTokenKey();
|
|
8
|
-
|
|
9
|
-
export const runnersTestAuthClient: AuthInterModuleClient = {
|
|
10
|
-
async mintRunnerSessionToken(claims) {
|
|
11
|
-
return {
|
|
12
|
-
token: await signHs256({
|
|
13
|
-
payload: claims,
|
|
14
|
-
secret: runnerSessionSecret,
|
|
15
|
-
expiresIn: '1h',
|
|
16
|
-
audience: RUNNER_SESSION_TOKEN_AUDIENCE,
|
|
17
|
-
}),
|
|
18
|
-
};
|
|
19
|
-
},
|
|
20
|
-
async mintJobLeaseToken(claims) {
|
|
21
|
-
return {
|
|
22
|
-
token: await signHs256({
|
|
23
|
-
payload: claims,
|
|
24
|
-
secret: leaseSecret,
|
|
25
|
-
expiresIn: '90m',
|
|
26
|
-
audience: JOB_LEASE_TOKEN_AUDIENCE,
|
|
27
|
-
}),
|
|
28
|
-
};
|
|
29
|
-
},
|
|
30
|
-
};
|
|
1
|
+
export {runnersTestAuthClient} from './auth.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTH_LEASED_JOB,
|
|
3
|
+
AUTH_RUNNER_SESSION,
|
|
4
|
+
setLeasedJobContext,
|
|
5
|
+
setRunnerSessionContext,
|
|
6
|
+
} from '@shipfox/api-auth-context';
|
|
7
|
+
import {
|
|
8
|
+
JOB_LEASE_TOKEN_AUDIENCE,
|
|
9
|
+
type JobLeaseTokenClaims,
|
|
10
|
+
RUNNER_SESSION_TOKEN_AUDIENCE,
|
|
11
|
+
type RunnerSessionTokenClaims,
|
|
12
|
+
} from '@shipfox/api-auth-dto';
|
|
13
|
+
import type {AuthInterModuleClient} from '@shipfox/api-auth-dto/inter-module';
|
|
14
|
+
import {type AuthMethod, ClientError, extractBearerToken} from '@shipfox/node-fastify';
|
|
15
|
+
|
|
16
|
+
const leaseClaims = new Map<string, JobLeaseTokenClaims>();
|
|
17
|
+
const sessionClaims = new Map<string, RunnerSessionTokenClaims>();
|
|
18
|
+
|
|
19
|
+
function opaqueToken(): string {
|
|
20
|
+
return `test-auth-${crypto.randomUUID()}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mintLeaseToken(claims: Omit<JobLeaseTokenClaims, 'aud' | 'iat' | 'exp'>): string {
|
|
24
|
+
const token = opaqueToken();
|
|
25
|
+
leaseClaims.set(token, {
|
|
26
|
+
...claims,
|
|
27
|
+
aud: JOB_LEASE_TOKEN_AUDIENCE,
|
|
28
|
+
iat: 0,
|
|
29
|
+
exp: Number.MAX_SAFE_INTEGER,
|
|
30
|
+
});
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mintRunnerSessionToken(
|
|
35
|
+
claims: Omit<RunnerSessionTokenClaims, 'aud' | 'iat' | 'exp'>,
|
|
36
|
+
): string {
|
|
37
|
+
const token = opaqueToken();
|
|
38
|
+
sessionClaims.set(token, {
|
|
39
|
+
...claims,
|
|
40
|
+
aud: RUNNER_SESSION_TOKEN_AUDIENCE,
|
|
41
|
+
iat: 0,
|
|
42
|
+
exp: Number.MAX_SAFE_INTEGER,
|
|
43
|
+
});
|
|
44
|
+
return token;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getLeaseTokenClaims(token: string): JobLeaseTokenClaims | undefined {
|
|
48
|
+
return leaseClaims.get(token);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getRunnerSessionTokenClaims(token: string): RunnerSessionTokenClaims | undefined {
|
|
52
|
+
return sessionClaims.get(token);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireClaims<T>(claims: Map<string, T>, authorization: string | undefined): T {
|
|
56
|
+
const token = extractBearerToken(authorization);
|
|
57
|
+
const context = token ? claims.get(token) : undefined;
|
|
58
|
+
if (!context) throw new ClientError('Authentication required', 'unauthorized', {status: 401});
|
|
59
|
+
return context;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const fakeLeaseTokenAuthMethod: AuthMethod = {
|
|
63
|
+
name: AUTH_LEASED_JOB,
|
|
64
|
+
authenticate: (request) => {
|
|
65
|
+
setLeasedJobContext(request, requireClaims(leaseClaims, request.headers.authorization));
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const fakeRunnerSessionAuthMethod: AuthMethod = {
|
|
71
|
+
name: AUTH_RUNNER_SESSION,
|
|
72
|
+
authenticate: (request) => {
|
|
73
|
+
setRunnerSessionContext(request, requireClaims(sessionClaims, request.headers.authorization));
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const runnersTestAuthClient: AuthInterModuleClient = {
|
|
79
|
+
mintRunnerSessionToken(claims) {
|
|
80
|
+
return Promise.resolve({token: mintRunnerSessionToken(claims)});
|
|
81
|
+
},
|
|
82
|
+
mintJobLeaseToken(claims) {
|
|
83
|
+
return Promise.resolve({token: mintLeaseToken(claims)});
|
|
84
|
+
},
|
|
85
|
+
};
|
package/test/index.ts
CHANGED
|
@@ -14,4 +14,12 @@ export {
|
|
|
14
14
|
export {reservationFactory} from './factories/reservation.js';
|
|
15
15
|
export {providerRunnerFactory} from './factories/runner-instance.js';
|
|
16
16
|
export {runnerSessionFactory} from './factories/runner-session.js';
|
|
17
|
+
export {
|
|
18
|
+
fakeLeaseTokenAuthMethod,
|
|
19
|
+
fakeRunnerSessionAuthMethod,
|
|
20
|
+
getLeaseTokenClaims,
|
|
21
|
+
getRunnerSessionTokenClaims,
|
|
22
|
+
mintLeaseToken,
|
|
23
|
+
mintRunnerSessionToken,
|
|
24
|
+
} from './fixtures/auth.js';
|
|
17
25
|
export {runnersTestAuthClient} from './fixtures/auth-inter-module.js';
|