Skip to content
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
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ persisted config, so a local control plane gives you an end-to-end loop without
cloud infrastructure.

The platform has a `dev:fake` mode that swaps in fake provider adapters, so it needs no
Neon, Fly or Tigris credentials:
Fly or Tigris credentials (nor Neon's — Neon is no longer used by any environment; its
adapter code is retained, not live):

```bash
# 1. Postgres for the control plane itself
Expand Down
30 changes: 18 additions & 12 deletions src/commands/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ type Opts = { branch?: string; group?: string; json?: boolean }
// Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,
// cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
// actual usage). Thin wrapper over PATCH /database/settings {scaleToZero} — insta-db-backed
// postgres only; Neon-backed services manage their own autosuspend and the platform returns an
// error for them.
// postgres only. Legacy Neon path: Neon is no longer used by any environment (postgres is 100%
// insta-db) and this code is retained, not live — Neon-backed services managed their own
// autosuspend and the platform returned an error for them.
export async function dbAlwaysOn(mode: string, opts: Opts): Promise<void> {
if (mode !== 'on' && mode !== 'off') throw new Error('mode must be on|off')
const api = await ApiClient.load()
Expand Down Expand Up @@ -46,10 +47,12 @@ export function fmtMib(mib: number): string {
}

// The read outcome, as a seam. rawRequest THROWS ApiError on any status >= 400 (api.ts — it only
// differs from request in returning {status,body} below 400, for 202 branching), so the Neon case
// and the friendly wrapping must live in a catch, not in status branching on the return value —
// branches on res.status >= 400 after rawRequest are unreachable. Takes the client as an argument
// so tests drive it with a stub, per this repo's pure-seam convention.
// differs from request in returning {status,body} below 400, for 202 branching), so the soft
// no-instance case and the friendly wrapping must live in a catch, not in status branching on the
// return value — branches on res.status >= 400 after rawRequest are unreachable. That soft case is
// the legacy Neon path: Neon is no longer used by any environment; the handling is retained, not
// live. Takes the client as an argument so tests drive it with a stub, per this repo's pure-seam
// convention.
export type DbInstanceRead = { kind: 'ok'; body: any } | { kind: 'no-instance' }

export async function fetchDbInstance(
Expand All @@ -62,7 +65,8 @@ export async function fetchDbInstance(
return { kind: 'ok', body: res.body }
} catch (e) {
// The platform answers a provider-shaped 502 for services with no manageable instance
// (Neon-backed): a soft case, not a failure. Everything else stays an error — an expired
// (the legacy Neon path — Neon is no longer used by any environment; this branch is retained,
// not live): a soft case, not a failure. Everything else stays an error — an expired
// token must not render as "no ceiling set" — but wrapped so the user sees what failed.
if (e instanceof ApiError && e.status === 502) return { kind: 'no-instance' }
if (e instanceof ApiError) throw new Error(`reading the instance failed (${e.status}): ${e.message}`)
Expand All @@ -85,7 +89,7 @@ export async function dbLimits(opts: Opts & { cpu?: string; memory?: string }):
if (!opts.cpu && !opts.memory) {
const read = await fetchDbInstance(api, p.projectId, suffix)
if (read.kind === 'no-instance') {
info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own resources)`)
info(`postgres ${opts.group ?? 'default'}: no manageable instance (this service manages its own resources)`)
return
}
if (opts.json) return printJson(read.body)
Expand Down Expand Up @@ -160,9 +164,11 @@ export function dbStatsLines(group: string, body: any): string[] {

// Point-in-time stats snapshot for a postgres service: connections vs the server's ceiling, cache
// hit rate, database size. Read-only. insta-db-backed: a suspended instance answers from the
// provider's control plane (shown as "(suspended)" with structural zeros), never dialed.
// Neon-backed: the platform reads over a direct SQL connection, so a one-shot call may wake a
// suspended endpoint — acceptable for an explicit command, which is why nothing here polls.
// provider's control plane (shown as "(suspended)" with structural zeros), never dialed. That is
// every environment today — the Neon-backed contrast below is historical: Neon is no longer used
// anywhere, and the code that handled it is retained, not live. Neon-backed: the platform read
// over a direct SQL connection, so a one-shot call could wake a suspended endpoint — acceptable
// for an explicit command, which is why nothing here polls.
export async function dbStats(opts: Opts): Promise<void> {
const api = await ApiClient.load()
const p = await requireProject()
Expand Down Expand Up @@ -202,7 +208,7 @@ export async function dbVolume(opts: Opts & { size?: string }): Promise<void> {
if (!opts.size) {
const read = await fetchDbInstance(api, p.projectId, suffix)
if (read.kind === 'no-instance') {
info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own storage)`)
info(`postgres ${opts.group ?? 'default'}: no manageable instance (this service manages its own storage)`)
return
}
if (opts.json) return printJson(read.body)
Expand Down
4 changes: 3 additions & 1 deletion src/commands/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ function printDimensions(dims: Dim[]): void {

// insta usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) for the
// current billing cycle. Shows the whole ORG by default (with a per-project breakdown); pass --proj
// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw fly/neon meters.
// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw provider
// meters. (Historical: those were fly/neon meters — Neon is no longer used by any environment,
// though the adapter code is retained, not live.)
export async function usage(opts: { from?: string; to?: string; json?: boolean; proj?: string | boolean }): Promise<void> {
const api = await ApiClient.load()
const p = await requireProject()
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ svc.command('set-access <type> <name> <access>').description('Set a storage serv
.option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o)))
svc.command('scale <type> <name> <number> [region]').description('Set a compute service machine count (paid plans only)')
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)))
svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
svc.command('upgrade <type> <name> <spec>').description('Change a compute service spec (paid plans only). Postgres upgrades are rejected by the platform — use `insta db limits` instead')
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)))
svc.command('secrets <type> <name>').description("List a service's secret names")
.option('--branch <b>').option('--json').action(guard((type, name, o) => services.servicesSecrets(type, name, o)))
Expand Down
13 changes: 8 additions & 5 deletions test/limits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ describe('fmtMib (display must not claim a ceiling the API did not set)', () =>
})
})

// The round-3 Critical: rawRequest THROWS on >=400, so the Neon soft-path must live in a catch —
// status-branching on its return value was unreachable dead code and a Neon read crashed with a
// raw ApiError. These drive the seam with a stub client, which is exactly the test that would
// have caught it (the 502 branch was never taken by any test).
// The round-3 Critical: rawRequest THROWS on >=400, so the no-instance soft-path must live in a
// catch — status-branching on its return value was unreachable dead code and the read crashed with
// a raw ApiError. These drive the seam with a stub client, which is exactly the test that would
// have caught it (the 502 branch was never taken by any test). That soft path is the legacy Neon
// path: Neon is no longer used by any environment, so these stay as retained coverage for code
// that is not live.
describe('fetchDbInstance (the read seam)', () => {
const stub = (fn: () => Promise<any>) => ({ rawRequest: fn }) as any

Expand All @@ -91,7 +93,8 @@ describe('fetchDbInstance (the read seam)', () => {
expect(read).toEqual({ kind: 'ok', body: { cpuMilli: 4000 } })
})

it('maps the provider-shaped 502 (Neon-backed) to the soft no-instance case', async () => {
// Legacy Neon path — Neon is no longer used by any environment; this test is retained coverage.
it('maps the provider-shaped 502 (legacy Neon-backed) to the soft no-instance case', async () => {
const read = await fetchDbInstance(stub(async () => { throw new ApiError(502, 'provider request failed') }), 'p1', '')
expect(read).toEqual({ kind: 'no-instance' })
})
Expand Down
Loading