diff --git a/control-plane/src/container-driver.ts b/control-plane/src/container-driver.ts index 66716ba2c..76211c08f 100644 --- a/control-plane/src/container-driver.ts +++ b/control-plane/src/container-driver.ts @@ -40,6 +40,11 @@ export type ContainerDriverConfig = { * `product` string `TenantProvisioningRequest` carries; an unconfigured product is a real * misconfiguration, not a silent no-op (see `bindingFor`). */ bindings: Record; + /** The hosted fleet's central Sentry DSN (#7876), injected into every tenant container at cold boot as + * {@link SENTRY_DSN_ENV_VAR} so its self-host process reports to it. Omitted/blank ⇒ containers start with + * no injected DSN, byte-identical to the pre-#7876 call — the platform simply isn't reporting yet (e.g. + * before #7875 provisions the secret). Sourced from the control-plane's own env in driver-factory.ts. */ + centralSentryDsn?: string; }; export type ContainerDriver = { @@ -75,21 +80,34 @@ export const PINNED_VERSION_ENV_VAR = "LOOPOVER_PINNED_VERSION"; * actually has custodied -- this driver never sees or needs to know what that is. */ export const TENANT_SECRET_ENV_VAR = "LOOPOVER_TENANT_SECRET_TOKEN"; +/** The env var a tenant's container reads its Sentry DSN from at cold boot (#7876, implements #4934). A tenant + * container runs an unmodified self-host image (root Dockerfile for ORB; packages/loopover-miner/Dockerfile + * for AMS), whose process already inits error reporting from this exact var via the opt-in `initSentry` + * pattern (`src/selfhost/sentry.ts`: a complete no-op when unset) — so pointing the hosted fleet at the + * central DSN is purely INJECTING that value here, never a second Sentry-wiring mechanism. Deliberately the + * self-host name `SENTRY_DSN` (not a hosted-only alias): the image's own init reads this and only this, and a + * self-hoster who sets their own `SENTRY_DSN` is completely unaffected — this only supplies a value the + * hosted platform would otherwise leave unset. The value is a control-plane secret (provisioned by #7875), + * never hardcoded or committed. */ +export const SENTRY_DSN_ENV_VAR = "SENTRY_DSN"; + /** Idempotent: an already-provisioned tenant's container is left running as-is, never restarted -- a repeat * create must not interrupt a container mid-work. This is also the ONLY point in a tenant's lifecycle where * `envVars` actually reach the container (confirmed against the real `@cloudflare/containers` SDK: a `start()` * call against an already-running/starting instance is a no-op or throws, never re-applies `envVars`) -- so - * both of the values below must already be known by the time this runs, not supplied later. A tenant with a - * `pinnedVersion` (#4898) starts with that version in {@link PINNED_VERSION_ENV_VAR}; one with a + * all three of the values below must already be known by the time this runs, not supplied later. A tenant with + * a `pinnedVersion` (#4898) starts with that version in {@link PINNED_VERSION_ENV_VAR}; one with a * `bootstrapSecret` (#8202, set on `request` by `provisionTenant` from `injectSecrets`' result) starts with it - * in {@link TENANT_SECRET_ENV_VAR}; a tenant with neither gets the exact pre-#4898 `start()` call, so every - * existing tenant's behavior is byte-identical until either rollout applies. */ + * in {@link TENANT_SECRET_ENV_VAR}; and when the config carries a central Sentry DSN (#7876) every container + * starts with it in {@link SENTRY_DSN_ENV_VAR}. A tenant with none of them gets the exact pre-#4898 `start()` + * call, so every existing tenant's behavior is byte-identical until a rollout applies. */ export async function createTenantContainer(config: ContainerDriverConfig, request: TenantProvisioningRequest): Promise { const stub = bindingFor(config, request.product).getByName(instanceNameFor(request)); if (await stub.isProvisioned()) return; const envVars: Record = {}; if (request.tenant.pinnedVersion) envVars[PINNED_VERSION_ENV_VAR] = request.tenant.pinnedVersion; if (request.bootstrapSecret) envVars[TENANT_SECRET_ENV_VAR] = request.bootstrapSecret; + if (config.centralSentryDsn) envVars[SENTRY_DSN_ENV_VAR] = config.centralSentryDsn; if (Object.keys(envVars).length > 0) { await stub.start({ envVars }); } else { diff --git a/control-plane/src/driver-factory.ts b/control-plane/src/driver-factory.ts index d8922254e..9513bc1c7 100644 --- a/control-plane/src/driver-factory.ts +++ b/control-plane/src/driver-factory.ts @@ -75,7 +75,12 @@ export function createTenantProvisioningDriver( } if (containerBindings && Object.keys(containerBindings).length > 0) { - driver = withRealContainerDriver(driver, createContainerDriver({ bindings: containerBindings })); + // #7876: point every tenant container at the hosted fleet's central Sentry DSN when the control-plane + // secret is set (per #7875). Blank/unset ⇒ omitted, so containers start byte-identical to pre-#7876. + driver = withRealContainerDriver( + driver, + createContainerDriver({ bindings: containerBindings, ...(nonBlank(env.SENTRY_DSN) ? { centralSentryDsn: nonBlank(env.SENTRY_DSN) } : {}) }), + ); } const mainAppBaseUrl = nonBlank(env.MAIN_APP_BASE_URL); diff --git a/control-plane/src/env.d.ts b/control-plane/src/env.d.ts index 6fdbce9bc..6503d36b1 100644 --- a/control-plane/src/env.d.ts +++ b/control-plane/src/env.d.ts @@ -24,6 +24,12 @@ declare global { * (a plain var, see wrangler.jsonc); createTenantProvisioningDriver falls back to the fake otherwise. * Genuinely sensitive: whoever holds it can call every internal admin route in the main app. */ INTERNAL_JOB_TOKEN?: string; + /** The hosted fleet's central Sentry DSN (#7876, implements #4934), provisioned as a secret by #7875. + * createTenantProvisioningDriver injects it into every tenant container as `SENTRY_DSN` so the container's + * own self-host process reports to it (`src/selfhost/sentry.ts`'s opt-in init). Unset ⇒ containers start + * with no injected DSN, byte-identical to today. A DSN is a write-only ingest URL rather than a + * credential, but it's kept a secret (never a committed var) so the hosted ingest endpoint isn't public. */ + SENTRY_DSN?: string; } } diff --git a/control-plane/src/index.ts b/control-plane/src/index.ts index 45851d853..48d4c4d78 100644 --- a/control-plane/src/index.ts +++ b/control-plane/src/index.ts @@ -66,6 +66,7 @@ export { instanceNameFor, PINNED_VERSION_ENV_VAR, TENANT_SECRET_ENV_VAR, + SENTRY_DSN_ENV_VAR, tenantContainerExists, type ContainerDriver, type ContainerDriverConfig, diff --git a/control-plane/src/worker.ts b/control-plane/src/worker.ts index b1df38412..602652e93 100644 --- a/control-plane/src/worker.ts +++ b/control-plane/src/worker.ts @@ -56,7 +56,9 @@ export default { const driver = createTenantProvisioningDriver( // #8066: MAIN_APP_BASE_URL/INTERNAL_JOB_TOKEN select the real secret driver (against the main app's // token broker) once both are configured, same opt-in-per-piece shape as NEON_API_KEY/NEON_PROJECT_ID. - { NEON_API_KEY: env.NEON_API_KEY, NEON_PROJECT_ID: env.NEON_PROJECT_ID, MAIN_APP_BASE_URL: env.MAIN_APP_BASE_URL, INTERNAL_JOB_TOKEN: env.INTERNAL_JOB_TOKEN }, + // #7876: SENTRY_DSN is the hosted fleet's central DSN (a control-plane secret, #7875) — the factory + // injects it into every tenant container so its self-host process reports to it (unset ⇒ no-op). + { NEON_API_KEY: env.NEON_API_KEY, NEON_PROJECT_ID: env.NEON_PROJECT_ID, MAIN_APP_BASE_URL: env.MAIN_APP_BASE_URL, INTERNAL_JOB_TOKEN: env.INTERNAL_JOB_TOKEN, SENTRY_DSN: env.SENTRY_DSN }, { orb: env.ORB_TENANT_CONTAINER, ams: env.AMS_TENANT_CONTAINER }, ); const app = createTenantHttpApp({ diff --git a/control-plane/test/container-driver.test.ts b/control-plane/test/container-driver.test.ts index 8b460f7ca..90d7cda95 100644 --- a/control-plane/test/container-driver.test.ts +++ b/control-plane/test/container-driver.test.ts @@ -10,6 +10,7 @@ import { destroyTenantContainer, PINNED_VERSION_ENV_VAR, TENANT_SECRET_ENV_VAR, + SENTRY_DSN_ENV_VAR, tenantContainerExists, type ContainerDriverConfig, type ContainerNamespaceLike, @@ -224,3 +225,46 @@ test("a repeat create of an already-provisioned tenant with a bootstrap secret n assert.deepEqual(stub.startOptions, []); }); + +// #7876: the hosted fleet's central Sentry DSN rides into every tenant container at cold boot as +// SENTRY_DSN_ENV_VAR — the exact self-host var its own process reads via src/selfhost/sentry.ts's opt-in +// init, so pointing the fleet at central telemetry is purely injecting the value, not a second mechanism. +function configWithDsn(stub: ContainerStubLike, centralSentryDsn: string): ContainerDriverConfig { + return { bindings: { orb: { getByName: () => stub } }, centralSentryDsn }; +} + +test("a container starts with SENTRY_DSN_ENV_VAR when the config carries the central DSN (#7876)", async () => { + const stub = optionCapturingStub(); + + await createTenantContainer(configWithDsn(stub, "https://k@o0.ingest.sentry.io/1"), { tenant: { name: "acme" }, product: "orb" }); + + assert.deepEqual(stub.startOptions, [{ envVars: { [SENTRY_DSN_ENV_VAR]: "https://k@o0.ingest.sentry.io/1" } }]); +}); + +test("the central DSN merges with pinnedVersion + bootstrapSecret into one start() call (#7876)", async () => { + const stub = optionCapturingStub(); + + await createTenantContainer(configWithDsn(stub, "https://k@o0.ingest.sentry.io/1"), { + tenant: { name: "acme", pinnedVersion: "v1.4.2" }, + product: "orb", + bootstrapSecret: "orbsec_xyz", + }); + + assert.deepEqual(stub.startOptions, [ + { + envVars: { + [PINNED_VERSION_ENV_VAR]: "v1.4.2", + [TENANT_SECRET_ENV_VAR]: "orbsec_xyz", + [SENTRY_DSN_ENV_VAR]: "https://k@o0.ingest.sentry.io/1", + }, + }, + ]); +}); + +test("a config with no central DSN never injects SENTRY_DSN — byte-identical to today for an otherwise-plain tenant (#7876)", async () => { + const stub = optionCapturingStub(); + + await createTenantContainer(configFor(stub), { tenant: { name: "acme" }, product: "orb" }); + + assert.deepEqual(stub.startOptions, [undefined]); +}); diff --git a/control-plane/test/driver-factory.test.ts b/control-plane/test/driver-factory.test.ts index a75942ee2..7a63fb231 100644 --- a/control-plane/test/driver-factory.test.ts +++ b/control-plane/test/driver-factory.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, test } from "node:test"; import { createFakeTenantProvisioningDriver, createTenantProvisioningDriver, + SENTRY_DSN_ENV_VAR, withRealContainerDriver, withRealDatabaseDriver, withRealSecretDriver, @@ -17,6 +18,32 @@ import { type TenantProvisioningRequest, } from "../dist/index.js"; +/** A container namespace whose stub records each `start()` call's options — lets a factory test assert what + * the composed container driver actually injects (the shared fake above deliberately discards options). */ +function capturingContainerNamespace(): ContainerNamespaceLike & { startOptions: Parameters[0][] } { + const startOptions: Parameters[0][] = []; + let flag = false; + const stub: ContainerStubLike = { + async start(options) { + startOptions.push(options); + flag = true; + }, + async stop() { + flag = false; + }, + async isProvisioned() { + return flag; + }, + async markProvisioned() { + flag = true; + }, + async markDeprovisioned() { + flag = false; + }, + }; + return { getByName: () => stub, startOptions }; +} + function fakeContainerNamespace(provisioned = false): ContainerNamespaceLike { let flag = provisioned; const stub: ContainerStubLike = { @@ -172,6 +199,26 @@ test("createTenantProvisioningDriver: selects the real container driver when con assert.equal(await driver.containerExists(REQUEST), true); }); +test("createTenantProvisioningDriver: injects env.SENTRY_DSN into tenant containers when set (#7876)", async () => { + const orb = capturingContainerNamespace(); + const driver = createTenantProvisioningDriver({ SENTRY_DSN: "https://k@o0.ingest.sentry.io/1" }, { orb }); + + await driver.createContainer(REQUEST); + + assert.deepEqual(orb.startOptions, [{ envVars: { [SENTRY_DSN_ENV_VAR]: "https://k@o0.ingest.sentry.io/1" } }]); +}); + +test("createTenantProvisioningDriver: a blank or unset SENTRY_DSN injects nothing — containers start as before (#7876)", async () => { + for (const env of [{}, { SENTRY_DSN: "" }, { SENTRY_DSN: " " }]) { + const orb = capturingContainerNamespace(); + const driver = createTenantProvisioningDriver(env, { orb }); + + await driver.createContainer(REQUEST); + + assert.deepEqual(orb.startOptions, [undefined]); + } +}); + test("createTenantProvisioningDriver: composes both the real database driver AND the real container driver together", async () => { globalThis.fetch = (async () => new Response(JSON.stringify({ branches: [] }), { status: 200 })) as unknown as typeof fetch;