Skip to content

Commit f2c1922

Browse files
committed
fix(sim-setup): leave a shell-exported POSTGRES_PASSWORD to the operator
Compose interpolates a shell value over .env, so the wizard no longer copies it into .env and no longer needs rules for values .env or DATABASE_URL would change. An empty export is still refused, since Compose would use it and refuse to start.
1 parent ffc77b7 commit f2c1922

2 files changed

Lines changed: 19 additions & 92 deletions

File tree

‎packages/sim-setup/src/compose-database.test.ts‎

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
LEGACY_POSTGRES_PASSWORD,
99
postgresUser,
1010
} from './compose-database'
11-
import { SetupError } from './errors'
1211

1312
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
1413
const COMPOSE_FILES = [
@@ -62,27 +61,15 @@ describe('choosePostgresPassword', () => {
6261
).toEqual({ value: LEGACY_POSTGRES_PASSWORD, source: 'legacy' })
6362
})
6463

65-
it('persists a shell-only password, which is what Compose is using', () => {
64+
it('leaves a shell-exported password to the operator, since Compose uses it', () => {
6665
const hasDatabaseVolume = vi.fn(() => true)
6766
expect(
6867
choosePostgresPassword(undefined, 'sim-abc', {
6968
shell: { POSTGRES_PASSWORD: 'in-shell' },
7069
hasDatabaseVolume,
7170
})
72-
).toEqual({ value: 'in-shell', source: 'environment' })
73-
expect(hasDatabaseVolume).not.toHaveBeenCalled()
74-
})
75-
76-
it('accepts a shell password that matches .env', () => {
77-
expect(
78-
choosePostgresPassword('same', 'sim-abc', { shell: { POSTGRES_PASSWORD: 'same' } })
7971
).toBeNull()
80-
})
81-
82-
it('refuses a shell password that differs from .env', () => {
83-
expect(() =>
84-
choosePostgresPassword('in-env-file', 'sim-abc', { shell: { POSTGRES_PASSWORD: 'other' } })
85-
).toThrow(SetupError)
72+
expect(hasDatabaseVolume).not.toHaveBeenCalled()
8673
})
8774

8875
it('refuses an empty shell export, which Compose would use over .env', () => {
@@ -93,38 +80,11 @@ describe('choosePostgresPassword', () => {
9380
}
9481
})
9582

96-
it.each([
97-
'pa ss',
98-
'pass#word',
99-
'pa"ss',
100-
"pa'ss",
101-
'pa\\ss',
102-
'pa$ss',
103-
'pa/ss',
104-
'pa?ss',
105-
'pa%ss',
106-
'pa@ss',
107-
'pa:ss',
108-
])('refuses to persist %s, which .env or DATABASE_URL would change', (value) => {
109-
expect(() =>
110-
choosePostgresPassword(undefined, 'sim-abc', {
111-
shell: { POSTGRES_PASSWORD: value },
112-
hasDatabaseVolume: () => false,
113-
})
114-
).toThrow(SetupError)
115-
})
116-
117-
it('persists a shell password made of URL-unreserved characters', () => {
118-
expect(
119-
choosePostgresPassword(undefined, 'sim-abc', { shell: { POSTGRES_PASSWORD: 'Ab9._~-z' } })
120-
).toEqual({ value: 'Ab9._~-z', source: 'environment' })
121-
})
122-
12383
it('reads the process environment by default', () => {
12484
vi.stubEnv('POSTGRES_PASSWORD', 'from-process')
125-
expect(choosePostgresPassword(undefined, 'sim-abc', { hasDatabaseVolume: () => true })).toEqual(
126-
{ value: 'from-process', source: 'environment' }
127-
)
85+
expect(
86+
choosePostgresPassword(undefined, 'sim-abc', { hasDatabaseVolume: () => true })
87+
).toBeNull()
12888
})
12989
})
13090

‎packages/sim-setup/src/compose-database.ts‎

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,13 @@ export function composeFileRequiresPostgresPassword(composeFile: string): boolea
2525
}
2626

2727
/** Where a chosen `POSTGRES_PASSWORD` came from. */
28-
export type PostgresPasswordSource = 'environment' | 'generated' | 'legacy'
28+
export type PostgresPasswordSource = 'generated' | 'legacy'
2929

3030
export interface PostgresPasswordChoice {
3131
value: string
3232
source: PostgresPasswordSource
3333
}
3434

