From 32a698211c56868bde47d0945f0c3d30cba45fa3 Mon Sep 17 00:00:00 2001 From: Matt Brockman Date: Thu, 16 Jul 2026 18:58:04 -0700 Subject: [PATCH] Add feature-flagged BYOC setup Use one team-targeted LaunchDarkly payload to gate BYOC and supply the E2B principal, allowed regions, and provider-specific setup templates. Keep the public dashboard limited to validated placeholder substitution; deployment orchestration remains out of scope. --- src/app/dashboard/[teamSlug]/byoc/page.tsx | 7 +- src/configs/sidebar.ts | 8 +- .../feature-flags/boolean-definitions.ts | 7 - src/core/modules/feature-flags/definitions.ts | 40 ++++ .../feature-flags/feature-flags.client.tsx | 16 +- .../feature-flags/feature-flags.server.ts | 9 +- src/features/dashboard/byoc/byoc-setup.tsx | 174 ++++++++++++++++++ src/features/dashboard/byoc/terraform.ts | 22 +++ .../dashboard/sidebar/use-visible-links.ts | 13 +- tests/unit/byoc-page.test.tsx | 91 +++++++++ tests/unit/byoc-terraform.test.ts | 29 +++ tests/unit/feature-flags.test.ts | 35 +++- tests/unit/sidebar-feature-flags.test.ts | 61 ++++++ tests/unit/sidebar.test.ts | 2 +- 14 files changed, 477 insertions(+), 37 deletions(-) create mode 100644 src/features/dashboard/byoc/byoc-setup.tsx create mode 100644 src/features/dashboard/byoc/terraform.ts create mode 100644 tests/unit/byoc-page.test.tsx create mode 100644 tests/unit/byoc-terraform.test.ts create mode 100644 tests/unit/sidebar-feature-flags.test.ts diff --git a/src/app/dashboard/[teamSlug]/byoc/page.tsx b/src/app/dashboard/[teamSlug]/byoc/page.tsx index 7598b57e7..d8e77603b 100644 --- a/src/app/dashboard/[teamSlug]/byoc/page.tsx +++ b/src/app/dashboard/[teamSlug]/byoc/page.tsx @@ -4,6 +4,7 @@ import { AUTH_URLS } from '@/configs/urls' import { featureFlags } from '@/core/modules/feature-flags/feature-flags.server' import { getAuthContext } from '@/core/server/auth' import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug' +import { ByocSetup } from '@/features/dashboard/byoc/byoc-setup' export const metadata: Metadata = { title: 'BYOC - E2B', @@ -31,7 +32,7 @@ export default async function ByocPage({ params }: ByocPageProps) { notFound() } - const byocEnabled = await featureFlags.isEnabled('byocEnabled', { + const byocSetup = await featureFlags.getPayload('byocSetup', { user: { id: authContext.user.id, email: authContext.user.email ?? undefined, @@ -42,9 +43,9 @@ export default async function ByocPage({ params }: ByocPageProps) { }, }) - if (!byocEnabled) { + if (!byocSetup.enabled) { notFound() } - return null + return } diff --git a/src/configs/sidebar.ts b/src/configs/sidebar.ts index 032c96e4d..0e6244569 100644 --- a/src/configs/sidebar.ts +++ b/src/configs/sidebar.ts @@ -22,13 +22,15 @@ type SidebarNavArgs = { teamSlug?: string } +export type SidebarFeatureFlagId = BooleanFeatureFlagId | 'byocSetup' + export type SidebarNavItem = { label: string href: (args: SidebarNavArgs) => string icon: Icon group?: string activeMatch?: string - featureFlag?: BooleanFeatureFlagId + featureFlag?: SidebarFeatureFlagId } export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ @@ -69,7 +71,7 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ href: (args) => PROTECTED_URLS.BYOC(args.teamSlug!), icon: CloudIcon, activeMatch: `/dashboard/*/byoc`, - featureFlag: 'byocEnabled', + featureFlag: 'byocSetup', }, ...(INCLUDE_ARGUS ? [ @@ -148,7 +150,7 @@ export const SIDEBAR_ALL_LINKS = [...SIDEBAR_MAIN_LINKS, ...SIDEBAR_EXTRA_LINKS] export function filterSidebarLinks( links: SidebarNavItem[], - isEnabled: (flagId: BooleanFeatureFlagId) => boolean + isEnabled: (flagId: SidebarFeatureFlagId) => boolean ) { return links.filter( (link) => !link.featureFlag || isEnabled(link.featureFlag) diff --git a/src/core/modules/feature-flags/boolean-definitions.ts b/src/core/modules/feature-flags/boolean-definitions.ts index c6329be39..17a284ce1 100644 --- a/src/core/modules/feature-flags/boolean-definitions.ts +++ b/src/core/modules/feature-flags/boolean-definitions.ts @@ -8,13 +8,6 @@ export const BOOLEAN_FEATURE_FLAGS = { description: 'Enables the dashboard agents launcher.', exposure: 'both', }, - byocEnabled: { - kind: 'boolean', - key: 'byoc_enabled', - defaultValue: false, - description: 'Enables the dashboard BYOC page.', - exposure: 'both', - }, connectionsEnabled: { kind: 'boolean', key: 'connections_enabled', diff --git a/src/core/modules/feature-flags/definitions.ts b/src/core/modules/feature-flags/definitions.ts index de0b03ddb..918e1a8b3 100644 --- a/src/core/modules/feature-flags/definitions.ts +++ b/src/core/modules/feature-flags/definitions.ts @@ -16,8 +16,48 @@ export const developmentConnectionSchema = z.object({ export type DevelopmentConnection = z.infer +const gcpRegionSchema = z + .string() + .trim() + .regex(/^[a-z][a-z0-9-]{2,62}$/) + +const byocSetupTemplateSchema = z.string().trim().min(1).max(50_000) + +export const byocSetupSchema = z.object({ + enabled: z.literal(true), + principal: z + .string() + .trim() + .regex( + /^serviceAccount:[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$/ + ), + regions: z + .array(gcpRegionSchema) + .min(1) + .max(20) + .refine((regions) => new Set(regions).size === regions.length), + templates: z.object({ + gcloud: byocSetupTemplateSchema, + terraform: byocSetupTemplateSchema, + }), +}) + +export type ByocSetupConfig = z.infer +export type ByocSetupValue = ByocSetupConfig | { enabled: false } + export const FEATURE_FLAGS = { ...BOOLEAN_FEATURE_FLAGS, + byocSetup: { + kind: 'payload', + key: 'byoc_setup', + defaultValue: { enabled: false } as ByocSetupValue, + schema: z.discriminatedUnion('enabled', [ + z.object({ enabled: z.literal(false) }), + byocSetupSchema, + ]), + description: 'Configures team-targeted BYOC onboarding.', + exposure: 'both', + }, developmentConnections: { kind: 'payload', key: 'dev_connections', diff --git a/src/core/modules/feature-flags/feature-flags.client.tsx b/src/core/modules/feature-flags/feature-flags.client.tsx index 8b498d50d..a0a1b3843 100644 --- a/src/core/modules/feature-flags/feature-flags.client.tsx +++ b/src/core/modules/feature-flags/feature-flags.client.tsx @@ -16,6 +16,7 @@ import type { EvaluatedFeatureFlag } from '@/core/modules/feature-flags/types' type FeatureFlagsContextValue = { flags: EvaluatedFeatureFlag[] isEnabled(flagId: BooleanFeatureFlagId): boolean + hasPayload(flagId: 'byocSetup'): boolean } const FeatureFlagsContext = createContext( @@ -47,10 +48,17 @@ export function FeatureFlagsProvider({ [flagsById] ) - const value = useMemo( - () => ({ flags: initialFlags, isEnabled }), - [initialFlags, isEnabled] - ) + const hasPayload = (flagId: 'byocSetup') => { + const value = flagsById.get(flagId)?.value + return ( + typeof value === 'object' && + value !== null && + 'enabled' in value && + value.enabled === true + ) + } + + const value = { flags: initialFlags, hasPayload, isEnabled } return ( diff --git a/src/core/modules/feature-flags/feature-flags.server.ts b/src/core/modules/feature-flags/feature-flags.server.ts index eadf57ccd..e331a0d93 100644 --- a/src/core/modules/feature-flags/feature-flags.server.ts +++ b/src/core/modules/feature-flags/feature-flags.server.ts @@ -112,13 +112,12 @@ export function createFeatureFlagService( }, async getPayload(flagId, context) { - const flag = FEATURE_FLAGS[flagId] + const flag = FEATURE_FLAGS[flagId] as PayloadFeatureFlagDefinition< + PayloadFeatureFlagValue + > const snapshot = await provider.evaluate(context, [flag]) - return parsePayload( - flag, - snapshot.getPayload(flag.key) - ) as PayloadFeatureFlagValue + return parsePayload(flag, snapshot.getPayload(flag.key)) }, async evaluateAll(context) { diff --git a/src/features/dashboard/byoc/byoc-setup.tsx b/src/features/dashboard/byoc/byoc-setup.tsx new file mode 100644 index 000000000..14f231197 --- /dev/null +++ b/src/features/dashboard/byoc/byoc-setup.tsx @@ -0,0 +1,174 @@ +'use client' + +import { useState } from 'react' +import type { ByocSetupConfig } from '@/core/modules/feature-flags/definitions' +import { Page } from '@/features/dashboard/layouts/page' +import { CodeBlock } from '@/ui/code-block' +import { CloudIcon, InfoIcon, TerminalIcon } from '@/ui/primitives/icons' +import { Input } from '@/ui/primitives/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/ui/primitives/select' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs' +import { isValidGcpProjectId, renderByocSetupTemplate } from './terraform' + +const PROJECT_PLACEHOLDER = 'YOUR_GCP_PROJECT_ID' + +export function ByocSetup({ config }: { config: ByocSetupConfig }) { + const [projectId, setProjectId] = useState('') + const [region, setRegion] = useState(() => config.regions[0] ?? '') + const trimmedProjectId = projectId.trim() + const projectIdValid = isValidGcpProjectId(trimmedProjectId) + const projectIdInvalid = trimmedProjectId.length > 0 && !projectIdValid + const templateValues = { + principal: config.principal, + projectId: projectIdValid ? trimmedProjectId : PROJECT_PLACEHOLDER, + region, + } + const templates = { + gcloud: renderByocSetupTemplate({ + ...templateValues, + template: config.templates.gcloud, + }), + terraform: renderByocSetupTemplate({ + ...templateValues, + template: config.templates.terraform, + }), + } + + return ( + +
+
+
+ +

Set up BYOC

+
+

+ Create the project-local identity E2B will use to deploy and operate + your region. E2B receives impersonation access, never a service + account key. +

+
+ +
+
+
+

Configuration

+

+ Choose the destination +

+
+ + + + + +
+ +
+

+ E2B access principal +

+

+ {config.principal} +

+
+
+
+ +
+
+
+

Generated file

+

+ Access setup +

+
+

+ Choose Terraform or gcloud +

+
+ + + + Terraform + + + gcloud + + + + } + lang="hcl" + title="main.tf" + viewportProps={{ className: 'max-h-[560px] max-w-full' }} + > + {templates.terraform} + + + + } + lang="bash" + title="Run in Cloud Shell" + viewportProps={{ className: 'max-h-[560px] max-w-full' }} + > + {templates.gcloud} + + + +
+
+
+
+ ) +} diff --git a/src/features/dashboard/byoc/terraform.ts b/src/features/dashboard/byoc/terraform.ts new file mode 100644 index 000000000..77e528287 --- /dev/null +++ b/src/features/dashboard/byoc/terraform.ts @@ -0,0 +1,22 @@ +const GCP_PROJECT_ID_PATTERN = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/ + +export function isValidGcpProjectId(projectId: string) { + return GCP_PROJECT_ID_PATTERN.test(projectId) +} + +export function renderByocSetupTemplate({ + principal, + projectId, + region, + template, +}: { + principal: string + projectId: string + region: string + template: string +}) { + return template + .replaceAll('{{PROJECT_ID}}', projectId) + .replaceAll('{{REGION}}', region) + .replaceAll('{{E2B_PRINCIPAL}}', principal) +} diff --git a/src/features/dashboard/sidebar/use-visible-links.ts b/src/features/dashboard/sidebar/use-visible-links.ts index b15a5dff7..29c78c94b 100644 --- a/src/features/dashboard/sidebar/use-visible-links.ts +++ b/src/features/dashboard/sidebar/use-visible-links.ts @@ -1,11 +1,16 @@ 'use client' -import { useMemo } from 'react' -import { filterSidebarLinks, type SidebarNavItem } from '@/configs/sidebar' +import { + filterSidebarLinks, + type SidebarFeatureFlagId, + type SidebarNavItem, +} from '@/configs/sidebar' import { useFeatureFlags } from '@/core/modules/feature-flags/feature-flags.client' export function useVisibleSidebarLinks(links: SidebarNavItem[]) { - const { isEnabled } = useFeatureFlags() + const { hasPayload, isEnabled } = useFeatureFlags() - return useMemo(() => filterSidebarLinks(links, isEnabled), [isEnabled, links]) + return filterSidebarLinks(links, (flagId: SidebarFeatureFlagId) => + flagId === 'byocSetup' ? hasPayload(flagId) : isEnabled(flagId) + ) } diff --git a/tests/unit/byoc-page.test.tsx b/tests/unit/byoc-page.test.tsx new file mode 100644 index 000000000..f3712cc7d --- /dev/null +++ b/tests/unit/byoc-page.test.tsx @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + getAuthContext: vi.fn(), + getPayload: vi.fn(), + getTeamIdFromSlug: vi.fn(), + notFound: vi.fn(() => { + throw new Error('not found') + }), + redirect: vi.fn(() => { + throw new Error('redirect') + }), +})) + +vi.mock('next/navigation', () => ({ + notFound: mocks.notFound, + redirect: mocks.redirect, +})) + +vi.mock('@/core/server/auth', () => ({ + getAuthContext: mocks.getAuthContext, +})) + +vi.mock('@/core/server/functions/team/get-team-id-from-slug', () => ({ + getTeamIdFromSlug: mocks.getTeamIdFromSlug, +})) + +vi.mock('@/core/modules/feature-flags/feature-flags.server', () => ({ + featureFlags: { + getPayload: mocks.getPayload, + }, +})) + +import ByocPage from '@/app/dashboard/[teamSlug]/byoc/page' + +describe('ByocPage', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.getAuthContext.mockResolvedValue({ + accessToken: 'access-token', + user: { + id: 'user-id', + email: 'user@example.com', + }, + }) + mocks.getTeamIdFromSlug.mockResolvedValue({ + ok: true, + data: 'team-id', + }) + }) + + it('returns not found when BYOC setup is not configured', async () => { + mocks.getPayload.mockResolvedValue({ enabled: false }) + + await expect( + ByocPage({ + params: Promise.resolve({ teamSlug: 'team-slug' }), + }) + ).rejects.toThrow('not found') + }) + + it('targets the resolved team and renders its setup configuration', async () => { + const config = { + enabled: true, + principal: + 'serviceAccount:byoc-deployments-api@example-project.iam.gserviceaccount.com', + regions: ['us-central1'], + templates: { + gcloud: 'gcloud --project={{PROJECT_ID}}', + terraform: 'project_id = "{{PROJECT_ID}}"', + }, + } + mocks.getPayload.mockResolvedValue(config) + + const page = await ByocPage({ + params: Promise.resolve({ teamSlug: 'team-slug' }), + }) + + expect(mocks.getPayload).toHaveBeenCalledWith('byocSetup', { + user: { + id: 'user-id', + email: 'user@example.com', + }, + team: { + id: 'team-id', + slug: 'team-slug', + }, + }) + expect(page.props.config).toEqual(config) + }) +}) diff --git a/tests/unit/byoc-terraform.test.ts b/tests/unit/byoc-terraform.test.ts new file mode 100644 index 000000000..9e394c2c5 --- /dev/null +++ b/tests/unit/byoc-terraform.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { + isValidGcpProjectId, + renderByocSetupTemplate, +} from '@/features/dashboard/byoc/terraform' + +describe('BYOC setup templates', () => { + it('substitutes only the selected values into flag-provided content', () => { + const rendered = renderByocSetupTemplate({ + principal: + 'serviceAccount:byoc-deployments-api@example-project.iam.gserviceaccount.com', + projectId: 'customer-project', + region: 'us-west1', + template: + 'project={{PROJECT_ID}} region={{REGION}} principal={{E2B_PRINCIPAL}}', + }) + + expect(rendered).toBe( + 'project=customer-project region=us-west1 principal=serviceAccount:byoc-deployments-api@example-project.iam.gserviceaccount.com' + ) + }) + + it('accepts only Google Cloud project IDs', () => { + expect(isValidGcpProjectId('customer-project')).toBe(true) + expect(isValidGcpProjectId('Customer Project')).toBe(false) + expect(isValidGcpProjectId('x')).toBe(false) + expect(isValidGcpProjectId('customer_project')).toBe(false) + }) +}) diff --git a/tests/unit/feature-flags.test.ts b/tests/unit/feature-flags.test.ts index ea554d398..12ffcada6 100644 --- a/tests/unit/feature-flags.test.ts +++ b/tests/unit/feature-flags.test.ts @@ -16,6 +16,17 @@ const context = { }, } satisfies FeatureFlagContext +const byocSetup = { + enabled: true, + principal: + 'serviceAccount:byoc-deployments-api@example-project.iam.gserviceaccount.com', + regions: ['us-central1', 'us-west1'], + templates: { + gcloud: 'gcloud --project={{PROJECT_ID}}', + terraform: 'project_id = "{{PROJECT_ID}}"', + }, +} + describe('createFeatureFlagService', () => { it('evaluates boolean flags through the provider', async () => { const provider = { @@ -120,7 +131,11 @@ describe('createFeatureFlagService', () => { const provider = { evaluate: vi.fn().mockResolvedValue({ getFlagValue: vi.fn().mockReturnValue(true), - getPayload: vi.fn(), + getPayload: vi + .fn() + .mockImplementation((key) => + key === FEATURE_FLAGS.byocSetup.key ? byocSetup : undefined + ), }), } @@ -129,10 +144,10 @@ describe('createFeatureFlagService', () => { expect(provider.evaluate).toHaveBeenCalledTimes(1) expect(provider.evaluate).toHaveBeenCalledWith(context, [ FEATURE_FLAGS.agentsEnabled, - FEATURE_FLAGS.byocEnabled, FEATURE_FLAGS.connectionsEnabled, FEATURE_FLAGS.newSandboxList, FEATURE_FLAGS.disableE2BAccessTokenProvisioning, + FEATURE_FLAGS.byocSetup, ]) expect(result).toEqual([ { @@ -143,14 +158,6 @@ describe('createFeatureFlagService', () => { defaultValue: false, value: true, }, - { - id: 'byocEnabled', - key: 'byoc_enabled', - kind: 'boolean', - description: 'Enables the dashboard BYOC page.', - defaultValue: false, - value: true, - }, { id: 'connectionsEnabled', key: 'connections_enabled', @@ -177,6 +184,14 @@ describe('createFeatureFlagService', () => { defaultValue: false, value: true, }, + { + id: 'byocSetup', + key: 'byoc_setup', + kind: 'payload', + description: 'Configures team-targeted BYOC onboarding.', + defaultValue: { enabled: false }, + value: byocSetup, + }, ]) }) }) diff --git a/tests/unit/sidebar-feature-flags.test.ts b/tests/unit/sidebar-feature-flags.test.ts new file mode 100644 index 000000000..6ba71f6b7 --- /dev/null +++ b/tests/unit/sidebar-feature-flags.test.ts @@ -0,0 +1,61 @@ +import { createElement } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it } from 'vitest' +import { SIDEBAR_MAIN_LINKS } from '@/configs/sidebar' +import { + FeatureFlagsProvider, + useFeatureFlags, +} from '@/core/modules/feature-flags/feature-flags.client' + +function ByocVisibility() { + const { hasPayload } = useFeatureFlags() + return createElement( + 'span', + null, + hasPayload('byocSetup') ? 'shown' : 'hidden' + ) +} + +function renderByocVisibility(value: unknown) { + return renderToStaticMarkup( + createElement( + FeatureFlagsProvider, + { + initialFlags: [ + { + id: 'byocSetup', + key: 'byoc_setup', + kind: 'payload', + value, + defaultValue: { enabled: false }, + }, + ], + }, + createElement(ByocVisibility) + ) + ) +} + +describe('BYOC sidebar payload', () => { + it('uses the configured payload as the visibility gate', () => { + expect( + renderByocVisibility({ + enabled: true, + principal: + 'serviceAccount:byoc-deployments-api@example-project.iam.gserviceaccount.com', + regions: ['us-central1'], + templates: { + gcloud: 'gcloud --project={{PROJECT_ID}}', + terraform: 'project_id = "{{PROJECT_ID}}"', + }, + }) + ).toContain('shown') + expect(renderByocVisibility({ enabled: false })).toContain('hidden') + }) + + it('keeps the BYOC navigation tied to the payload flag', () => { + expect( + SIDEBAR_MAIN_LINKS.find((link) => link.label === 'BYOC')?.featureFlag + ).toBe('byocSetup') + }) +}) diff --git a/tests/unit/sidebar.test.ts b/tests/unit/sidebar.test.ts index d90752c65..e21f33b08 100644 --- a/tests/unit/sidebar.test.ts +++ b/tests/unit/sidebar.test.ts @@ -13,7 +13,7 @@ describe('filterSidebarLinks', () => { ) const byocOnly = filterSidebarLinks( SIDEBAR_MAIN_LINKS, - (flagId) => flagId === 'byocEnabled' + (flagId) => flagId === 'byocSetup' ) expect(agentsOnly.map((link) => link.label)).toContain('Agents')