Skip to content

Commit 1625172

Browse files
committed
fix(setup): warn about containers an earlier sim-setup exposed instead of replacing them
1 parent 41abe4a commit 1625172

4 files changed

Lines changed: 44 additions & 110 deletions

File tree

‎packages/sim-setup/src/db.ts‎

Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import { spawnSync } from 'node:child_process'
2-
import {
3-
DB_CONTAINER,
4-
type Detection,
5-
LOOPBACK_HOST,
6-
type PortBinding,
7-
publishedBinding,
8-
targetsLocalPort,
9-
} from './detect'
2+
import { DB_CONTAINER, type Detection, LOOPBACK_HOST } from './detect'
103
import { ensureDocker } from './docker'
114
import { generateSecret } from './env-files'
125
import { SetupError } from './errors'
@@ -43,8 +36,6 @@ function dockerOutput(args: string[]): string | null {
4336
interface ManagedContainer {
4437
running: boolean
4538
dsn: string
46-
password: string
47-
binding: PortBinding
4839
}
4940

5041
/**
@@ -72,16 +63,21 @@ function inspectManagedContainer(): ManagedContainer | null {
7263
?.slice('POSTGRES_PASSWORD='.length)
7364
if (!password) return null
7465

75-
const binding = publishedBinding(DB_CONTAINER, '5432/tcp')
76-
if (!binding) return null
66+
// `docker port` only reports a published port while the container runs; the
67+
// static config carries it either way.
68+
const hostPort = dockerOutput([
69+
'inspect',
70+
DB_CONTAINER,
71+
'--format',
72+
'{{(index .HostConfig.PortBindings "5432/tcp" 0).HostPort}}',
73+
])
74+
if (!hostPort) return null
7775

7876
return {
7977
running: running === 'true',
8078
// Read back from the container env verbatim, so it may be a password the
8179
// user supplied for an existing volume — encode it like any other.
82-
dsn: buildDsn(password, binding.hostPort),
83-
password,
84-
binding,
80+
dsn: buildDsn(password, hostPort),
8581
}
8682
}
8783

@@ -253,14 +249,6 @@ async function startManagedContainer(detection: Detection): Promise<string> {
253249
const password = volumeInitialized()
254250
? await resolveExistingVolume()
255251
: generateSecret().slice(0, 24)
256-
return launchManagedContainer(password, hostPort)
257-
}
258-
259-
/** Runs the managed container on `LOOPBACK_HOST` and waits until it accepts the password. */
260-
async function launchManagedContainer(
261-
password: string,
262-
hostPort: string | number
263-
): Promise<string> {
264252
// A user-supplied password can contain @ : / # — raw interpolation would
265253
// re-parse the DSN into a different host and fail a password that is correct.
266254
const dsn = buildDsn(password, hostPort)
@@ -321,41 +309,13 @@ async function launchManagedContainer(
321309
return dsn
322310
}
323311

324-
/**
325-
* Offers to re-publish a managed container that an earlier release bound to
326-
* every interface, keeping its password, port, and data volume. Returns the new
327-
* DSN when `existingDsn` pointed at that container (or nothing was configured),
328-
* so the env files move to the loopback address with it.
329-
*/
330-
async function rebindExposedContainer(existingDsn?: string): Promise<string | null> {
331-
const container = inspectManagedContainer()
332-
if (!container || container.binding.hostIp === LOOPBACK_HOST) return null
333-
const { hostPort } = container.binding
334-
const rebind = await p.confirm({
335-
message: `${DB_CONTAINER} publishes Postgres on every network interface (set up by an earlier sim-setup). Recreate it on ${LOOPBACK_HOST} only? Its data and password are kept.`,
336-
initialValue: true,
337-
})
338-
if (!rebind) {
339-
p.log.warn(`${DB_CONTAINER} stays reachable from your network on :${hostPort}.`)
340-
return null
341-
}
342-
docker(['rm', '-f', DB_CONTAINER])
343-
const dsn = await launchManagedContainer(container.password, hostPort)
344-
return !existingDsn || targetsLocalPort(existingDsn, hostPort) ? dsn : null
345-
}
346-
347312
/**
348313
* The mode-B database ladder: reuse a working DSN, offer (never silently adopt)
349314
* a Postgres already on 5432, start/reuse the wizard-managed pgvector
350315
* container, or take an external DSN. Adopting an existing database is always
351316
* an explicit choice — migrations run against whatever is chosen here.
352317
*/
353318
export async function resolveDatabase(detection: Detection, existingDsn?: string): Promise<string> {
354-
if (detection.dbContainer?.managed) {
355-
const rebound = await rebindExposedContainer(existingDsn)
356-
if (rebound) return rebound
357-
}
358-
359319
if (existingDsn && (await probeWithSpinner(existingDsn, 'Testing existing DATABASE_URL…'))) {
360320
return existingDsn
361321
}

‎packages/sim-setup/src/detect.ts‎

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,41 +16,41 @@ export const REDIS_CONTAINER = 'sim-redis'
1616
*/
1717
export const LOOPBACK_HOST = '127.0.0.1'
1818

19-
export interface PortBinding {
20-
/** Empty when Docker publishes on every interface. */
21-
hostIp: string
22-
hostPort: string
23-
}
24-
2519
/**
26-
* The host side of a container's published port, read from its static config so
27-
* a stopped container answers too. Containers from releases before
28-
* `LOOPBACK_HOST` was enforced report an empty or wildcard `hostIp`.
20+
* Whether a container publishes `containerPort` beyond `LOOPBACK_HOST`, read from
21+
* its static config so a stopped container answers too. Containers created
22+
* before managed ports were bound to loopback report an empty `HostIp`.
2923
*/
30-
export function publishedBinding(container: string, containerPort: string): PortBinding | null {
24+
function publishedBeyondLoopback(container: string, containerPort: string): boolean {
3125
const out = commandOutput('docker', [
3226
'inspect',
3327
container,
3428
'--format',
3529
`{{json (index .HostConfig.PortBindings "${containerPort}")}}`,
3630
])
37-
if (!out) return null
31+
if (!out) return false
3832
try {
39-
const [first] = JSON.parse(out) as { HostIp?: string; HostPort?: string }[]
40-
return first?.HostPort ? { hostIp: first.HostIp ?? '', hostPort: first.HostPort } : null
33+
const bindings = JSON.parse(out) as { HostIp?: string }[] | null
34+
return (bindings ?? []).some((binding) => binding.HostIp !== LOOPBACK_HOST)
4135
} catch {
42-
return null
36+
return false
4337
}
4438
}
4539

46-
/** Whether `url` addresses this machine on `port`, so it reaches a container published there. */
47-
export function targetsLocalPort(url: string, port: string): boolean {
48-
try {
49-
const { hostname, port: urlPort } = new URL(url)
50-
return ['localhost', LOOPBACK_HOST, '[::1]'].includes(hostname) && urlPort === port
51-
} catch {
52-
return false
40+
/**
41+
* Managed containers that an earlier sim-setup published on every interface.
42+
* They are reported, not recreated: Redis holds live realtime sessions and the
43+
* Postgres URL may name its own database, so replacing either is the user's call.
44+
*/
45+
export function exposedManagedContainers(detection: Detection): string[] {
46+
const exposed: string[] = []
47+
if (detection.dbContainer?.managed && publishedBeyondLoopback(DB_CONTAINER, '5432/tcp')) {
48+
exposed.push(DB_CONTAINER)
49+
}
50+
if (detection.redisContainer?.managed && publishedBeyondLoopback(REDIS_CONTAINER, '6379/tcp')) {
51+
exposed.push(REDIS_CONTAINER)
5352
}
53+
return exposed
5454
}
5555

5656
const SHELL_LLM_KEYS = [

‎packages/sim-setup/src/modes/dev.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { truncate } from '@sim/utils/string'
44
import { EMAIL_SETUP, JOBS_SETUP, STORAGE_SETUP } from '../capability-config'
55
import { promptCapabilitySetup, stageCapabilitySetupTransition } from '../capability-setup'
66
import { resolveDatabase } from '../db'
7-
import type { Detection } from '../detect'
7+
import { type Detection, exposedManagedContainers, LOOPBACK_HOST } from '../detect'
88
import { ROOT, readEnvFile, reconcileEnvValues, writeEnvValues } from '../env-files'
99
import { SetupError } from '../errors'
1010
import { pgProbe } from '../probes'
@@ -75,6 +75,12 @@ export async function runDevMode(
7575
detection: Detection,
7676
quick: boolean
7777
): Promise<{ startNow: boolean; script: string }> {
78+
const exposed = exposedManagedContainers(detection)
79+
if (exposed.length > 0) {
80+
p.log.warn(
81+
`${exposed.join(' and ')} ${exposed.length > 1 ? 'publish their ports' : 'publishes its port'} on every network interface (created by an earlier sim-setup). To keep ${exposed.length > 1 ? 'them' : 'it'} on ${LOOPBACK_HOST}, stop the app, run ${theme.command('npx sim-setup down')} (data is kept), then re-run ${theme.command('npx sim-setup')}.`
82+
)
83+
}
7884
const sim = readEnvFile('sim')
7985
const dsn = await resolveDatabase(detection, sim.vars.get('DATABASE_URL'))
8086
const secrets = collectSecrets(sim)

‎packages/sim-setup/src/redis.ts‎

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { spawnSync } from 'node:child_process'
22
import { docker } from './db'
3-
import {
4-
type Detection,
5-
LOOPBACK_HOST,
6-
publishedBinding,
7-
REDIS_CONTAINER,
8-
targetsLocalPort,
9-
} from './detect'
3+
import { type Detection, LOOPBACK_HOST, REDIS_CONTAINER } from './detect'
104
import { ensureDocker } from './docker'
115
import { SetupError } from './errors'
126
import { redisPing, waitFor } from './probes'
@@ -35,27 +29,8 @@ async function pingWithSpinner(url: string, label: string): Promise<boolean> {
3529
return ping.ok
3630
}
3731

38-
function defaultRedisPort(detection: Detection): number {
39-
return detection.redisPortOpen ? 6380 : 6379
40-
}
41-
42-
/**
43-
* Recreates a managed container that an earlier release published on every
44-
* interface, on the same port. It holds no data, so this needs no prompt.
45-
* Returns the new URL when `existing` pointed at it (or nothing was configured).
46-
*/
47-
async function rebindExposedRedis(detection: Detection, existing?: string): Promise<string | null> {
48-
if (!detection.redisContainer?.managed) return null
49-
const binding = publishedBinding(REDIS_CONTAINER, '6379/tcp')
50-
if (!binding || binding.hostIp === LOOPBACK_HOST) return null
51-
p.log.step(
52-
`Recreating ${REDIS_CONTAINER} on ${LOOPBACK_HOST}: an earlier sim-setup published it on every network interface`
53-
)
54-
const url = await startManagedRedis(binding.hostPort)
55-
return !existing || targetsLocalPort(existing, binding.hostPort) ? url : null
56-
}
57-
58-
async function startManagedRedis(hostPort: number | string): Promise<string> {
32+
async function startManagedRedis(detection: Detection): Promise<string> {
33+
const hostPort = detection.redisPortOpen ? 6380 : 6379
5934
const url = `redis://${LOOPBACK_HOST}:${hostPort}`
6035
// A prior sim-redis (unhealthy, or bound to a stale port) would collide on the
6136
// name. Unlike Postgres this container has no data volume, so removing it
@@ -113,8 +88,6 @@ async function promptRedisUrl(existing?: string): Promise<string> {
11388
* failing the whole setup.
11489
*/
11590
export async function ensureRedis(detection: Detection, existing?: string): Promise<string | null> {
116-
const rebound = await rebindExposedRedis(detection, existing)
117-
if (rebound) return rebound
11891
if (existing && (await redisPing(existing)).ok) return existing
11992
if (detection.redisPortOpen && (await redisPing(LOCAL_URL)).ok) {
12093
p.log.step(`Using the Redis already on :6379`)
@@ -134,17 +107,14 @@ export async function ensureRedis(detection: Detection, existing?: string): Prom
134107
)
135108
return null
136109
}
137-
return startManagedRedis(defaultRedisPort(detection))
110+
return startManagedRedis(detection)
138111
}
139112

140113
/**
141114
* Redis ladder, mirroring the Postgres one: reuse what's running (with
142115
* consent), restart/start a wizard-managed container, or take a URL.
143116
*/
144117
export async function resolveRedis(detection: Detection, existing?: string): Promise<string> {
145-
const rebound = await rebindExposedRedis(detection, existing)
146-
if (rebound) return rebound
147-
148118
// A configured REDIS_URL wins over the port scan: it may point at a non-default
149119
// port, or at a host that isn't this machine at all.
150120
if (
@@ -196,7 +166,5 @@ export async function resolveRedis(detection: Detection, existing?: string): Pro
196166
p.log.warn('Docker is not available, so the wizard cannot manage a Redis container for you.')
197167
}
198168
const choice = await p.select({ message: 'Where should Redis live?', options })
199-
return choice === 'container'
200-
? startManagedRedis(defaultRedisPort(detection))
201-
: promptRedisUrl(existing)
169+
return choice === 'container' ? startManagedRedis(detection) : promptRedisUrl(existing)
202170
}

0 commit comments

Comments
 (0)