@shipfox/api-runners 6.0.0 → 7.0.2

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.
Files changed (129) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +26 -0
  3. package/dist/config.js +1 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/entities/runner-instance.d.ts +1 -0
  6. package/dist/core/entities/runner-instance.d.ts.map +1 -1
  7. package/dist/core/entities/runner-instance.js.map +1 -1
  8. package/dist/core/errors.d.ts +6 -6
  9. package/dist/core/errors.d.ts.map +1 -1
  10. package/dist/core/errors.js +8 -8
  11. package/dist/core/errors.js.map +1 -1
  12. package/dist/core/index.d.ts +2 -4
  13. package/dist/core/index.d.ts.map +1 -1
  14. package/dist/core/index.js +2 -3
  15. package/dist/core/index.js.map +1 -1
  16. package/dist/core/runner-assignments.d.ts +6 -0
  17. package/dist/core/runner-assignments.d.ts.map +1 -0
  18. package/dist/core/runner-assignments.js +6 -0
  19. package/dist/core/runner-assignments.js.map +1 -0
  20. package/dist/core/runner-instances.d.ts +2 -9
  21. package/dist/core/runner-instances.d.ts.map +1 -1
  22. package/dist/core/runner-instances.js +3 -6
  23. package/dist/core/runner-instances.js.map +1 -1
  24. package/dist/db/db.d.ts +34 -218
  25. package/dist/db/db.d.ts.map +1 -1
  26. package/dist/db/db.js +0 -2
  27. package/dist/db/db.js.map +1 -1
  28. package/dist/db/index.d.ts +2 -2
  29. package/dist/db/index.d.ts.map +1 -1
  30. package/dist/db/index.js +2 -2
  31. package/dist/db/index.js.map +1 -1
  32. package/dist/db/runner-assignments.d.ts +7 -0
  33. package/dist/db/runner-assignments.d.ts.map +1 -0
  34. package/dist/db/runner-assignments.js +55 -0
  35. package/dist/db/runner-assignments.js.map +1 -0
  36. package/dist/db/runner-instances.d.ts +2 -9
  37. package/dist/db/runner-instances.d.ts.map +1 -1
  38. package/dist/db/runner-instances.js +2 -16
  39. package/dist/db/runner-instances.js.map +1 -1
  40. package/dist/db/schema/runner-instances.d.ts +17 -0
  41. package/dist/db/schema/runner-instances.d.ts.map +1 -1
  42. package/dist/db/schema/runner-instances.js +5 -1
  43. package/dist/db/schema/runner-instances.js.map +1 -1
  44. package/dist/index.d.ts +0 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +0 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/presentation/dto/index.d.ts +0 -1
  49. package/dist/presentation/dto/index.d.ts.map +1 -1
  50. package/dist/presentation/dto/index.js +0 -1
  51. package/dist/presentation/dto/index.js.map +1 -1
  52. package/dist/presentation/routes/assign-runner-instances.d.ts +2 -0
  53. package/dist/presentation/routes/assign-runner-instances.d.ts.map +1 -0
  54. package/dist/presentation/routes/assign-runner-instances.js +40 -0
  55. package/dist/presentation/routes/assign-runner-instances.js.map +1 -0
  56. package/dist/presentation/routes/index.d.ts.map +1 -1
  57. package/dist/presentation/routes/index.js +4 -8
  58. package/dist/presentation/routes/index.js.map +1 -1
  59. package/dist/presentation/routes/runner-enrollment.d.ts +1 -0
  60. package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
  61. package/dist/presentation/routes/runner-enrollment.js +27 -0
  62. package/dist/presentation/routes/runner-enrollment.js.map +1 -1
  63. package/dist/tsconfig.test.tsbuildinfo +1 -1
  64. package/drizzle/0000_initial.sql +1 -12
  65. package/drizzle/meta/0000_snapshot.json +6 -97
  66. package/package.json +8 -29
  67. package/src/config.ts +1 -1
  68. package/src/core/entities/runner-instance.ts +1 -0
  69. package/src/core/errors.ts +8 -8
  70. package/src/core/index.ts +2 -9
  71. package/src/core/runner-assignments.ts +9 -0
  72. package/src/core/runner-instances.ts +4 -13
  73. package/src/db/db.ts +0 -2
  74. package/src/db/index.ts +2 -3
  75. package/src/db/runner-assignments.test.ts +163 -0
  76. package/src/db/runner-assignments.ts +115 -0
  77. package/src/db/runner-instances.test.ts +36 -23
  78. package/src/db/runner-instances.ts +3 -23
  79. package/src/db/schema/runner-instances.ts +3 -1
  80. package/src/index.ts +0 -9
  81. package/src/presentation/dto/index.ts +0 -4
  82. package/src/presentation/routes/assign-runner-instances.ts +49 -0
  83. package/src/presentation/routes/index.ts +4 -7
  84. package/src/presentation/routes/runner-enrollment.test.ts +60 -0
  85. package/src/presentation/routes/runner-enrollment.ts +22 -0
  86. package/test/globalSetup.ts +0 -1
  87. package/tsconfig.build.tsbuildinfo +1 -1
  88. package/dist/core/capacity-assignments.d.ts +0 -7
  89. package/dist/core/capacity-assignments.d.ts.map +0 -1
  90. package/dist/core/capacity-assignments.js +0 -6
  91. package/dist/core/capacity-assignments.js.map +0 -1
  92. package/dist/core/entities/capacity-assignment.d.ts +0 -9
  93. package/dist/core/entities/capacity-assignment.d.ts.map +0 -1
  94. package/dist/core/entities/capacity-assignment.js +0 -3
  95. package/dist/core/entities/capacity-assignment.js.map +0 -1
  96. package/dist/db/capacity-assignments.d.ts +0 -7
  97. package/dist/db/capacity-assignments.d.ts.map +0 -1
  98. package/dist/db/capacity-assignments.js +0 -72
  99. package/dist/db/capacity-assignments.js.map +0 -1
  100. package/dist/db/schema/capacity-assignments.d.ts +0 -113
  101. package/dist/db/schema/capacity-assignments.d.ts.map +0 -1
  102. package/dist/db/schema/capacity-assignments.js +0 -29
  103. package/dist/db/schema/capacity-assignments.js.map +0 -1
  104. package/dist/presentation/dto/mint-registration-tokens.d.ts +0 -7
  105. package/dist/presentation/dto/mint-registration-tokens.d.ts.map +0 -1
  106. package/dist/presentation/dto/mint-registration-tokens.js +0 -19
  107. package/dist/presentation/dto/mint-registration-tokens.js.map +0 -1
  108. package/dist/presentation/routes/assign-capacity.d.ts +0 -2
  109. package/dist/presentation/routes/assign-capacity.d.ts.map +0 -1
  110. package/dist/presentation/routes/assign-capacity.js +0 -44
  111. package/dist/presentation/routes/assign-capacity.js.map +0 -1
  112. package/dist/presentation/routes/mint-registration-tokens.d.ts +0 -2
  113. package/dist/presentation/routes/mint-registration-tokens.d.ts.map +0 -1
  114. package/dist/presentation/routes/mint-registration-tokens.js +0 -74
  115. package/dist/presentation/routes/mint-registration-tokens.js.map +0 -1
  116. package/dist/presentation/routes/planned-capacity.d.ts +0 -3
  117. package/dist/presentation/routes/planned-capacity.d.ts.map +0 -1
  118. package/dist/presentation/routes/planned-capacity.js +0 -54
  119. package/dist/presentation/routes/planned-capacity.js.map +0 -1
  120. package/src/core/capacity-assignments.ts +0 -10
  121. package/src/core/entities/capacity-assignment.ts +0 -8
  122. package/src/db/capacity-assignments.test.ts +0 -159
  123. package/src/db/capacity-assignments.ts +0 -116
  124. package/src/db/schema/capacity-assignments.ts +0 -34
  125. package/src/presentation/dto/mint-registration-tokens.ts +0 -28
  126. package/src/presentation/routes/assign-capacity.ts +0 -48
  127. package/src/presentation/routes/mint-registration-tokens.test.ts +0 -453
  128. package/src/presentation/routes/mint-registration-tokens.ts +0 -80
  129. package/src/presentation/routes/planned-capacity.ts +0 -49
