@shipfox/api-runners 12.0.0 → 12.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 +30 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -2
- package/dist/config.js.map +1 -1
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/rate-limit.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +23 -9
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/core/runner-instances.d.ts +1 -0
- package/dist/core/runner-instances.d.ts.map +1 -1
- package/dist/core/runner-instances.js.map +1 -1
- package/dist/core/runner-labels.d.ts +11 -0
- package/dist/core/runner-labels.d.ts.map +1 -0
- package/dist/core/runner-labels.js +41 -0
- package/dist/core/runner-labels.js.map +1 -0
- package/dist/core/runner-sessions.d.ts.map +1 -1
- package/dist/core/runner-sessions.js +5 -1
- package/dist/core/runner-sessions.js.map +1 -1
- package/dist/db/db.d.ts +64 -64
- package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.js +12 -2
- package/dist/db/ephemeral-registration-tokens.js.map +1 -1
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +15 -5
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-instances.d.ts +1 -0
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +6 -4
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-sessions.d.ts.map +1 -1
- package/dist/db/runner-sessions.js +12 -1
- package/dist/db/runner-sessions.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +1 -1
- package/dist/db/schema/ephemeral-registration-tokens.d.ts +1 -1
- package/dist/db/schema/manual-registration-tokens.d.ts +1 -1
- package/dist/db/schema/outbox.d.ts +1 -1
- package/dist/db/schema/pending-job-executions.d.ts +3 -3
- package/dist/db/schema/provisioner-capability-snapshots.d.ts +3 -3
- package/dist/db/schema/provisioner-tokens.d.ts +2 -2
- package/dist/db/schema/rate-limits.d.ts +1 -1
- package/dist/db/schema/reservations.d.ts +3 -3
- package/dist/db/schema/runner-activation-tokens.d.ts +1 -1
- package/dist/db/schema/runner-control-sessions.d.ts +2 -2
- package/dist/db/schema/runner-instances.d.ts +4 -4
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +1 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/db/schema/runner-sessions.d.ts +4 -4
- package/dist/db/schema/running-job-executions.d.ts +5 -5
- package/dist/metrics/index.d.ts +2 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +13 -8
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +8 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/dto/admin-provisioner-token.d.ts +1 -1
- package/dist/presentation/dto/admin-provisioner-token.d.ts.map +1 -1
- package/dist/presentation/dto/admin-runner-instances.d.ts +2 -2
- package/dist/presentation/dto/admin-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.js +12 -6
- package/dist/presentation/routes/poll-demand.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/register.d.ts.map +1 -1
- package/dist/presentation/routes/register.js +9 -1
- package/dist/presentation/routes/register.js.map +1 -1
- package/dist/presentation/routes/report-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/report-runner-instances.js +1 -0
- package/dist/presentation/routes/report-runner-instances.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0004_huge_skreet.sql +1 -0
- package/drizzle/meta/0004_snapshot.json +2194 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +9 -9
- package/src/config.test.ts +81 -0
- package/src/config.ts +39 -2
- package/src/core/errors.ts +9 -0
- package/src/core/index.ts +1 -0
- package/src/core/runner-control-sessions.test.ts +239 -0
- package/src/core/runner-control-sessions.ts +29 -10
- package/src/core/runner-instances.ts +1 -0
- package/src/core/runner-labels.test.ts +53 -0
- package/src/core/runner-labels.ts +51 -0
- package/src/core/runner-sessions.test.ts +34 -1
- package/src/core/runner-sessions.ts +8 -1
- package/src/db/ephemeral-registration-tokens.ts +15 -1
- package/src/db/reservations.test.ts +169 -0
- package/src/db/reservations.ts +42 -8
- package/src/db/runner-instances.test.ts +45 -1
- package/src/db/runner-instances.ts +7 -2
- package/src/db/runner-sessions.ts +14 -1
- package/src/db/schema/runner-instances.ts +3 -0
- package/src/metrics/index.ts +2 -0
- package/src/metrics/instance.ts +18 -0
- package/src/presentation/routes/poll-demand.test.ts +99 -3
- package/src/presentation/routes/poll-demand.ts +18 -6
- package/src/presentation/routes/register.test.ts +116 -2
- package/src/presentation/routes/register.ts +7 -0
- package/src/presentation/routes/report-runner-instances.test.ts +33 -0
- package/src/presentation/routes/report-runner-instances.ts +1 -0
- package/src/presentation/routes/runner-enrollment.test.ts +65 -5
- package/src/temporal/activities/maintenance-activities.test.ts +24 -31
- package/test/env.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {AUTH_PROVISIONER_TOKEN, AUTH_USER} from '@shipfox/api-auth-context';
|
|
2
2
|
import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
|
|
3
3
|
import type {AuthMethod} from '@shipfox/node-fastify';
|
|
4
|
-
import {closeApp, createApp} from '@shipfox/node-fastify';
|
|
4
|
+
import {ClientError, closeApp, createApp} from '@shipfox/node-fastify';
|
|
5
5
|
import {generateOpaqueToken} from '@shipfox/node-tokens';
|
|
6
6
|
import {eq, sql} from 'drizzle-orm';
|
|
7
|
-
import type {FastifyInstance} from 'fastify';
|
|
7
|
+
import type {FastifyInstance, FastifyReply, FastifyRequest} from 'fastify';
|
|
8
8
|
import {config} from '#config.js';
|
|
9
|
+
import {RunnerLabelsReservedError} from '#core/errors.js';
|
|
9
10
|
import {hashRunnersRateLimitIdentifier} from '#core/rate-limit.js';
|
|
10
11
|
import {db} from '#db/db.js';
|
|
11
12
|
import {revokeManualRegistrationToken} from '#db/manual-registration-tokens.js';
|
|
@@ -19,9 +20,11 @@ import {
|
|
|
19
20
|
fakeRunnerSessionAuthMethod,
|
|
20
21
|
getRunnerSessionTokenClaims,
|
|
21
22
|
manualRegistrationTokenFactory,
|
|
23
|
+
provisionerTokenFactory,
|
|
22
24
|
runnersTestAuthClient,
|
|
23
25
|
} from '#test/index.js';
|
|
24
26
|
import {createRunnerRoutes} from './index.js';
|
|
27
|
+
import {createRegisterRoute} from './register.js';
|
|
25
28
|
|
|
26
29
|
const fakeUserAuth: AuthMethod = {
|
|
27
30
|
name: AUTH_USER,
|
|
@@ -123,6 +126,38 @@ describe('POST /runners/register', () => {
|
|
|
123
126
|
expect(session?.toolCapabilitiesReportedAt).toBeInstanceOf(Date);
|
|
124
127
|
});
|
|
125
128
|
|
|
129
|
+
it('strips reserved labels from manual registration', async () => {
|
|
130
|
+
const res = await app.inject({
|
|
131
|
+
method: 'POST',
|
|
132
|
+
url: '/runners/register',
|
|
133
|
+
headers: {authorization: `Bearer ${rawToken}`},
|
|
134
|
+
payload: {labels: ['linux', 'shipfox-managed', 'x64']},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const [session] = await db()
|
|
138
|
+
.select()
|
|
139
|
+
.from(runnerSessions)
|
|
140
|
+
.where(eq(runnerSessions.id, res.json().session_id));
|
|
141
|
+
|
|
142
|
+
expect(res.statusCode).toBe(200);
|
|
143
|
+
expect(session?.labels).toEqual(['linux', 'x64']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns a distinct error when all manual registration labels are reserved', async () => {
|
|
147
|
+
const res = await app.inject({
|
|
148
|
+
method: 'POST',
|
|
149
|
+
url: '/runners/register',
|
|
150
|
+
headers: {authorization: `Bearer ${rawToken}`},
|
|
151
|
+
payload: {labels: ['shipfox-managed']},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(res.statusCode).toBe(400);
|
|
155
|
+
expect(res.json()).toMatchObject({
|
|
156
|
+
code: 'runner-labels-reserved',
|
|
157
|
+
details: {labels: ['shipfox-managed']},
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
126
161
|
it('exchanges an ephemeral registration token for a one-claim runner session', async () => {
|
|
127
162
|
const ephemeralRawToken = generateOpaqueToken('ephemeralRegistrationToken');
|
|
128
163
|
const token = await ephemeralRegistrationTokenFactory.create(
|
|
@@ -165,6 +200,65 @@ describe('POST /runners/register', () => {
|
|
|
165
200
|
expect(consumed?.consumedSessionId).toBe(body.session_id);
|
|
166
201
|
});
|
|
167
202
|
|
|
203
|
+
it('strips reserved labels from ephemeral registration', async () => {
|
|
204
|
+
const ephemeralRawToken = generateOpaqueToken('ephemeralRegistrationToken');
|
|
205
|
+
const provisioner = await provisionerTokenFactory.create({
|
|
206
|
+
scope: 'workspace',
|
|
207
|
+
workspaceId,
|
|
208
|
+
});
|
|
209
|
+
const token = await ephemeralRegistrationTokenFactory.create(
|
|
210
|
+
{workspaceId, provisionerId: provisioner.id},
|
|
211
|
+
{transient: {rawToken: ephemeralRawToken}},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const res = await app.inject({
|
|
215
|
+
method: 'POST',
|
|
216
|
+
url: '/runners/register',
|
|
217
|
+
headers: {authorization: `Bearer ${ephemeralRawToken}`},
|
|
218
|
+
payload: {labels: ['linux', 'shipfox-managed']},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const [session] = await db()
|
|
222
|
+
.select()
|
|
223
|
+
.from(runnerSessions)
|
|
224
|
+
.where(eq(runnerSessions.id, res.json().session_id));
|
|
225
|
+
|
|
226
|
+
expect(res.statusCode).toBe(200);
|
|
227
|
+
expect(session).toMatchObject({
|
|
228
|
+
id: res.json().session_id,
|
|
229
|
+
provisionerId: token.provisionerId,
|
|
230
|
+
labels: ['linux'],
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('preserves reserved labels for installation-scope ephemeral registration', async () => {
|
|
235
|
+
const ephemeralRawToken = generateOpaqueToken('ephemeralRegistrationToken');
|
|
236
|
+
const provisioner = await provisionerTokenFactory.create({scope: 'installation'});
|
|
237
|
+
const token = await ephemeralRegistrationTokenFactory.create(
|
|
238
|
+
{workspaceId, provisionerId: provisioner.id},
|
|
239
|
+
{transient: {rawToken: ephemeralRawToken}},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const res = await app.inject({
|
|
243
|
+
method: 'POST',
|
|
244
|
+
url: '/runners/register',
|
|
245
|
+
headers: {authorization: `Bearer ${ephemeralRawToken}`},
|
|
246
|
+
payload: {labels: ['linux', 'shipfox-managed']},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const [session] = await db()
|
|
250
|
+
.select()
|
|
251
|
+
.from(runnerSessions)
|
|
252
|
+
.where(eq(runnerSessions.id, res.json().session_id));
|
|
253
|
+
|
|
254
|
+
expect(res.statusCode).toBe(200);
|
|
255
|
+
expect(session).toMatchObject({
|
|
256
|
+
id: res.json().session_id,
|
|
257
|
+
provisionerId: token.provisionerId,
|
|
258
|
+
labels: ['linux', 'shipfox-managed'],
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
168
262
|
it('rejects malformed capability reports without creating a runner session', async () => {
|
|
169
263
|
const res = await app.inject({
|
|
170
264
|
method: 'POST',
|
|
@@ -418,6 +512,26 @@ describe('POST /runners/register', () => {
|
|
|
418
512
|
expect(res.statusCode).toBe(200);
|
|
419
513
|
});
|
|
420
514
|
|
|
515
|
+
it('maps reserved-label-only registration failures to a public error code', () => {
|
|
516
|
+
const route = createRegisterRoute(runnersTestAuthClient);
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
route.errorHandler?.(
|
|
520
|
+
new RunnerLabelsReservedError(['shipfox-managed']),
|
|
521
|
+
{} as FastifyRequest,
|
|
522
|
+
{} as FastifyReply,
|
|
523
|
+
);
|
|
524
|
+
throw new Error('Expected register route error handler to throw');
|
|
525
|
+
} catch (error) {
|
|
526
|
+
expect(error).toBeInstanceOf(ClientError);
|
|
527
|
+
expect(error).toMatchObject({
|
|
528
|
+
code: 'runner-labels-reserved',
|
|
529
|
+
details: {labels: ['shipfox-managed']},
|
|
530
|
+
status: 400,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
421
535
|
async function seedEphemeralRegisterRateLimit(
|
|
422
536
|
tokenId: string,
|
|
423
537
|
seedCount: number,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
EmptyRunnerLabelsError,
|
|
6
6
|
RegistrationTokenConsumedError,
|
|
7
7
|
RegistrationTokenExpiredError,
|
|
8
|
+
RunnerLabelsReservedError,
|
|
8
9
|
} from '#core/errors.js';
|
|
9
10
|
import {registerRunnerSession} from '#core/runner-sessions.js';
|
|
10
11
|
import {getRunnerContext} from '#presentation/auth/index.js';
|
|
@@ -23,6 +24,12 @@ export function createRegisterRoute(auth: AuthInterModuleClient) {
|
|
|
23
24
|
},
|
|
24
25
|
preHandler: createEphemeralRegisterRateLimitPreHandler(),
|
|
25
26
|
errorHandler: (error, request) => {
|
|
27
|
+
if (error instanceof RunnerLabelsReservedError) {
|
|
28
|
+
throw new ClientError(error.message, 'runner-labels-reserved', {
|
|
29
|
+
details: {labels: error.labels},
|
|
30
|
+
status: 400,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
26
33
|
if (error instanceof EmptyRunnerLabelsError) {
|
|
27
34
|
throw new ClientError(error.message, 'empty-runner-labels', {status: 400});
|
|
28
35
|
}
|
|
@@ -113,6 +113,39 @@ describe('POST /provisioners/runner-instances/report', () => {
|
|
|
113
113
|
expect(rows[0]?.reportedAt.toISOString()).toBe(reportedAt);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
it('strips reserved labels from workspace runner reports', async () => {
|
|
117
|
+
const res = await app.inject({
|
|
118
|
+
method: 'POST',
|
|
119
|
+
url: '/provisioners/runner-instances/report',
|
|
120
|
+
headers: {authorization: `Bearer ${VALID_PROVISIONER_TOKEN}`},
|
|
121
|
+
payload: {
|
|
122
|
+
events: [
|
|
123
|
+
{
|
|
124
|
+
provider_runner_id: 'provisioned-runner-reserved-label',
|
|
125
|
+
labels: ['linux', 'shipfox-managed'],
|
|
126
|
+
state: 'starting',
|
|
127
|
+
reported_at: new Date('2025-01-01T00:00:00.000Z').toISOString(),
|
|
128
|
+
provider_kind: 'docker',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const [row] = await db()
|
|
135
|
+
.select()
|
|
136
|
+
.from(providerRunners)
|
|
137
|
+
.where(
|
|
138
|
+
and(
|
|
139
|
+
eq(providerRunners.workspaceId, workspaceId),
|
|
140
|
+
eq(providerRunners.provisionerId, provisionerTokenId),
|
|
141
|
+
eq(providerRunners.providerRunnerId, 'provisioned-runner-reserved-label'),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(res.statusCode).toBe(200);
|
|
146
|
+
expect(row?.labels).toEqual(['linux']);
|
|
147
|
+
});
|
|
148
|
+
|
|
116
149
|
it('returns 400 when the batch exceeds the DTO limit', async () => {
|
|
117
150
|
const event = {
|
|
118
151
|
provider_runner_id: 'provisioned-runner-1',
|
|
@@ -20,6 +20,7 @@ export const reportRunnerInstancesRoute = defineRoute({
|
|
|
20
20
|
handler: async (request) => {
|
|
21
21
|
const context = requireProvisionerContext(request);
|
|
22
22
|
const result = await reportRunnerInstances({
|
|
23
|
+
scope: context.scope,
|
|
23
24
|
workspaceId: context.scope === 'workspace' ? context.workspaceId : null,
|
|
24
25
|
provisionerId: context.provisionerTokenId,
|
|
25
26
|
events: request.body.events.map((event) => ({
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
fakeLeaseTokenAuthMethod,
|
|
22
22
|
fakeRunnerSessionAuthMethod,
|
|
23
|
+
provisionerTokenFactory,
|
|
23
24
|
runnersTestAuthClient,
|
|
24
25
|
} from '#test/index.js';
|
|
25
26
|
import {createRunnerRoutes} from './index.js';
|
|
@@ -29,15 +30,27 @@ import {
|
|
|
29
30
|
} from './runner-enrollment.js';
|
|
30
31
|
|
|
31
32
|
const token = 'provisioner-test-token';
|
|
33
|
+
const workspaceToken = 'workspace-provisioner-test-token';
|
|
32
34
|
const fakeUserAuth: AuthMethod = {name: AUTH_USER, authenticate: () => Promise.resolve()};
|
|
33
35
|
|
|
34
36
|
describe('runner enrollment control plane', () => {
|
|
35
37
|
let app: FastifyInstance;
|
|
36
38
|
let provisionerId: string;
|
|
39
|
+
let workspaceProvisionerId: string;
|
|
40
|
+
let workspaceProvisionerWorkspaceId: string;
|
|
37
41
|
const provisionerAuth: AuthMethod = {
|
|
38
42
|
name: AUTH_PROVISIONER_TOKEN,
|
|
39
43
|
authenticate: (request: FastifyRequest) => {
|
|
40
|
-
|
|
44
|
+
const rawToken = extractBearerToken(request.headers.authorization);
|
|
45
|
+
if (rawToken === workspaceToken) {
|
|
46
|
+
setProvisionerContext(request, {
|
|
47
|
+
scope: 'workspace',
|
|
48
|
+
workspaceId: workspaceProvisionerWorkspaceId,
|
|
49
|
+
provisionerTokenId: workspaceProvisionerId,
|
|
50
|
+
});
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
}
|
|
53
|
+
if (rawToken !== token)
|
|
41
54
|
throw new ClientError('Invalid provisioner token', 'unauthorized', {status: 401});
|
|
42
55
|
setProvisionerContext(request, {scope: 'installation', provisionerTokenId: provisionerId});
|
|
43
56
|
return Promise.resolve();
|
|
@@ -64,8 +77,17 @@ describe('runner enrollment control plane', () => {
|
|
|
64
77
|
await closeApp();
|
|
65
78
|
});
|
|
66
79
|
|
|
67
|
-
beforeEach(() => {
|
|
68
|
-
|
|
80
|
+
beforeEach(async () => {
|
|
81
|
+
const provisioner = await provisionerTokenFactory.create({scope: 'installation'});
|
|
82
|
+
const workspaceProvisioner = await provisionerTokenFactory.create({
|
|
83
|
+
scope: 'workspace',
|
|
84
|
+
});
|
|
85
|
+
if (workspaceProvisioner.scope !== 'workspace') {
|
|
86
|
+
throw new Error('Expected a workspace provisioner token');
|
|
87
|
+
}
|
|
88
|
+
provisionerId = provisioner.id;
|
|
89
|
+
workspaceProvisionerId = workspaceProvisioner.id;
|
|
90
|
+
workspaceProvisionerWorkspaceId = workspaceProvisioner.workspaceId;
|
|
69
91
|
});
|
|
70
92
|
|
|
71
93
|
it('uses the requested assignment wait below the server cap and caps larger requests', () => {
|
|
@@ -112,7 +134,11 @@ describe('runner enrollment control plane', () => {
|
|
|
112
134
|
method: 'POST',
|
|
113
135
|
url: '/runner-control/enrollment',
|
|
114
136
|
headers: {authorization: `Bearer ${controlToken}`},
|
|
115
|
-
payload: {
|
|
137
|
+
payload: {
|
|
138
|
+
labels: ['Linux', 'linux', 'shipfox-managed'],
|
|
139
|
+
provider_kind: 'docker',
|
|
140
|
+
protocol_version: '1',
|
|
141
|
+
},
|
|
116
142
|
});
|
|
117
143
|
expect(enrolled.statusCode).toBe(200);
|
|
118
144
|
expect(enrolled.json()).toEqual({activation_token: null});
|
|
@@ -131,7 +157,7 @@ describe('runner enrollment control plane', () => {
|
|
|
131
157
|
.where(eq(providerRunners.id, runner.runner_instance_id));
|
|
132
158
|
expect(instance).toMatchObject({
|
|
133
159
|
provisionerId,
|
|
134
|
-
labels: ['linux'],
|
|
160
|
+
labels: ['linux', 'shipfox-managed'],
|
|
135
161
|
providerRunnerId: 'container-1',
|
|
136
162
|
state: 'running',
|
|
137
163
|
protocolVersion: '1',
|
|
@@ -150,6 +176,40 @@ describe('runner enrollment control plane', () => {
|
|
|
150
176
|
expect(jobs.statusCode).toBe(401);
|
|
151
177
|
});
|
|
152
178
|
|
|
179
|
+
it('strips reserved labels when a workspace provisioner enrolls a runner', async () => {
|
|
180
|
+
const created = await app.inject({
|
|
181
|
+
method: 'POST',
|
|
182
|
+
url: '/provisioners/runner-instances/batch',
|
|
183
|
+
headers: {authorization: `Bearer ${workspaceToken}`},
|
|
184
|
+
payload: {runner_instances: [{}]},
|
|
185
|
+
});
|
|
186
|
+
const runner = created.json().runner_instances[0];
|
|
187
|
+
const exchanged = await app.inject({
|
|
188
|
+
method: 'POST',
|
|
189
|
+
url: '/runner-enrollment/exchange',
|
|
190
|
+
payload: {bootstrap_token: runner.bootstrap_token},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const enrolled = await app.inject({
|
|
194
|
+
method: 'POST',
|
|
195
|
+
url: '/runner-control/enrollment',
|
|
196
|
+
headers: {authorization: `Bearer ${exchanged.json().control_session_token}`},
|
|
197
|
+
payload: {
|
|
198
|
+
labels: ['linux', 'shipfox-managed'],
|
|
199
|
+
provider_kind: 'docker',
|
|
200
|
+
protocol_version: '1',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const [instance] = await db()
|
|
205
|
+
.select()
|
|
206
|
+
.from(providerRunners)
|
|
207
|
+
.where(eq(providerRunners.id, runner.runner_instance_id));
|
|
208
|
+
|
|
209
|
+
expect(enrolled.statusCode).toBe(200);
|
|
210
|
+
expect(instance?.labels).toEqual(['linux']);
|
|
211
|
+
});
|
|
212
|
+
|
|
153
213
|
it('promotes an intended reservation during enrollment and returns its activation token', async () => {
|
|
154
214
|
const workspaceId = crypto.randomUUID();
|
|
155
215
|
const [reservation] = await db()
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deleteExpiredEphemeralRegistrationTokens,
|
|
3
|
-
deleteExpiredRunnerReservations,
|
|
4
|
-
deleteExpiredRunnerSessions,
|
|
5
|
-
detectAndExpireStuckJobs,
|
|
6
|
-
reapStaleRunnerInstances,
|
|
7
|
-
} from '#core/maintenance.js';
|
|
8
|
-
import {
|
|
9
|
-
deleteExpiredEphemeralRegistrationTokensActivity,
|
|
10
|
-
deleteExpiredReservationsActivity,
|
|
11
|
-
deleteExpiredRunnerSessionsActivity,
|
|
12
|
-
detectAndExpireStuckJobsActivity,
|
|
13
|
-
reapStaleRunnerInstancesActivity,
|
|
14
|
-
} from './maintenance-activities.js';
|
|
15
|
-
|
|
16
1
|
vi.mock('#core/maintenance.js', () => ({
|
|
17
2
|
deleteExpiredEphemeralRegistrationTokens: vi.fn(),
|
|
18
3
|
deleteExpiredRunnerReservations: vi.fn(),
|
|
@@ -21,46 +6,52 @@ vi.mock('#core/maintenance.js', () => ({
|
|
|
21
6
|
reapStaleRunnerInstances: vi.fn(),
|
|
22
7
|
}));
|
|
23
8
|
|
|
24
|
-
|
|
9
|
+
let maintenance: typeof import('#core/maintenance.js');
|
|
10
|
+
let activities: typeof import('./maintenance-activities.js');
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
maintenance = await import('#core/maintenance.js');
|
|
15
|
+
activities = await import('./maintenance-activities.js');
|
|
25
16
|
vi.clearAllMocks();
|
|
26
17
|
});
|
|
27
18
|
|
|
28
19
|
describe('detectAndExpireStuckJobsActivity', () => {
|
|
29
20
|
it('delegates to core maintenance', async () => {
|
|
30
|
-
vi.mocked(detectAndExpireStuckJobs).mockResolvedValueOnce({expired: 2});
|
|
21
|
+
vi.mocked(maintenance.detectAndExpireStuckJobs).mockResolvedValueOnce({expired: 2});
|
|
31
22
|
|
|
32
|
-
const result = await detectAndExpireStuckJobsActivity({thresholdSeconds: 180});
|
|
23
|
+
const result = await activities.detectAndExpireStuckJobsActivity({thresholdSeconds: 180});
|
|
33
24
|
|
|
34
25
|
expect(result).toEqual({expired: 2});
|
|
35
|
-
expect(detectAndExpireStuckJobs).toHaveBeenCalledWith({thresholdSeconds: 180});
|
|
26
|
+
expect(maintenance.detectAndExpireStuckJobs).toHaveBeenCalledWith({thresholdSeconds: 180});
|
|
36
27
|
});
|
|
37
28
|
});
|
|
38
29
|
|
|
39
30
|
describe('deleteExpiredReservationsActivity', () => {
|
|
40
31
|
it('delegates to core maintenance', async () => {
|
|
41
|
-
vi.mocked(deleteExpiredRunnerReservations).mockResolvedValueOnce({deleted: 3});
|
|
32
|
+
vi.mocked(maintenance.deleteExpiredRunnerReservations).mockResolvedValueOnce({deleted: 3});
|
|
42
33
|
|
|
43
|
-
const result = await deleteExpiredReservationsActivity({limit: 50});
|
|
34
|
+
const result = await activities.deleteExpiredReservationsActivity({limit: 50});
|
|
44
35
|
|
|
45
36
|
expect(result).toEqual({deleted: 3});
|
|
46
|
-
expect(deleteExpiredRunnerReservations).toHaveBeenCalledWith({limit: 50});
|
|
37
|
+
expect(maintenance.deleteExpiredRunnerReservations).toHaveBeenCalledWith({limit: 50});
|
|
47
38
|
});
|
|
48
39
|
});
|
|
49
40
|
|
|
50
41
|
describe('reapStaleRunnerInstancesActivity', () => {
|
|
51
42
|
it('delegates to core maintenance', async () => {
|
|
52
|
-
vi.mocked(reapStaleRunnerInstances).mockResolvedValueOnce({
|
|
43
|
+
vi.mocked(maintenance.reapStaleRunnerInstances).mockResolvedValueOnce({
|
|
53
44
|
reaped: 4,
|
|
54
45
|
reservationsReleased: 2,
|
|
55
46
|
});
|
|
56
47
|
|
|
57
|
-
const result = await reapStaleRunnerInstancesActivity({
|
|
48
|
+
const result = await activities.reapStaleRunnerInstancesActivity({
|
|
58
49
|
thresholdSeconds: 300,
|
|
59
50
|
limit: 100,
|
|
60
51
|
});
|
|
61
52
|
|
|
62
53
|
expect(result).toEqual({reaped: 4, reservationsReleased: 2});
|
|
63
|
-
expect(reapStaleRunnerInstances).toHaveBeenCalledWith({
|
|
54
|
+
expect(maintenance.reapStaleRunnerInstances).toHaveBeenCalledWith({
|
|
64
55
|
thresholdSeconds: 300,
|
|
65
56
|
limit: 100,
|
|
66
57
|
});
|
|
@@ -69,16 +60,16 @@ describe('reapStaleRunnerInstancesActivity', () => {
|
|
|
69
60
|
|
|
70
61
|
describe('deleteExpiredRunnerSessionsActivity', () => {
|
|
71
62
|
it('delegates to core maintenance', async () => {
|
|
72
|
-
vi.mocked(deleteExpiredRunnerSessions).mockResolvedValueOnce({deleted: 4});
|
|
63
|
+
vi.mocked(maintenance.deleteExpiredRunnerSessions).mockResolvedValueOnce({deleted: 4});
|
|
73
64
|
|
|
74
|
-
const result = await deleteExpiredRunnerSessionsActivity({
|
|
65
|
+
const result = await activities.deleteExpiredRunnerSessionsActivity({
|
|
75
66
|
manualRetentionDays: 30,
|
|
76
67
|
ephemeralRetentionDays: 7,
|
|
77
68
|
limit: 25,
|
|
78
69
|
});
|
|
79
70
|
|
|
80
71
|
expect(result).toEqual({deleted: 4});
|
|
81
|
-
expect(deleteExpiredRunnerSessions).toHaveBeenCalledWith({
|
|
72
|
+
expect(maintenance.deleteExpiredRunnerSessions).toHaveBeenCalledWith({
|
|
82
73
|
manualRetentionDays: 30,
|
|
83
74
|
ephemeralRetentionDays: 7,
|
|
84
75
|
limit: 25,
|
|
@@ -88,15 +79,17 @@ describe('deleteExpiredRunnerSessionsActivity', () => {
|
|
|
88
79
|
|
|
89
80
|
describe('deleteExpiredEphemeralRegistrationTokensActivity', () => {
|
|
90
81
|
it('delegates to core maintenance', async () => {
|
|
91
|
-
vi.mocked(deleteExpiredEphemeralRegistrationTokens).mockResolvedValueOnce({
|
|
82
|
+
vi.mocked(maintenance.deleteExpiredEphemeralRegistrationTokens).mockResolvedValueOnce({
|
|
83
|
+
deleted: 6,
|
|
84
|
+
});
|
|
92
85
|
|
|
93
|
-
const result = await deleteExpiredEphemeralRegistrationTokensActivity({
|
|
86
|
+
const result = await activities.deleteExpiredEphemeralRegistrationTokensActivity({
|
|
94
87
|
retentionDays: 7,
|
|
95
88
|
limit: 25,
|
|
96
89
|
});
|
|
97
90
|
|
|
98
91
|
expect(result).toEqual({deleted: 6});
|
|
99
|
-
expect(deleteExpiredEphemeralRegistrationTokens).toHaveBeenCalledWith({
|
|
92
|
+
expect(maintenance.deleteExpiredEphemeralRegistrationTokens).toHaveBeenCalledWith({
|
|
100
93
|
retentionDays: 7,
|
|
101
94
|
limit: 25,
|
|
102
95
|
});
|
package/test/env.ts
CHANGED
|
@@ -4,6 +4,7 @@ process.env.POSTGRES_USERNAME ??= 'shipfox';
|
|
|
4
4
|
process.env.POSTGRES_PASSWORD ??= 'password';
|
|
5
5
|
process.env.POSTGRES_DATABASE = 'api_test';
|
|
6
6
|
process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
|
|
7
|
+
process.env.RUNNER_RESERVED_LABELS ??= 'shipfox-managed';
|
|
7
8
|
process.env.WORKSPACE_JWT_SECRET = 'test-secret';
|
|
8
9
|
process.env.AUTH_ROOT_KEY = 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
|
|
9
10
|
process.env.TZ = 'UTC';
|