@@ -11,8 +11,12 @@ const RELEASE = 'sim-dev'
1111const NAMESPACE = 'sim-dev'
1212const LOCAL_CONTEXT_PREFIXES = [ 'kind-' , 'docker-desktop' , 'minikube' , 'orbstack' ]
1313
14- function run ( command : string , args : string [ ] , failMessage : string ) : string {
15- const result = spawnSync ( command , args , { encoding : 'utf8' } )
14+ /**
15+ * `input` is piped on stdin rather than passed as arguments — argv is readable
16+ * by any process on the machine, so secrets must never travel that way.
17+ */
18+ function run ( command : string , args : string [ ] , failMessage : string , input ?: string ) : string {
19+ const result = spawnSync ( command , args , { encoding : 'utf8' , input } )
1620 if ( result . status !== 0 ) {
1721 throw new Error ( `${ failMessage } : ${ result . stderr . trim ( ) || result . stdout . trim ( ) } ` )
1822 }
@@ -65,11 +69,12 @@ async function ensureLocalContext(detection: Detection): Promise<string> {
6569 return 'kind-sim'
6670}
6771
68- function existingReleaseSecrets ( ) : Record < string , string > | null {
69- const status = spawnSync ( 'helm' , [ 'status' , RELEASE , '-n' , NAMESPACE ] , { stdio : 'ignore' } )
72+ function existingReleaseSecrets ( context : string ) : Record < string , string > | null {
73+ const scope = [ '--kube-context' , context , '-n' , NAMESPACE ]
74+ const status = spawnSync ( 'helm' , [ 'status' , RELEASE , ...scope ] , { stdio : 'ignore' } )
7075 if ( status . status !== 0 ) return null
7176 const values = JSON . parse (
72- run ( 'helm' , [ 'get' , 'values' , RELEASE , '-n' , NAMESPACE , '-o' , 'json' ] , 'helm get values failed' )
77+ run ( 'helm' , [ 'get' , 'values' , RELEASE , ... scope , '-o' , 'json' ] , 'helm get values failed' )
7378 ) as { app ?: { env ?: Record < string , string > } ; postgresql ?: { auth ?: { password ?: string } } }
7479 const env = values . app ?. env ?? { }
7580 const password = values . postgresql ?. auth ?. password
@@ -91,10 +96,27 @@ function existingReleaseSecrets(): Record<string, string> | null {
9196 }
9297}
9398
99+ /**
100+ * Values document piped to helm on stdin instead of `--set`. `JSON.stringify`
101+ * quotes and escapes each value — JSON is a subset of YAML, so a secret
102+ * containing `#`, `:`, or a leading `*` can neither break the document nor be
103+ * reinterpreted as YAML syntax.
104+ */
105+ function secretValues ( secrets : Record < string , string > ) : string {
106+ const { POSTGRES_PASSWORD , ...appEnv } = secrets
107+ const env = Object . entries ( appEnv )
108+ . map ( ( [ key , value ] ) => ` ${ key } : ${ JSON . stringify ( value ) } ` )
109+ . join ( '\n' )
110+ return `app:\n env:\n${ env } \npostgresql:\n auth:\n password: ${ JSON . stringify ( POSTGRES_PASSWORD ) } \n`
111+ }
112+
94113export async function runK8sMode ( detection : Detection ) : Promise < void > {
95- await ensureLocalContext ( detection )
114+ // Pin every subsequent call to the context we validated: the ambient context
115+ // can change between detection and deploy, which would send generated
116+ // credentials to an unintended cluster.
117+ const context = await ensureLocalContext ( detection )
96118
97- const reused = existingReleaseSecrets ( )
119+ const reused = existingReleaseSecrets ( context )
98120 const secrets = reused ?? {
99121 BETTER_AUTH_SECRET : generateSecret ( ) ,
100122 ENCRYPTION_KEY : generateSecret ( ) ,
@@ -114,26 +136,21 @@ export async function runK8sMode(detection: Detection): Promise<void> {
114136 '--install' ,
115137 RELEASE ,
116138 './helm/sim' ,
139+ '--kube-context' ,
140+ context ,
117141 '--namespace' ,
118142 NAMESPACE ,
119143 '--create-namespace' ,
120144 '--values' ,
121145 './helm/sim/examples/values-development.yaml' ,
122- '--set' ,
123- `app.env.BETTER_AUTH_SECRET=${ secrets . BETTER_AUTH_SECRET } ` ,
124- '--set' ,
125- `app.env.ENCRYPTION_KEY=${ secrets . ENCRYPTION_KEY } ` ,
126- '--set' ,
127- `app.env.INTERNAL_API_SECRET=${ secrets . INTERNAL_API_SECRET } ` ,
128- '--set' ,
129- `app.env.CRON_SECRET=${ secrets . CRON_SECRET } ` ,
130- '--set' ,
131- `postgresql.auth.password=${ secrets . POSTGRES_PASSWORD } ` ,
146+ '--values' ,
147+ '-' ,
132148 '--wait' ,
133149 '--timeout' ,
134150 '15m' ,
135151 ] ,
136- 'helm upgrade --install failed'
152+ 'helm upgrade --install failed' ,
153+ secretValues ( secrets )
137154 )
138155 } catch ( error ) {
139156 spin . stop ( `${ theme . error ( '✗' ) } helm install failed` )
@@ -147,7 +164,7 @@ export async function runK8sMode(detection: Detection): Promise<void> {
147164
148165 const testSpin = p . spinner ( )
149166 testSpin . start ( 'Running helm test…' )
150- const test = spawnSync ( 'helm' , [ 'test' , RELEASE , '-n' , NAMESPACE ] , {
167+ const test = spawnSync ( 'helm' , [ 'test' , RELEASE , '--kube-context' , context , '- n', NAMESPACE ] , {
151168 encoding : 'utf8' ,
152169 cwd : ROOT ,
153170 } )
@@ -162,9 +179,9 @@ export async function runK8sMode(detection: Detection): Promise<void> {
162179
163180 p . note (
164181 [
165- `kubectl -n ${ NAMESPACE } port-forward svc/${ RELEASE } -app 3000:3000` ,
166- `kubectl -n ${ NAMESPACE } get pods` ,
167- `helm uninstall ${ RELEASE } -n ${ NAMESPACE } # tear down` ,
182+ `kubectl --context ${ context } - n ${ NAMESPACE } port-forward svc/${ RELEASE } -app 3000:3000` ,
183+ `kubectl --context ${ context } - n ${ NAMESPACE } get pods` ,
184+ `helm uninstall ${ RELEASE } --kube-context ${ context } - n ${ NAMESPACE } # tear down` ,
168185 ] . join ( '\n' ) ,
169186 'Reach your cluster'
170187 )
0 commit comments