@@ -50,6 +50,7 @@ export function runtimeServiceEvidenceFromError(error: unknown): RuntimeServiceE
5050interface ManagedRuntimeService {
5151 env : Record < string , string >
5252 secretEnv : Record < string , string >
53+ secretEnvTargets : Record < string , string >
5354 evidence : RuntimeServiceEvidence
5455 release ( ) : Promise < void >
5556 control ( action : RuntimeServiceControlAction , options ?: Record < string , unknown > ) : Promise < RuntimeServiceControlResult >
@@ -66,6 +67,7 @@ export interface RuntimeServiceProvider {
6667 readonly name : string
6768 readonly kind : string
6869 version ( service : WorkspaceRecipeRuntimeService ) : string
70+ secretEnvTargets ( service : WorkspaceRecipeRuntimeService ) : Record < string , string >
6971 provision ( service : WorkspaceRecipeRuntimeService , dependencies : RuntimeServiceDependencies , context : RuntimeServiceProvisionContext , evidence : RuntimeServiceEvidence [ ] ) : Promise < ManagedRuntimeService >
7072}
7173
@@ -82,6 +84,7 @@ export interface ProvisionRuntimeServicesOptions {
8284 policy ?: RuntimePolicy
8385 externalServices ?: WorkspaceRecipeExternalServiceBoundary [ ]
8486 externalServiceWritesApproved ?: boolean
87+ reservedEnvNames ?: readonly string [ ]
8588}
8689
8790const defaultDependencies : RuntimeServiceDependencies = {
@@ -99,21 +102,24 @@ export function runtimeServicePlan(services: WorkspaceRecipeRuntimeService[]): A
99102 } )
100103}
101104
102- export async function provisionRuntimeServices ( services : WorkspaceRecipeRuntimeService [ ] , options : ProvisionRuntimeServicesOptions = { } ) : Promise < { env : Record < string , string > ; secretEnv : Record < string , string > ; evidence : RuntimeServiceEvidence [ ] ; control ( serviceId : string , action : RuntimeServiceControlAction , controlOptions ?: Record < string , unknown > ) : Promise < RuntimeServiceControlResult > ; release ( ) : Promise < void > } > {
105+ export async function provisionRuntimeServices ( services : WorkspaceRecipeRuntimeService [ ] , options : ProvisionRuntimeServicesOptions = { } ) : Promise < { env : Record < string , string > ; secretEnv : Record < string , string > ; secretEnvTargets : Record < string , string > ; evidence : RuntimeServiceEvidence [ ] ; control ( serviceId : string , action : RuntimeServiceControlAction , controlOptions ?: Record < string , unknown > ) : Promise < RuntimeServiceControlResult > ; release ( ) : Promise < void > } > {
103106 const dependencies = options . dependencies ?? defaultDependencies
104107 const provisioned : ManagedRuntimeService [ ] = [ ]
105108 const evidence : RuntimeServiceEvidence [ ] = [ ]
109+ let environment : ReturnType < typeof aggregateRuntimeServiceEnvironment >
106110 const context : RuntimeServiceProvisionContext = {
107111 signal : options . signal ,
108112 policy : options . policy ,
109113 externalServices : options . externalServices ?? [ ] ,
110114 externalServiceWritesApproved : options . externalServiceWritesApproved ?? false ,
111115 }
112116 try {
117+ validateDeclaredRuntimeServiceSecretTargets ( services , options . reservedEnvNames ?? [ ] )
113118 for ( const service of services ) {
114119 const managed = await runtimeServiceProvider ( service ) . provision ( service , dependencies , context , evidence )
115120 provisioned . push ( managed )
116121 }
122+ environment = aggregateRuntimeServiceEnvironment ( provisioned , options . reservedEnvNames ?? [ ] )
117123 } catch ( error ) {
118124 await releaseServices ( provisioned ) . catch ( ( ) => undefined )
119125 if ( error instanceof RuntimeServiceProvisionError ) throw error
@@ -126,8 +132,7 @@ export async function provisionRuntimeServices(services: WorkspaceRecipeRuntimeS
126132 const lease = provisioned . length > 0 ? setInterval ( ( ) => undefined , 1_000 ) : undefined
127133
128134 return {
129- env : Object . assign ( { } , ...provisioned . map ( ( service ) => service . env ) ) ,
130- secretEnv : Object . assign ( { } , ...provisioned . map ( ( service ) => service . secretEnv ) ) ,
135+ ...environment ,
131136 evidence,
132137 async control ( serviceId , action , controlOptions ) {
133138 const service = provisioned . find ( ( candidate ) => candidate . evidence . id === serviceId )
@@ -159,6 +164,7 @@ export async function provisionRuntimeServicesForRecipe(
159164 policy : options . policy ,
160165 externalServices : options . externalServices ,
161166 externalServiceWritesApproved : options . externalServiceWritesApproved ,
167+ reservedEnvNames : options . reservedEnvNames ,
162168 } )
163169 try {
164170 return await guard ( provisioning )
@@ -185,24 +191,30 @@ const mysqlDockerProvider: RuntimeServiceProvider = {
185191 name : "docker" ,
186192 kind : "mysql" ,
187193 version : mysqlDockerImage ,
194+ secretEnvTargets : mysqlRuntimeServiceSecretTargets ,
188195 provision : provisionMysqlDockerService ,
189196}
190197
191198const mysqlExternalProvider : RuntimeServiceProvider = {
192199 name : "external" ,
193200 kind : "mysql" ,
194201 version : ( service ) => `mysql-compatible:${ service . configuration ?. engine ?? "mysql" } ` ,
202+ secretEnvTargets : mysqlRuntimeServiceSecretTargets ,
195203 provision : provisionMysqlExternalService ,
196204}
197205
198- const redisDockerProvider : RuntimeServiceProvider = { name : "docker" , kind : "redis" , version : ( service ) => service . configuration ?. image ?? SERVICE_IMAGES . redis , provision : provisionRedisDockerService }
199- const smtpDockerProvider : RuntimeServiceProvider = { name : "docker" , kind : "smtp" , version : ( service ) => service . configuration ?. image ?? SERVICE_IMAGES . smtp , provision : provisionSmtpDockerService }
200- const httpDockerProvider : RuntimeServiceProvider = { name : "docker" , kind : "http" , version : ( service ) => service . configuration ?. image ?? SERVICE_IMAGES . http , provision : provisionHttpDockerService }
206+ const redisDockerProvider : RuntimeServiceProvider = { name : "docker" , kind : "redis" , version : ( service ) => service . configuration ?. image ?? SERVICE_IMAGES . redis , secretEnvTargets : ( ) => ( { } ) , provision : provisionRedisDockerService }
207+ const smtpDockerProvider : RuntimeServiceProvider = { name : "docker" , kind : "smtp" , version : ( service ) => service . configuration ?. image ?? SERVICE_IMAGES . smtp , secretEnvTargets : ( ) => ( { } ) , provision : provisionSmtpDockerService }
208+ const httpDockerProvider : RuntimeServiceProvider = { name : "docker" , kind : "http" , version : ( service ) => service . configuration ?. image ?? SERVICE_IMAGES . http , secretEnvTargets : ( ) => ( { } ) , provision : provisionHttpDockerService }
201209
202210function mysqlDockerImage ( service : WorkspaceRecipeRuntimeService ) : string {
203211 return MYSQL_IMAGES [ service . configuration ?. engine ?? "mysql" ]
204212}
205213
214+ function mysqlRuntimeServiceSecretTargets ( service : WorkspaceRecipeRuntimeService ) : Record < string , string > {
215+ return service . outputs . password ? { DB_PASSWORD : service . outputs . password } : { }
216+ }
217+
206218function runtimeServiceProvider ( service : WorkspaceRecipeRuntimeService ) : RuntimeServiceProvider {
207219 if ( service . kind === mysqlDockerProvider . kind ) return service . configuration ?. provider === "external" ? mysqlExternalProvider : mysqlDockerProvider
208220 if ( service . configuration ?. provider === "external" ) throw new Error ( `Managed runtime service kind does not support the external provider: ${ service . kind } ` )
@@ -243,8 +255,9 @@ async function provisionMysqlDockerService(service: WorkspaceRecipeRuntimeServic
243255 evidence . lifecycle = "provisioned"
244256 const values : Record < string , string > = { host : "127.0.0.1" , port : String ( port ) , username : "runtime" , password, database : "runtime" }
245257 return {
246- env : Object . fromEntries ( Object . entries ( service . outputs ) . map ( ( [ output , name ] ) => [ name , values [ output ] ?? " "] ) ) ,
258+ env : runtimeServiceOutputEnvironment ( service , values , new Set ( [ "password "] ) ) ,
247259 secretEnv : service . outputs . password ? { [ service . outputs . password ] : password } : { } ,
260+ secretEnvTargets : mysqlRuntimeServiceSecretTargets ( service ) ,
248261 evidence,
249262 async control ( action , options ) { return await controlDockerService ( container , evidence , dependencies , action , options , async ( customAction ) => {
250263 if ( customAction === "flush" ) {
@@ -352,8 +365,9 @@ async function provisionMysqlExternalService(service: WorkspaceRecipeRuntimeServ
352365 evidence . lifecycle = "provisioned"
353366 const values : Record < string , string > = { host : connection . host , port : String ( connection . port ) , username, password, database }
354367 return {
355- env : Object . fromEntries ( Object . entries ( service . outputs ) . map ( ( [ output , name ] ) => [ name , values [ output ] ?? " "] ) ) ,
368+ env : runtimeServiceOutputEnvironment ( service , values , new Set ( [ "password "] ) ) ,
356369 secretEnv : service . outputs . password ? { [ service . outputs . password ] : password } : { } ,
370+ secretEnvTargets : mysqlRuntimeServiceSecretTargets ( service ) ,
357371 evidence,
358372 async control ( action ) {
359373 const result : RuntimeServiceControlResult = { serviceId : service . id , action, status : "unsupported" , fidelity : "unsupported" , reason : `The ${ service . kind } provider does not support ${ action } .` }
@@ -419,6 +433,62 @@ function mysqlConnectionArgs(host: string, port: number, username: string): stri
419433 return [ "--batch" , "--skip-column-names" , "--protocol=TCP" , "--host" , host , "--port" , String ( port ) , "--user" , username ]
420434}
421435
436+ function runtimeServiceOutputEnvironment ( service : WorkspaceRecipeRuntimeService , values : Record < string , string > , secretOutputs : ReadonlySet < string > = new Set ( ) ) : Record < string , string > {
437+ return Object . fromEntries ( Object . entries ( service . outputs )
438+ . filter ( ( [ output ] ) => ! secretOutputs . has ( output ) )
439+ . map ( ( [ output , name ] ) => [ name , values [ output ] ?? "" ] ) )
440+ }
441+
442+ function validateDeclaredRuntimeServiceSecretTargets ( services : readonly WorkspaceRecipeRuntimeService [ ] , reservedEnvNames : readonly string [ ] ) : void {
443+ const reserved = new Set ( reservedEnvNames )
444+ const outputOwners = new Map < string , Array < { serviceIndex : number ; output : string } > > ( )
445+ for ( const [ serviceIndex , service ] of services . entries ( ) ) {
446+ for ( const [ output , name ] of Object . entries ( service . outputs ) ) {
447+ const owners = outputOwners . get ( name ) ?? [ ]
448+ owners . push ( { serviceIndex, output } )
449+ outputOwners . set ( name , owners )
450+ }
451+ }
452+ const targets = new Map < string , string > ( )
453+ for ( const [ serviceIndex , service ] of services . entries ( ) ) {
454+ for ( const [ target , source ] of Object . entries ( runtimeServiceProvider ( service ) . secretEnvTargets ( service ) ) ) {
455+ if ( reserved . has ( target ) ) throw new Error ( `Managed runtime service secret target is reserved by injected environment: ${ target } ` )
456+ const conflictingOutput = ( outputOwners . get ( target ) ?? [ ] ) . some ( ( owner ) => ! ( owner . serviceIndex === serviceIndex && owner . output === "password" && source === target ) )
457+ if ( conflictingOutput ) throw new Error ( `Managed runtime service secret target collides with managed output: ${ target } ` )
458+ if ( targets . has ( target ) ) throw new Error ( `Managed runtime service secret target is ambiguous: ${ target } ` )
459+ targets . set ( target , source )
460+ }
461+ }
462+ }
463+
464+ function aggregateRuntimeServiceEnvironment ( services : readonly ManagedRuntimeService [ ] , reservedEnvNames : readonly string [ ] ) : { env : Record < string , string > ; secretEnv : Record < string , string > ; secretEnvTargets : Record < string , string > } {
465+ const env : Record < string , string > = { }
466+ const secretEnv : Record < string , string > = { }
467+ const secretEnvTargets : Record < string , string > = { }
468+ for ( const service of services ) {
469+ mergeUniqueEnvironment ( env , service . env , "runtime service environment" )
470+ mergeUniqueEnvironment ( secretEnv , service . secretEnv , "runtime service secret environment" )
471+ for ( const [ target , source ] of Object . entries ( service . secretEnvTargets ) ) {
472+ const existing = secretEnvTargets [ target ]
473+ if ( existing !== undefined && existing !== source ) throw new Error ( `Managed runtime service secret target is ambiguous: ${ target } ` )
474+ secretEnvTargets [ target ] = source
475+ }
476+ }
477+ for ( const [ target , source ] of Object . entries ( secretEnvTargets ) ) {
478+ if ( ! ( source in secretEnv ) ) throw new Error ( `Managed runtime service secret target references an unavailable secret: ${ target } ` )
479+ if ( target in env ) throw new Error ( `Managed runtime service secret target collides with non-secret environment: ${ target } ` )
480+ if ( reservedEnvNames . includes ( target ) ) throw new Error ( `Managed runtime service secret target is reserved by injected environment: ${ target } ` )
481+ }
482+ return { env, secretEnv, secretEnvTargets }
483+ }
484+
485+ function mergeUniqueEnvironment ( target : Record < string , string > , source : Record < string , string > , label : string ) : void {
486+ for ( const [ name , value ] of Object . entries ( source ) ) {
487+ if ( name in target ) throw new Error ( `Duplicate ${ label } name: ${ name } ` )
488+ target [ name ] = value
489+ }
490+ }
491+
422492function validateGeneratedMysqlIdentifier ( identifier : string ) : string {
423493 if ( ! / ^ [ a - z ] [ a - z 0 - 9 _ ] { 0 , 63 } $ / . test ( identifier ) ) throw new Error ( "Generated MySQL isolation identifier is unsafe" )
424494 return identifier
@@ -501,6 +571,7 @@ async function provisionSimpleDockerService(
501571 return {
502572 env : Object . fromEntries ( Object . entries ( service . outputs ) . map ( ( [ output , name ] ) => [ name , values [ output ] ?? "" ] ) ) ,
503573 secretEnv : { } ,
574+ secretEnvTargets : { } ,
504575 evidence,
505576 async control ( action , options ) { return await controlDockerService ( container , evidence , dependencies , action , options , spec . customControl ? async ( candidate , candidateOptions ) => await spec . customControl ?.( container , candidate , candidateOptions ) ?? false : undefined , async ( ) => await dependencies . waitForReady ( "127.0.0.1" , ports [ 0 ] as number , 30_000 ) ) } ,
506577 async release ( ) { await releaseService ( container , evidence , dependencies ) } ,
0 commit comments