35-
/**
36-
* A password the wizard can persist verbatim: Compose interpolates it unescaped
37-
* into `DATABASE_URL`, and `.env` reinterprets whitespace, comments, quotes and
38-
* `$`, so only URL-unreserved characters survive both unchanged.
39-
*/
40-
const PERSISTABLE_PASSWORD = /^[A-Za-z0-9._~-]+$/
41-
4235
interface ChooseOptions {
4336
/** The shell environment, whose `POSTGRES_PASSWORD` Compose interpolates over `.env`. */
4437
shell?: NodeJS.ProcessEnv
@@ -47,57 +40,35 @@ interface ChooseOptions {
4740

4841
/**
4942
* Picks the `POSTGRES_PASSWORD` to write to a Compose install's `.env`, or null
50-
* when `.env` already has the right one. The production Compose file requires
51-
* the variable, and the value must match what the data volume was created with —
43+
* when nothing should be written. The production Compose file requires the
44+
* variable, and the value must match what the data volume was created with —
5245
* Postgres ignores `POSTGRES_PASSWORD` on an existing data directory, so a
5346
* wrong value locks the app out of its own database:
5447
*
55-
* - A value exported in the shell is what Compose is using. It is persisted when
56-
* `.env` has none, so a later run without the export does not fall back to a
57-
* guess. An empty export, one that differs from `.env`, or one `.env` cannot
58-
* hold verbatim is refused: which value the volume was created with cannot be
59-
* known, and either silent choice can lock the app out.
60-
* - With no value anywhere, an existing volume was created with the legacy
61-
* password, and a project with no volume yet gets a generated one.
48+
* - A value exported in the shell is the operator's to manage: Compose
49+
* interpolates it over `.env`, so nothing is written. An empty export is
50+
* refused, because Compose would use the empty value and refuse to start
51+
* however good the one in `.env` is.
52+
* - Otherwise a value in `.env` stands, an existing volume was created with the
53+
* legacy password the file used to default to, and a project with no volume
54+
* yet gets a generated one.
6255
*/
6356
export function choosePostgresPassword(
6457
envFileValue: string | undefined,
6558
project: string,
6659
{ shell = process.env, hasDatabaseVolume = composeDatabaseVolumeExists }: ChooseOptions = {}
6760
): PostgresPasswordChoice | null {
6861
const shellValue = shell.POSTGRES_PASSWORD
69-
if (shellValue === undefined) {
70-
if (envFileValue) return null
71-
return hasDatabaseVolume(project)
72-
? { value: LEGACY_POSTGRES_PASSWORD, source: 'legacy' }
73-
: { value: generateSecret(), source: 'generated' }
74-
}
7562
if (shellValue === '') {
7663
throw new SetupError(
7764
'POSTGRES_PASSWORD is exported but empty, and Compose uses it over .env.',
7865
['unset it (unset POSTGRES_PASSWORD) so the value in .env applies']
7966
)
8067
}
81-
if (envFileValue) {
82-
if (envFileValue === shellValue) return null
83-
throw new SetupError(
84-
'POSTGRES_PASSWORD in the shell differs from the one in .env, so it is unclear which one the database was created with.',
85-
[
86-
'unset the exported POSTGRES_PASSWORD if .env holds the database password',
87-
'or set the exported value in .env if that is the database password',
88-
]
89-
)
90-
}
91-
if (!PERSISTABLE_PASSWORD.test(shellValue)) {
92-
throw new SetupError(
93-
'POSTGRES_PASSWORD is exported only in the shell, and contains characters that cannot be stored in .env and embedded in DATABASE_URL unchanged.',
94-
[
95-
'use only letters, digits and . _ ~ - (for a new install: openssl rand -hex 24)',
96-
'or add it to .env yourself if you have confirmed it works in a connection URL',
97-
]
98-
)
99-
}
100-
return { value: shellValue, source: 'environment' }
68+
if (shellValue !== undefined || envFileValue) return null
69+
return hasDatabaseVolume(project)
70+
? { value: LEGACY_POSTGRES_PASSWORD, source: 'legacy' }
71+
: { value: generateSecret(), source: 'generated' }
10172
}
10273

10374
/** Whether a Compose project already has a Postgres data volume, found by Compose's own labels. */
@@ -159,10 +130,6 @@ export function reportPostgresPasswordChoice(
159130
choice: PostgresPasswordChoice,
160131
{ compose, user, envPath }: { compose: string; user: string; envPath: string }
161132
): void {
162-
if (choice.source === 'environment') {
163-
p.log.step(`Saved POSTGRES_PASSWORD from the shell environment to ${envPath}`)
164-
return
165-
}
166133
if (choice.source === 'generated') {
167134
p.log.step(`Generated POSTGRES_PASSWORD in ${envPath}`)
168135
return

0 commit comments

Comments
 (0)