Skip to content

Commit e59614a

Browse files
authored
feat(webapp): gate microvm regions behind compute access feature flag (#3366)
Adds region-level gating so MICROVM regions are only visible and usable by orgs with the `hasComputeAccess` feature flag. Admins and explicit allowlist behavior unchanged. - New shared helper (`regionAccess.server.ts`) with `resolveComputeAccess`, `defaultVisibilityFilter`, and `isComputeRegionAccessible` - `RegionsPresenter` filters out MICROVM regions for non-compute orgs - `SetDefaultRegionService` blocks setting a MICROVM region as default without compute access - `WorkerGroupService` blocks triggering runs in MICROVM regions without compute access - `computeTemplateCreation` refactored to use shared `resolveComputeAccess` - Updated snapshot callback schema
1 parent bd41bb2 commit e59614a

File tree

7 files changed

+121
-28
lines changed

7 files changed

+121
-28
lines changed

apps/supervisor/src/services/computeSnapshotService.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ export class ComputeSnapshotService {
8080

8181
/** Handle the callback from the gateway after a snapshot completes or fails. */
8282
async handleCallback(body: SnapshotCallbackPayload) {
83+
const snapshotId = body.status === "completed" ? body.snapshot_id : undefined;
84+
8385
this.logger.debug("Snapshot callback", {
84-
snapshotId: body.snapshot_id,
86+
snapshotId,
8587
instanceId: body.instance_id,
8688
status: body.status,
87-
error: body.error,
89+
error: body.status === "failed" ? body.error : undefined,
8890
metadata: body.metadata,
8991
durationMs: body.duration_ms,
9092
});
@@ -97,7 +99,7 @@ export class ComputeSnapshotService {
9799
return { ok: false as const, status: 400 };
98100
}
99101

100-
this.#emitSnapshotSpan(runId, body.duration_ms, body.snapshot_id);
102+
this.#emitSnapshotSpan(runId, body.duration_ms, snapshotId);
101103

102104
if (body.status === "completed") {
103105
const result = await this.workerClient.submitSuspendCompletion({

apps/webapp/app/presenters/v3/RegionsPresenter.server.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type Project } from "~/models/project.server";
22
import { type User } from "~/models/user.server";
33
import { FEATURE_FLAG } from "~/v3/featureFlags";
44
import { makeFlag } from "~/v3/featureFlags.server";
5+
import { defaultVisibilityFilter, resolveComputeAccess } from "~/v3/regionAccess.server";
56
import { BasePresenter } from "./basePresenter.server";
67
import { getCurrentPlan } from "~/services/platform.v3.server";
78

@@ -32,6 +33,9 @@ export class RegionsPresenter extends BasePresenter {
3233
organizationId: true,
3334
defaultWorkerGroupId: true,
3435
allowedWorkerQueues: true,
36+
organization: {
37+
select: { featureFlags: true },
38+
},
3539
},
3640
where: {
3741
slug: projectSlug,
@@ -58,6 +62,11 @@ export class RegionsPresenter extends BasePresenter {
5862
throw new Error("Default worker instance group not found");
5963
}
6064

65+
const hasComputeAccess = await resolveComputeAccess(
66+
this._replica,
67+
project.organization.featureFlags
68+
);
69+
6170
const visibleRegions = await this._replica.workerInstanceGroup.findMany({
6271
select: {
6372
id: true,
@@ -75,9 +84,7 @@ export class RegionsPresenter extends BasePresenter {
7584
? {
7685
masterQueue: { in: project.allowedWorkerQueues },
7786
}
78-
: {
79-
hidden: false,
80-
},
87+
: defaultVisibilityFilter(hasComputeAccess),
8188
orderBy: {
8289
name: "asc",
8390
},
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { type Prisma, type WorkloadType } from "@trigger.dev/database";
2+
import { type PrismaClientOrTransaction } from "~/db.server";
3+
import { FEATURE_FLAG } from "./featureFlags";
4+
import { makeFlag } from "./featureFlags.server";
5+
6+
/**
7+
* Resolves whether an org has compute access based on feature flags.
8+
*/
9+
export async function resolveComputeAccess(
10+
prisma: PrismaClientOrTransaction,
11+
orgFeatureFlags: unknown
12+
): Promise<boolean> {
13+
const flag = makeFlag(prisma);
14+
return flag({
15+
key: FEATURE_FLAG.hasComputeAccess,
16+
defaultValue: false,
17+
overrides: (orgFeatureFlags as Record<string, unknown>) ?? {},
18+
});
19+
}
20+
21+
/**
22+
* Builds a visibility filter for non-admin, non-allowlisted users.
23+
* Without compute access, MICROVM regions are excluded entirely.
24+
* With compute access, hidden flag works normally (existing behavior).
25+
*/
26+
export function defaultVisibilityFilter(
27+
hasComputeAccess: boolean
28+
): Prisma.WorkerInstanceGroupWhereInput {
29+
if (hasComputeAccess) {
30+
return { hidden: false };
31+
}
32+
33+
return { hidden: false, workloadType: { not: "MICROVM" } };
34+
}
35+
36+
/**
37+
* Whether a region is accessible given compute access.
38+
* MICROVM regions require compute access; all other types pass through.
39+
*/
40+
export function isComputeRegionAccessible(
41+
region: { workloadType: WorkloadType },
42+
hasComputeAccess: boolean
43+
): boolean {
44+
if (region.workloadType !== "MICROVM") {
45+
return true;
46+
}
47+
48+
// Allow access to any MICROVM region if the org has compute access
49+
return hasComputeAccess;
50+
}

apps/webapp/app/v3/services/computeTemplateCreation.server.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ import { machinePresetFromName } from "~/v3/machinePresets.server";
33
import { env } from "~/env.server";
44
import { logger } from "~/services/logger.server";
55
import type { PrismaClientOrTransaction } from "~/db.server";
6-
import { FEATURE_FLAG } from "~/v3/featureFlags";
7-
import { makeFlag } from "~/v3/featureFlags.server";
86
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
97
import { ServiceValidationError } from "./baseService.server";
108
import { FailDeploymentService } from "./failDeployment.server";
9+
import { resolveComputeAccess } from "../regionAccess.server";
1110

1211
type TemplateCreationMode = "required" | "shadow" | "skip";
1312

@@ -101,9 +100,7 @@ export class ComputeTemplateCreationService {
101100
},
102101
});
103102

104-
throw new ServiceValidationError(
105-
`Compute template creation failed: ${result.error}`
106-
);
103+
throw new ServiceValidationError(`Compute template creation failed: ${result.error}`);
107104
}
108105

109106
logger.info("Compute template created", {
@@ -132,16 +129,15 @@ export class ComputeTemplateCreationService {
132129
},
133130
});
134131

135-
if (project?.defaultWorkerGroup?.workloadType === "MICROVM") {
132+
if (!project) {
133+
return "skip";
134+
}
135+
136+
if (project.defaultWorkerGroup?.workloadType === "MICROVM") {
136137
return "required";
137138
}
138139

139-
const flag = makeFlag(prisma);
140-
const hasComputeAccess = await flag({
141-
key: FEATURE_FLAG.hasComputeAccess,
142-
defaultValue: false,
143-
overrides: (project?.organization?.featureFlags as Record<string, unknown>) ?? {},
144-
});
140+
const hasComputeAccess = await resolveComputeAccess(prisma, project.organization.featureFlags);
145141

146142
if (hasComputeAccess) {
147143
return "shadow";

apps/webapp/app/v3/services/setDefaultRegion.server.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isComputeRegionAccessible, resolveComputeAccess } from "~/v3/regionAccess.server";
12
import { BaseService, ServiceValidationError } from "./baseService.server";
23

34
export class SetDefaultRegionService extends BaseService {
@@ -24,6 +25,9 @@ export class SetDefaultRegionService extends BaseService {
2425
where: {
2526
id: projectId,
2627
},
28+
include: {
29+
organization: { select: { featureFlags: true } },
30+
},
2731
});
2832

2933
if (!project) {
@@ -36,8 +40,21 @@ export class SetDefaultRegionService extends BaseService {
3640
if (!project.allowedWorkerQueues.includes(workerGroup.masterQueue)) {
3741
throw new ServiceValidationError("You're not allowed to set this region as default");
3842
}
39-
} else if (workerGroup.hidden) {
40-
throw new ServiceValidationError("This region is not available to you");
43+
} else {
44+
if (workerGroup.hidden) {
45+
throw new ServiceValidationError("This region is not available to you");
46+
}
47+
48+
if (workerGroup.workloadType === "MICROVM") {
49+
const hasComputeAccess = await resolveComputeAccess(
50+
this._prisma,
51+
project.organization.featureFlags
52+
);
53+
54+
if (!isComputeRegionAccessible(workerGroup, hasComputeAccess)) {
55+
throw new ServiceValidationError("This region requires compute access");
56+
}
57+
}
4158
}
4259
}
4360

apps/webapp/app/v3/services/worker/workerGroupService.server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { WorkerGroupTokenService } from "./workerGroupTokenService.server";
44
import { logger } from "~/services/logger.server";
55
import { FEATURE_FLAG } from "~/v3/featureFlags";
66
import { makeFlag, makeSetFlag } from "~/v3/featureFlags.server";
7+
import { isComputeRegionAccessible, resolveComputeAccess } from "~/v3/regionAccess.server";
78

89
export class WorkerGroupService extends WithRunEngine {
910
private readonly defaultNamePrefix = "worker_group";
@@ -207,6 +208,7 @@ export class WorkerGroupService extends WithRunEngine {
207208
},
208209
include: {
209210
defaultWorkerGroup: true,
211+
organization: { select: { featureFlags: true } },
210212
},
211213
});
212214

@@ -243,6 +245,17 @@ export class WorkerGroupService extends WithRunEngine {
243245
throw new Error(`The region you specified isn't available to you ("${regionOverride}").`);
244246
}
245247

248+
if (workerGroup.workloadType === "MICROVM") {
249+
const hasComputeAccess = await resolveComputeAccess(
250+
this._prisma,
251+
project.organization.featureFlags
252+
);
253+
254+
if (!isComputeRegionAccessible(workerGroup, hasComputeAccess)) {
255+
throw new Error(`The region you specified isn't available to you ("${regionOverride}").`);
256+
}
257+
}
258+
246259
return workerGroup;
247260
}
248261

internal-packages/compute/src/types.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,20 @@ export const SnapshotRestoreRequestSchema = z.object({
6262
});
6363
export type SnapshotRestoreRequest = z.infer<typeof SnapshotRestoreRequestSchema>;
6464

65-
export const SnapshotCallbackPayloadSchema = z.object({
66-
snapshot_id: z.string(),
67-
instance_id: z.string(),
68-
status: z.enum(["completed", "failed"]),
69-
error: z.string().optional(),
70-
metadata: z.record(z.string()).optional(),
71-
duration_ms: z.number().optional(),
72-
});
65+
export const SnapshotCallbackPayloadSchema = z.discriminatedUnion("status", [
66+
z.object({
67+
status: z.literal("completed"),
68+
snapshot_id: z.string(),
69+
instance_id: z.string(),
70+
metadata: z.record(z.string()).optional(),
71+
duration_ms: z.number().optional(),
72+
}),
73+
z.object({
74+
status: z.literal("failed"),
75+
instance_id: z.string(),
76+
error: z.string().optional(),
77+
metadata: z.record(z.string()).optional(),
78+
duration_ms: z.number().optional(),
79+
}),
80+
]);
7381
export type SnapshotCallbackPayload = z.infer<typeof SnapshotCallbackPayloadSchema>;

0 commit comments

Comments
 (0)