Skip to content

Improve worker management admin route #2032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions apps/supervisor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ curl -sS \
-d "{\"name\": \"$wg_name\"}"
```

If the worker group is newly created, the response will include a `token` field. If the group already exists, no token is returned.

2. Create `.env` and set the worker token

```sh
Expand All @@ -43,25 +45,61 @@ pnpm exec trigger deploy --self-hosted
pnpm exec trigger deploy --self-hosted --network host
```

## Additional worker groups
## Worker group management

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:
### Shared variables

```sh
api_url=http://localhost:3030
admin_pat=tr_pat_... # edit this
```

- These are used by all commands

### Create a worker group

```sh
wg_name=my-worker

# edit these
admin_pat=tr_pat_...
curl -sS \
-X POST \
"$api_url/admin/api/v1/workers" \
-H "Authorization: Bearer $admin_pat" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$wg_name\"}"
```

- If the worker group already exists, no token will be returned

### Set a worker group as default for a project

```sh
wg_name=my-worker
project_id=clsw6q8wz...

curl -sS \
-X POST \
"$api_url/admin/api/v1/workers" \
-H "Authorization: Bearer $admin_pat" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$wg_name\", \"projectId\": \"$project_id\", \"makeDefaultForProject\": true}"
```

- If the worker group doesn't exist, yet it will be created
- If the worker group already exists, it will be attached to the project as default. No token will be returned.

### Remove the default worker group from a project

```sh
project_id=clsw6q8wz...

curl -sS \
-X POST \
"$api_url/admin/api/v1/workers" \
-H "Authorization: Bearer $admin_pat" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$wg_name\",
\"makeDefaultForProjectId\": \"$project_id\"
}"
-d "{\"projectId\": \"$project_id\", \"removeDefaultFromProject\": true}"
```

- The project will then use the global default again
- When `removeDefaultFromProject: true` no other actions will be performed
217 changes: 199 additions & 18 deletions apps/webapp/app/routes/admin.api.v1.workers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { tryCatch } from "@trigger.dev/core";
import { type Project } from "@trigger.dev/database";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
Expand All @@ -7,7 +9,9 @@ import { WorkerGroupService } from "~/v3/services/worker/workerGroupService.serv
const RequestBodySchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
makeDefaultForProjectId: z.string().optional(),
projectId: z.string().optional(),
makeDefaultForProject: z.boolean().default(false),
removeDefaultFromProject: z.boolean().default(false),
});

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

const user = await prisma.user.findUnique({
const user = await prisma.user.findFirst({
where: {
id: authenticationResult.userId,
},
Expand All @@ -34,30 +38,207 @@ export async function action({ request }: ActionFunctionArgs) {

try {
const rawBody = await request.json();
const { name, description, makeDefaultForProjectId } = RequestBodySchema.parse(rawBody ?? {});
const { name, description, projectId, makeDefaultForProject, removeDefaultFromProject } =
RequestBodySchema.parse(rawBody ?? {});

const service = new WorkerGroupService();
const { workerGroup, token } = await service.createWorkerGroup({
name,
description,
if (removeDefaultFromProject) {
if (!projectId) {
return json(
{
error: "projectId is required to remove default worker group from project",
},
{ status: 400 }
);
}

const updated = await removeDefaultWorkerGroupFromProject(projectId);

if (!updated.success) {
return json(
{ error: `failed to remove default worker group from project: ${updated.error}` },
{ status: 400 }
);
}

return json({
outcome: "removed default worker group from project",
project: updated.project,
});
}

const existingWorkerGroup = await prisma.workerInstanceGroup.findFirst({
where: {
// We only check managed worker groups
masterQueue: name,
},
});

if (makeDefaultForProjectId) {
await prisma.project.update({
where: {
id: makeDefaultForProjectId,
if (!existingWorkerGroup) {
const { workerGroup, token } = await createWorkerGroup(name, description);

if (!makeDefaultForProject) {
return json({
outcome: "created new worker group",
token,
workerGroup,
});
}

if (!projectId) {
return json(
{ error: "projectId is required to set worker group as default for project" },
{ status: 400 }
);
}

const updated = await setWorkerGroupAsDefaultForProject(workerGroup.id, projectId);

if (!updated.success) {
return json({ error: updated.error }, { status: 400 });
}

return json({
outcome: "set new worker group as default for project",
token,
workerGroup,
project: updated.project,
});
}

if (!makeDefaultForProject) {
return json(
{
error: "worker group already exists",
workerGroup: existingWorkerGroup,
},
data: {
defaultWorkerGroupId: workerGroup.id,
{ status: 400 }
);
}

if (!projectId) {
return json(
{ error: "projectId is required to set worker group as default for project" },
{ status: 400 }
);
}

const updated = await setWorkerGroupAsDefaultForProject(existingWorkerGroup.id, projectId);

if (!updated.success) {
return json(
{
error: `failed to set worker group as default for project: ${updated.error}`,
workerGroup: existingWorkerGroup,
},
});
{ status: 400 }
);
}

return json({
token,
workerGroup,
outcome: "set existing worker group as default for project",
workerGroup: existingWorkerGroup,
project: updated.project,
});
} catch (error) {
return json({ error: error instanceof Error ? error.message : error }, { status: 400 });
return json(
{
outcome: "unknown error",
error: error instanceof Error ? error.message : error,
},
{ status: 400 }
);
}
}

async function createWorkerGroup(name: string | undefined, description: string | undefined) {
const service = new WorkerGroupService();
return await service.createWorkerGroup({ name, description });
}

async function removeDefaultWorkerGroupFromProject(projectId: string) {
const project = await prisma.project.findFirst({
where: {
id: projectId,
},
});

if (!project) {
return {
success: false,
error: "project not found",
};
}

const [error] = await tryCatch(
prisma.project.update({
where: {
id: projectId,
},
data: {
defaultWorkerGroupId: null,
},
})
);

if (error) {
return {
success: false,
error: error instanceof Error ? error.message : error,
};
}

return {
success: true,
project,
};
}

async function setWorkerGroupAsDefaultForProject(
workerGroupId: string,
projectId: string
): Promise<
| {
success: false;
error: string;
}
| {
success: true;
project: Project;
}
> {
const project = await prisma.project.findFirst({
where: {
id: projectId,
},
});

if (!project) {
return {
success: false,
error: "project not found",
};
}

const [error] = await tryCatch(
prisma.project.update({
where: {
id: projectId,
},
data: {
defaultWorkerGroupId: workerGroupId,
},
})
);

if (error) {
return {
success: false,
error: error instanceof Error ? error.message : error,
};
}

return {
success: true,
project,
};
}