@@ -3,8 +3,7 @@ import {and, desc, eq, inArray, or, sql} from 'drizzle-orm';
3
3
  import {db} from '#db/db.js';
4
4
  import {createRunnerSessionConsumingEphemeralToken} from '#db/ephemeral-registration-tokens.js';
5
5
  import {
6
- attachProviderRunnerId,
7
- createPlannedProvisionedCapacity,
6
+ attachRunnerInstanceProviderId,
8
7
  listActiveRunnerInstanceCountsByTemplateTx,
9
8
  listActiveRunnerInstances,
10
9
  listProvisionerTerminateIntentRowsTx,
@@ -1014,26 +1013,33 @@ describe('reapStaleRunnerInstances', () => {
1014
1013
  expect(reservation?.count).toBe(1);
1015
1014
  });
1016
1015
 
1017
- it('fails stale planned installation capacity before provider attachment', async () => {
1016
+ it('fails a stale installation runner instance before provider attachment', async () => {
1018
1017
  const installationProvisioner = await provisionerTokenFactory.create({
1019
1018
  scope: 'installation',
1020
1019
  workspaceId: null,
1021
1020
  });
1022
- const capacity = await createPlannedProvisionedCapacity({
1023
- provisionerId: installationProvisioner.id,
1024
- providerKind: 'docker',
1025
- templateKey: null,
1026
- });
1027
1021
  await db()
1028
- .update(providerRunners)
1029
- .set({reportedAt: staleAt(), updatedAt: staleAt()})
1030
- .where(eq(providerRunners.id, capacity.capacityId));
1022
+ .update(provisionerTokens)
1023
+ .set({lastSeenAt: null})
1024
+ .where(eq(provisionerTokens.id, installationProvisioner.id));
1025
+ const [instance] = await db()
1026
+ .insert(providerRunners)
1027
+ .values({
1028
+ provisionerId: installationProvisioner.id,
1029
+ providerKind: 'docker',
1030
+ labels: [],
1031
+ state: 'starting',
1032
+ reportedAt: staleAt(),
1033
+ updatedAt: staleAt(),
1034
+ })
1035
+ .returning({id: providerRunners.id});
1036
+ if (!instance) throw new Error('Runner instance insert returned no row');
1031
1037
 
1032
1038
  const result = await reapStaleRunnerInstances({thresholdSeconds: 60, limit: 100});
1033
1039
  const [row] = await db()
1034
1040
  .select()
1035
1041
  .from(providerRunners)
1036
- .where(eq(providerRunners.id, capacity.capacityId));
1042
+ .where(eq(providerRunners.id, instance.id));
1037
1043
 
1038
1044
  expect(result.reaped).toBe(1);
1039
1045
  expect(row).toMatchObject({workspaceId: null, providerRunnerId: null, state: 'failed'});
@@ -1815,29 +1821,36 @@ describe('reconcileRunnerInstances', () => {
1815
1821
  }
1816
1822
  });
1817
1823
 
1818
- describe('planned provisioned capacity', () => {
1824
+ describe('runner instance provider attachment', () => {
1819
1825
  it('belongs to its provisioner until it receives one provider runner identity', async () => {
1820
1826
  const provisionerId = crypto.randomUUID();
1821
- const capacity = await createPlannedProvisionedCapacity({
1822
- provisionerId,
1823
- providerKind: 'docker',
1824
- templateKey: 'linux',
1825
- });
1827
+ const [instance] = await db()
1828
+ .insert(providerRunners)
1829
+ .values({
1830
+ provisionerId,
1831
+ providerKind: 'docker',
1832
+ templateKey: 'linux',
1833
+ labels: [],
1834
+ state: 'starting',
1835
+ reportedAt: new Date(),
1836
+ })
1837
+ .returning({id: providerRunners.id});
1838
+ if (!instance) throw new Error('Runner instance insert returned no row');
1826
1839
 
1827
- const attached = await attachProviderRunnerId({
1828
- capacityId: capacity.capacityId,
1840
+ const attached = await attachRunnerInstanceProviderId({
1841
+ runnerInstanceId: instance.id,
1829
1842
  provisionerId,
1830
1843
  providerRunnerId: 'container-1',
1831
1844
  });
1832
- const rebound = await attachProviderRunnerId({
1833
- capacityId: capacity.capacityId,
1845
+ const rebound = await attachRunnerInstanceProviderId({
1846
+ runnerInstanceId: instance.id,
1834
1847
  provisionerId,
1835
1848
  providerRunnerId: 'container-2',
1836
1849
  });
1837
1850
  const [row] = await db()
1838
1851
  .select()
1839
1852
  .from(providerRunners)
1840
- .where(eq(providerRunners.id, capacity.capacityId));
1853
+ .where(eq(providerRunners.id, instance.id));
1841
1854
 
1842
1855
  expect(attached).toBe(true);
1843
1856
  expect(rebound).toBe(false);
@@ -97,28 +97,8 @@ export interface ReapStaleRunnerInstancesResult {
97
97
  reservationsReleased: number;
98
98
  }
99
99
 
100
- export async function createPlannedProvisionedCapacity(params: {
101
- provisionerId: string;
102
- providerKind: string | null;
103
- templateKey: string | null;
104
- }): Promise<{capacityId: string}> {
105
- const [row] = await db()
106
- .insert(providerRunners)
107
- .values({
108
- provisionerId: params.provisionerId,
109
- providerKind: params.providerKind,
110
- templateKey: params.templateKey,
111
- state: 'starting',
112
- labels: [],
113
- reportedAt: new Date(),
114
- })
115
- .returning({capacityId: providerRunners.id});
116
- if (!row) throw new Error('Planned capacity insert returned no row');
117
- return row;
118
- }
119
-
120
- export async function attachProviderRunnerId(params: {
121
- capacityId: string;
100
+ export async function attachRunnerInstanceProviderId(params: {
101
+ runnerInstanceId: string;
122
102
  provisionerId: string;
123
103
  providerRunnerId: string;
124
104
  }): Promise<boolean> {
@@ -127,7 +107,7 @@ export async function attachProviderRunnerId(params: {
127
107
  .set({providerRunnerId: params.providerRunnerId, updatedAt: sql`now()`})
128
108
  .where(
129
109
  and(
130
- eq(providerRunners.id, params.capacityId),
110
+ eq(providerRunners.id, params.runnerInstanceId),
131
111
  eq(providerRunners.provisionerId, params.provisionerId),
132
112
  isNull(providerRunners.providerRunnerId),
133
113
  notInArray(providerRunners.state, [...terminalStates]),
@@ -24,6 +24,7 @@ export const providerRunners = pgTable(
24
24
  provisionerId: uuid('provisioner_id').notNull(),
25
25
  providerRunnerId: text('provider_runner_id'),
26
26
  reservationId: uuid('reservation_id'),
27
+ assignedAt: timestamp('assigned_at', {withTimezone: true}),
27
28
  templateKey: text('template_key'),
28
29
  labels: text('labels').array().notNull().default([]),
29
30
  state: providerRunnerStateEnum('state').notNull(),
@@ -62,13 +63,14 @@ export type RunnerInstanceDb = typeof providerRunners.$inferSelect;
62
63
  export type RunnerInstanceInsertDb = typeof providerRunners.$inferInsert;
63
64
 
64
65
  export function toRunnerInstance(row: RunnerInstanceDb): RunnerInstance {
65
- if (!row.providerRunnerId) throw new Error('Planned capacity has no provider runner identity');
66
+ if (!row.providerRunnerId) throw new Error('Runner instance has no provider runner identity');
66
67
  return {
67
68
  id: row.id,
68
69
  workspaceId: row.workspaceId,
69
70
  provisionerId: row.provisionerId,
70
71
  providerRunnerId: row.providerRunnerId,
71
72
  reservationId: row.reservationId,
73
+ assignedAt: row.assignedAt,
72
74
  templateKey: row.templateKey,
73
75
  labels: row.labels,
74
76
  state: row.state,
package/src/index.ts CHANGED
@@ -21,15 +21,6 @@ import {createRunnersInterModulePresentation} from '#presentation/inter-module.j
21
21
  import {createRunnersMaintenanceActivities} from '#temporal/activities/index.js';
22
22
  import {RUNNERS_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';
23
23
 
24
- export {
25
- type MintEphemeralRegistrationTokenParams,
26
- type MintEphemeralRegistrationTokenResult,
27
- type MintEphemeralRegistrationTokensBatchParams,
28
- type MintEphemeralRegistrationTokensBatchResult,
29
- type MintEphemeralRegistrationTokensBatchRunnerInstance,
30
- mintEphemeralRegistrationToken,
31
- mintEphemeralRegistrationTokensBatch,
32
- } from '#core/ephemeral-registration-tokens.js';
33
24
  export {
34
25
  type EffectiveRunnerToolCapabilitiesResult,
35
26
  getEffectiveRunnerToolCapabilities,
@@ -1,9 +1,5 @@
1
1
  export {toActiveProvisionerDto} from './active-provisioner.js';
2
2
  export {toManualRegistrationTokenDto} from './manual-registration-token.js';
3
- export {
4
- toMintRegistrationTokensResponseDto,
5
- toMintRegistrationTokensRunnerInstances,
6
- } from './mint-registration-tokens.js';
7
3
  export {toPollDemandResponseDto} from './poll-demand.js';
8
4
  export {toProvisionerTokenDto} from './provisioner-token.js';
9
5
  export {
@@ -0,0 +1,49 @@
1
+ import {requireProvisionerContext} from '@shipfox/api-auth-context';
2
+ import {
3
+ assignRunnerInstancesBodySchema,
4
+ assignRunnerInstancesResponseSchema,
5
+ } from '@shipfox/api-runners-dto';
6
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
7
+ import {
8
+ ReservationExpiredError,
9
+ ReservationNotFoundError,
10
+ RunnerInstanceAlreadyAssignedError,
11
+ RunnerInstanceNotAssignableError,
12
+ } from '#core/errors.js';
13
+ import {assignRunnerInstances} from '#core/runner-assignments.js';
14
+
15
+ export const assignRunnerInstancesRoute = defineRoute({
16
+ method: 'POST',
17
+ path: '/runner-instances/assignments',
18
+ description: 'Assign enrolled runner instances to an owned demand reservation',
19
+ schema: {
20
+ body: assignRunnerInstancesBodySchema,
21
+ response: {200: assignRunnerInstancesResponseSchema},
22
+ },
23
+ errorHandler: (error) => {
24
+ if (error instanceof ReservationNotFoundError)
25
+ throw new ClientError('Reservation not found', 'reservation-not-found', {status: 404});
26
+ if (error instanceof ReservationExpiredError)
27
+ throw new ClientError('Reservation has expired', 'reservation-expired', {status: 409});
28
+ if (
29
+ error instanceof RunnerInstanceAlreadyAssignedError ||
30
+ error instanceof RunnerInstanceNotAssignableError
31
+ )
32
+ throw new ClientError(
33
+ 'Runner instance cannot be assigned',
34
+ 'runner-instance-not-assignable',
35
+ {status: 409},
36
+ );
37
+ throw error;
38
+ },
39
+ handler: async (request) => {
40
+ const {provisionerTokenId} = requireProvisionerContext(request);
41
+ return {
42
+ runner_instance_ids: await assignRunnerInstances({
43
+ provisionerId: provisionerTokenId,
44
+ reservationId: request.body.reservation_id,
45
+ runnerInstanceIds: request.body.runner_instance_ids,
46
+ }),
47
+ };
48
+ },
49
+ });
@@ -8,7 +8,7 @@ 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 {assignCapacityRoute} from './assign-capacity.js';
11
+ import {assignRunnerInstancesRoute} from './assign-runner-instances.js';
12
12
  import {createManualRegistrationTokenRoute} from './create-manual-registration-token.js';
13
13
  import {createProvisionerTokenRoute} from './create-provisioner-token.js';
14
14
  import {getProvisionerMeRoute} from './get-provisioner-me.js';
@@ -17,8 +17,6 @@ import {listActiveProvisionersRoute} from './list-active-provisioners.js';
17
17
  import {listActiveRunnersRoute} from './list-active-runners.js';
18
18
  import {listManualRegistrationTokensRoute} from './list-manual-registration-tokens.js';
19
19
  import {listProvisionerTokensRoute} from './list-provisioner-tokens.js';
20
- import {mintRegistrationTokensRoute} from './mint-registration-tokens.js';
21
- import {attachProviderRunnerRoute, createPlannedCapacityRoute} from './planned-capacity.js';
22
20
  import {createPollDemandRoute, pollDemandRoute} from './poll-demand.js';
23
21
  import {reconcileRunnerInstancesRoute} from './reconcile-runner-instances.js';
24
22
  import {createRegisterRoute} from './register.js';
@@ -28,6 +26,7 @@ import {revokeManualRegistrationTokenRoute} from './revoke-manual-registration-t
28
26
  import {revokeProvisionerTokenRoute} from './revoke-provisioner-token.js';
29
27
  import {
30
28
  attachRunnerControlProviderIdRoute,
29
+ attachRunnerInstanceProviderIdRoute,
31
30
  createRunnerInstancesRoute,
32
31
  enrollRunnerRoute,
33
32
  exchangeRunnerBootstrapRoute,
@@ -72,10 +71,8 @@ function createRunnerOnlyRoutes(auth: AuthInterModuleClient): RouteGroup[] {
72
71
  routes: [
73
72
  pollDemandRoute,
74
73
  createRunnerInstancesRoute,
75
- createPlannedCapacityRoute,
76
- attachProviderRunnerRoute,
77
- assignCapacityRoute,
78
- mintRegistrationTokensRoute,
74
+ attachRunnerInstanceProviderIdRoute,
75
+ assignRunnerInstancesRoute,
79
76
  reportRunnerInstancesRoute,
80
77
  reconcileRunnerInstancesRoute,
81
78
  ],
@@ -10,6 +10,7 @@ import {
10
10
  import {eq} from 'drizzle-orm';
11
11
  import type {FastifyInstance, FastifyRequest} from 'fastify';
12
12
  import {db} from '#db/db.js';
13
+ import {reservations} from '#db/schema/reservations.js';
13
14
  import {runnerActivationTokens} from '#db/schema/runner-activation-tokens.js';
14
15
  import {runnerBootstrapTokens, runnerControlSessions} from '#db/schema/runner-control-sessions.js';
15
16
  import {providerRunners} from '#db/schema/runner-instances.js';
@@ -162,6 +163,65 @@ describe('runner enrollment control plane', () => {
162
163
  expect(enrolled.json()).toMatchObject({code: 'runner-control-session-invalid'});
163
164
  });
164
165
 
166
+ it('assigns an enrolled runner instance only through the owned reservation', async () => {
167
+ const workspaceId = crypto.randomUUID();
168
+ const created = await app.inject({
169
+ method: 'POST',
170
+ url: '/provisioners/runner-instances/batch',
171
+ headers: {authorization: `Bearer ${token}`},
172
+ payload: {runner_instances: [{template_key: 'linux'}]},
173
+ });
174
+ const runner = created.json().runner_instances[0];
175
+ const exchanged = await app.inject({
176
+ method: 'POST',
177
+ url: '/runner-enrollment/exchange',
178
+ payload: {bootstrap_token: runner.bootstrap_token},
179
+ });
180
+ const controlToken = exchanged.json().control_session_token;
181
+ await app.inject({
182
+ method: 'POST',
183
+ url: '/runner-control/enrollment',
184
+ headers: {authorization: `Bearer ${controlToken}`},
185
+ payload: {labels: ['linux'], provider_kind: 'docker', protocol_version: '1'},
186
+ });
187
+ await app.inject({
188
+ method: 'POST',
189
+ url: '/runner-control/provider-runner',
190
+ headers: {authorization: `Bearer ${controlToken}`},
191
+ payload: {provider_runner_id: 'container-assignment-test'},
192
+ });
193
+ const [reservation] = await db()
194
+ .insert(reservations)
195
+ .values({
196
+ workspaceId,
197
+ provisionerId,
198
+ requiredLabels: ['linux'],
199
+ count: 1,
200
+ expiresAt: new Date(Date.now() + 60_000),
201
+ })
202
+ .returning();
203
+ if (!reservation) throw new Error('Reservation insert returned no row');
204
+
205
+ const assigned = await app.inject({
206
+ method: 'POST',
207
+ url: '/provisioners/runner-instances/assignments',
208
+ headers: {authorization: `Bearer ${token}`},
209
+ payload: {reservation_id: reservation.id, runner_instance_ids: [runner.runner_instance_id]},
210
+ });
211
+
212
+ expect(assigned.statusCode).toBe(200);
213
+ expect(assigned.json()).toEqual({runner_instance_ids: [runner.runner_instance_id]});
214
+ const [instance] = await db()
215
+ .select()
216
+ .from(providerRunners)
217
+ .where(eq(providerRunners.id, runner.runner_instance_id));
218
+ expect(instance).toMatchObject({
219
+ workspaceId,
220
+ reservationId: reservation.id,
221
+ assignedAt: expect.any(Date),
222
+ });
223
+ });
224
+
165
225
  it('returns an activation token only for its assigned runner and closes control access after registration', async () => {
166
226
  const created = await app.inject({
167
227
  method: 'POST',
@@ -25,6 +25,7 @@ import {
25
25
  RunnerControlSessionInvalidError,
26
26
  touchRunnerControlSession,
27
27
  } from '#core/runner-control-sessions.js';
28
+ import {attachRunnerInstanceProviderId} from '#core/runner-instances.js';
28
29
  import {runnerBootstrapExchangeCount, runnerControlHeartbeatCount} from '#metrics/instance.js';
29
30
  import {authenticateRunnerControlSession} from '#presentation/auth/index.js';
30
31
 
@@ -56,6 +57,27 @@ export const createRunnerInstancesRoute = defineRoute({
56
57
  },
57
58
  });
58
59
 
60
+ export const attachRunnerInstanceProviderIdRoute = defineRoute({
61
+ method: 'POST',
62
+ path: '/runner-instances/:runnerInstanceId/provider-runner',
63
+ description: 'Attach the provider identity after the provisioner launches a runner instance',
64
+ schema: {
65
+ params: z.object({runnerInstanceId: z.string().uuid()}),
66
+ body: attachRunnerControlProviderIdBodySchema,
67
+ response: {200: z.object({attached: z.boolean()})},
68
+ },
69
+ handler: async (request) => {
70
+ const {provisionerTokenId} = requireProvisionerContext(request);
71
+ return {
72
+ attached: await attachRunnerInstanceProviderId({
73
+ runnerInstanceId: request.params.runnerInstanceId,
74
+ provisionerId: provisionerTokenId,
75
+ providerRunnerId: request.body.provider_runner_id,
76
+ }),
77
+ };
78
+ },
79
+ });
80
+
59
81
  export const exchangeRunnerBootstrapRoute = defineRoute({
60
82
  method: 'POST',
61
83
  path: '/exchange',
@@ -9,7 +9,6 @@ export async function setup() {
9
9
 
10
10
  await runMigrations(db(), migrationsPath, '__drizzle_migrations_runners');
11
11
  await db().execute(sql`TRUNCATE runners_provisioner_capability_snapshots CASCADE`);
12
- await db().execute(sql`TRUNCATE runners_capacity_assignments CASCADE`);
13
12
  await db().execute(sql`TRUNCATE runners_runner_instances, runners_reservations CASCADE`);
14
13
  await db().execute(sql`TRUNCATE runners_provisioner_tokens CASCADE`);
15
14
  await db().execute(sql`TRUNCATE runners_ephemeral_registration_tokens CASCADE`);