Skip to content

Commit 1b1ad16

Browse files
authored
Improve worker management admin route (#2032)
* improve route to add worker group, handles existing groups gracefully * add option to remove default worker group from project * separate project id field * update supervisor readme with route changes
1 parent 37aadfc commit 1b1ad16

File tree

2 files changed

+245
-26
lines changed

2 files changed

+245
-26
lines changed

apps/supervisor/README.md

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ curl -sS \
1919
-d "{\"name\": \"$wg_name\"}"
2020
```
2121

22+
If the worker group is newly created, the response will include a `token` field. If the group already exists, no token is returned.
23+
2224
2. Create `.env` and set the worker token
2325

2426
```sh
@@ -43,25 +45,61 @@ pnpm exec trigger deploy --self-hosted
4345
pnpm exec trigger deploy --self-hosted --network host
4446
```
4547

46-
## Additional worker groups
48+
## Worker group management
4749

48-
When adding more worker groups you might also want to make them the default for a specific project. This will allow you to test it without having to change the global default:
50+
### Shared variables
4951

5052
```sh
5153
api_url=http://localhost:3030
54+
admin_pat=tr_pat_... # edit this
55+
```
56+
57+
- These are used by all commands
58+
59+
### Create a worker group
60+
61+
```sh
5262
wg_name=my-worker
5363

54-
# edit these
55-
admin_pat=tr_pat_...
64+
curl -sS \
65+
-X POST \
66+
"$api_url/admin/api/v1/workers" \
67+
-H "Authorization: Bearer $admin_pat" \
68+
-H "Content-Type: application/json" \
69+
-d "{\"name\": \"$wg_name\"}"
70+
```
71+
72+
- If the worker group already exists, no token will be returned
73+
74+
### Set a worker group as default for a project
75+
76+
```sh
77+
wg_name=my-worker
78+
project_id=clsw6q8wz...
79+
80+
curl -sS \
81+
-X POST \
82+
"$api_url/admin/api/v1/workers" \
83+
-H "Authorization: Bearer $admin_pat" \
84+
-H "Content-Type: application/json" \
85+
-d "{\"name\": \"$wg_name\", \"projectId\": \"$project_id\", \"makeDefaultForProject\": true}"
86+
```
87+
88+
- If the worker group doesn't exist, yet it will be created
89+
- If the worker group already exists, it will be attached to the project as default. No token will be returned.
90+
91+
### Remove the default worker group from a project
92+
93+
```sh
5694
project_id=clsw6q8wz...
5795

5896
curl -sS \
5997
-X POST \
6098
"$api_url/admin/api/v1/workers" \
6199
-H "Authorization: Bearer $admin_pat" \
62100
-H "Content-Type: application/json" \
63-
-d "{
64-
\"name\": \"$wg_name\",
65-
\"makeDefaultForProjectId\": \"$project_id\"
66-
}"
101+
-d "{\"projectId\": \"$project_id\", \"removeDefaultFromProject\": true}"
67102
```
103+
104+
- The project will then use the global default again
105+
- When `removeDefaultFromProject: true` no other actions will be performed
Lines changed: 199 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
1+
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
2+
import { tryCatch } from "@trigger.dev/core";
3+
import { type Project } from "@trigger.dev/database";
24
import { z } from "zod";
35
import { prisma } from "~/db.server";
46
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
@@ -7,7 +9,9 @@ import { WorkerGroupService } from "~/v3/services/worker/workerGroupService.serv
79
const RequestBodySchema = z.object({
810
name: z.string().optional(),
911
description: z.string().optional(),
10-
makeDefaultForProjectId: z.string().optional(),
12+
projectId: z.string().optional(),
13+
makeDefaultForProject: z.boolean().default(false),
14+
removeDefaultFromProject: z.boolean().default(false),
1115
});
1216

1317
export async function action({ request }: ActionFunctionArgs) {
@@ -18,7 +22,7 @@ export async function action({ request }: ActionFunctionArgs) {
1822
return json({ error: "Invalid or Missing API key" }, { status: 401 });
1923
}
2024

21-
const user = await prisma.user.findUnique({
25+
const user = await prisma.user.findFirst({
2226
where: {
2327
id: authenticationResult.userId,
2428
},
@@ -34,30 +38,207 @@ export async function action({ request }: ActionFunctionArgs) {
3438

3539
try {
3640
const rawBody = await request.json();
37-
const { name, description, makeDefaultForProjectId } = RequestBodySchema.parse(rawBody ?? {});
41+
const { name, description, projectId, makeDefaultForProject, removeDefaultFromProject } =
42+
RequestBodySchema.parse(rawBody ?? {});
3843

39-
const service = new WorkerGroupService();
40-
const { workerGroup, token } = await service.createWorkerGroup({
41-
name,
42-
description,
44+
if (removeDefaultFromProject) {
45+
if (!projectId) {
46+
return json(
47+
{
48+
error: "projectId is required to remove default worker group from project",
49+
},
50+
{ status: 400 }
51+
);
52+
}
53+
54+
const updated = await removeDefaultWorkerGroupFromProject(projectId);
55+
56+
if (!updated.success) {
57+
return json(
58+
{ error: `failed to remove default worker group from project: ${updated.error}` },
59+
{ status: 400 }
60+
);
61+
}
62+
63+
return json({
64+
outcome: "removed default worker group from project",
65+
project: updated.project,
66+
});
67+
}
68+
69+
const existingWorkerGroup = await prisma.workerInstanceGroup.findFirst({
70+
where: {
71+
// We only check managed worker groups
72+
masterQueue: name,
73+
},
4374
});
4475

45-
if (makeDefaultForProjectId) {
46-
await prisma.project.update({
47-
where: {
48-
id: makeDefaultForProjectId,
76+
if (!existingWorkerGroup) {
77+
const { workerGroup, token } = await createWorkerGroup(name, description);
78+
79+
if (!makeDefaultForProject) {
80+
return json({
81+
outcome: "created new worker group",
82+
token,
83+
workerGroup,
84+
});
85+
}
86+
87+
if (!projectId) {
88+
return json(
89+
{ error: "projectId is required to set worker group as default for project" },
90+
{ status: 400 }
91+
);
92+
}
93+
94+
const updated = await setWorkerGroupAsDefaultForProject(workerGroup.id, projectId);
95+
96+
if (!updated.success) {
97+
return json({ error: updated.error }, { status: 400 });
98+
}
99+
100+
return json({
101+
outcome: "set new worker group as default for project",
102+
token,
103+
workerGroup,
104+
project: updated.project,
105+
});
106+
}
107+
108+
if (!makeDefaultForProject) {
109+
return json(
110+
{
111+
error: "worker group already exists",
112+
workerGroup: existingWorkerGroup,
49113
},
50-
data: {
51-
defaultWorkerGroupId: workerGroup.id,
114+
{ status: 400 }
115+
);
116+
}
117+
118+
if (!projectId) {
119+
return json(
120+
{ error: "projectId is required to set worker group as default for project" },
121+
{ status: 400 }
122+
);
123+
}
124+
125+
const updated = await setWorkerGroupAsDefaultForProject(existingWorkerGroup.id, projectId);
126+
127+
if (!updated.success) {
128+
return json(
129+
{
130+
error: `failed to set worker group as default for project: ${updated.error}`,
131+
workerGroup: existingWorkerGroup,
52132
},
53-
});
133+
{ status: 400 }
134+
);
54135
}
55136

56137
return json({
57-
token,
58-
workerGroup,
138+
outcome: "set existing worker group as default for project",
139+
workerGroup: existingWorkerGroup,
140+
project: updated.project,
59141
});
60142
} catch (error) {
61-
return json({ error: error instanceof Error ? error.message : error }, { status: 400 });
143+
return json(
144+
{
145+
outcome: "unknown error",
146+
error: error instanceof Error ? error.message : error,
147+
},
148+
{ status: 400 }
149+
);
150+
}
151+
}
152+
153+
async function createWorkerGroup(name: string | undefined, description: string | undefined) {
154+
const service = new WorkerGroupService();
155+
return await service.createWorkerGroup({ name, description });
156+
}
157+
158+
async function removeDefaultWorkerGroupFromProject(projectId: string) {
159+
const project = await prisma.project.findFirst({
160+
where: {
161+
id: projectId,
162+
},
163+
});
164+
165+
if (!project) {
166+
return {
167+
success: false,
168+
error: "project not found",
169+
};
62170
}
171+
172+
const [error] = await tryCatch(
173+
prisma.project.update({
174+
where: {
175+
id: projectId,
176+
},
177+
data: {
178+
defaultWorkerGroupId: null,
179+
},
180+
})
181+
);
182+
183+
if (error) {
184+
return {
185+
success: false,
186+
error: error instanceof Error ? error.message : error,
187+
};
188+
}
189+
190+
return {
191+
success: true,
192+
project,
193+
};
194+
}
195+
196+
async function setWorkerGroupAsDefaultForProject(
197+
workerGroupId: string,
198+
projectId: string
199+
): Promise<
200+
| {
201+
success: false;
202+
error: string;
203+
}
204+
| {
205+
success: true;
206+
project: Project;
207+
}
208+
> {
209+
const project = await prisma.project.findFirst({
210+
where: {
211+
id: projectId,
212+
},
213+
});
214+
215+
if (!project) {
216+
return {
217+
success: false,
218+
error: "project not found",
219+
};
220+
}
221+
222+
const [error] = await tryCatch(
223+
prisma.project.update({
224+
where: {
225+
id: projectId,
226+
},
227+
data: {
228+
defaultWorkerGroupId: workerGroupId,
229+
},
230+
})
231+
);
232+
233+
if (error) {
234+
return {
235+
success: false,
236+
error: error instanceof Error ? error.message : error,
237+
};
238+
}
239+
240+
return {
241+
success: true,
242+
project,
243+
};
63244
}

0 commit comments

Comments
 (0)