@@ -10,7 +10,7 @@ import textTable from 'text-table';
1010import * as jsonc from 'jsonc-parser' ;
1111
1212import { createDockerParams , createLog , launch , ProvisionOptions } from './devContainers' ;
13- import { SubstitutedConfig , createContainerProperties , envListToObj , inspectDockerImage , isDockerFileConfig , SubstituteConfig , addSubstitution , findContainerAndIdLabels , getCacheFolder , runAsyncHandler } from './utils' ;
13+ import { SubstitutedConfig , createContainerProperties , envListToObj , inspectDockerImage , isDockerFileConfig , SubstituteConfig , addSubstitution , findContainerAndIdLabels , getCacheFolder , runAsyncHandler , BuildSecret } from './utils' ;
1414import { URI } from 'vscode-uri' ;
1515import { ContainerError } from '../spec-common/errors' ;
1616import { Log , LogDimensions , LogLevel , makeLog , mapLogLevel } from '../spec-utils/log' ;
@@ -50,6 +50,43 @@ const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell';
5050
5151const mountRegex = / ^ t y p e = ( b i n d | v o l u m e ) , s o u r c e = ( [ ^ , ] + ) , t a r g e t = ( [ ^ , ] + ) (?: , e x t e r n a l = ( t r u e | f a l s e ) ) ? $ / ;
5252
53+ function parseBuildSecrets ( buildSecretsArg : string [ ] | undefined ) : BuildSecret [ ] {
54+ if ( ! buildSecretsArg ) {
55+ return [ ] ;
56+ }
57+ const secrets = Array . isArray ( buildSecretsArg ) ? buildSecretsArg : [ buildSecretsArg ] ;
58+ return secrets . map ( secret => {
59+ // Support shorthand: id=name (assumes env=name)
60+ const shorthandMatch = secret . match ( / ^ i d = ( [ ^ , ] + ) $ / ) ;
61+ if ( shorthandMatch ) {
62+ return {
63+ id : shorthandMatch [ 1 ] ,
64+ env : shorthandMatch [ 1 ] . toUpperCase ( )
65+ } ;
66+ }
67+
68+ // Support file format: id=name,src=path
69+ const fileMatch = secret . match ( / ^ i d = ( [ ^ , ] + ) , s r c = ( .+ ) $ / ) ;
70+ if ( fileMatch ) {
71+ return {
72+ id : fileMatch [ 1 ] ,
73+ file : path . resolve ( process . cwd ( ) , fileMatch [ 2 ] )
74+ } ;
75+ }
76+
77+ // Support env format: id=name,env=VAR
78+ const envMatch = secret . match ( / ^ i d = ( [ ^ , ] + ) , e n v = ( .+ ) $ / ) ;
79+ if ( envMatch ) {
80+ return {
81+ id : envMatch [ 1 ] ,
82+ env : envMatch [ 2 ]
83+ } ;
84+ }
85+
86+ throw new Error ( `Invalid build-secret format: ${ secret } . Supported formats are: "id=<id>,src=<path>", "id=<id>,env=<var>", or "id=<id>" (which assumes env=<ID>).` ) ;
87+ } ) ;
88+ }
89+
5390( async ( ) => {
5491
5592 const packageFolder = path . join ( __dirname , '..' , '..' ) ;
@@ -137,6 +174,7 @@ function provisionOptions(y: Argv) {
137174 'container-session-data-folder' : { type : 'string' , description : 'Folder to cache CLI data, for example userEnvProbe results' } ,
138175 'omit-config-remote-env-from-metadata' : { type : 'boolean' , default : false , hidden : true , description : 'Omit remoteEnv from devcontainer.json for container metadata label' } ,
139176 'secrets-file' : { type : 'string' , description : 'Path to a json file containing secret environment variables as key-value pairs.' } ,
177+ 'build-secret' : { type : 'string' , description : 'Build secrets in the format id=<id>,src=<path>, id=<id>,env=<var>, or id=<id> (assumes env=<ID>). These will be passed as Docker build secrets to feature installation steps.' } ,
140178 'experimental-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Write lockfile' } ,
141179 'experimental-frozen-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Ensure lockfile remains unchanged' } ,
142180 'omit-syntax-directive' : { type : 'boolean' , default : false , hidden : true , description : 'Omit Dockerfile syntax directives' } ,
@@ -162,6 +200,10 @@ function provisionOptions(y: Argv) {
162200 if ( remoteEnvs ?. some ( remoteEnv => ! / .+ = .* / . test ( remoteEnv ) ) ) {
163201 throw new Error ( 'Unmatched argument format: remote-env must match <name>=<value>' ) ;
164202 }
203+ const buildSecrets = ( argv [ 'build-secret' ] && ( Array . isArray ( argv [ 'build-secret' ] ) ? argv [ 'build-secret' ] : [ argv [ 'build-secret' ] ] ) ) as string [ ] | undefined ;
204+ if ( buildSecrets ?. some ( buildSecret => ! / ^ i d = [ ^ , ] + ( , s r c = .+ | , e n v = .+ ) ? $ / . test ( buildSecret ) ) ) {
205+ throw new Error ( 'Unmatched argument format: build-secret must match id=<id>,src=<path>, id=<id>,env=<var>, or id=<id>' ) ;
206+ }
165207 return true ;
166208 } ) ;
167209}
@@ -211,6 +253,7 @@ async function provision({
211253 'container-session-data-folder' : containerSessionDataFolder ,
212254 'omit-config-remote-env-from-metadata' : omitConfigRemotEnvFromMetadata ,
213255 'secrets-file' : secretsFile ,
256+ 'build-secret' : buildSecret ,
214257 'experimental-lockfile' : experimentalLockfile ,
215258 'experimental-frozen-lockfile' : experimentalFrozenLockfile ,
216259 'omit-syntax-directive' : omitSyntaxDirective ,
@@ -223,6 +266,7 @@ async function provision({
223266 const addCacheFroms = addCacheFrom ? ( Array . isArray ( addCacheFrom ) ? addCacheFrom as string [ ] : [ addCacheFrom ] ) : [ ] ;
224267 const additionalFeatures = additionalFeaturesJson ? jsonc . parse ( additionalFeaturesJson ) as Record < string , string | boolean | Record < string , string | boolean > > : { } ;
225268 const providedIdLabels = idLabel ? Array . isArray ( idLabel ) ? idLabel as string [ ] : [ idLabel ] : undefined ;
269+ const buildSecrets = parseBuildSecrets ( buildSecret as string [ ] | undefined ) ;
226270
227271 const cwd = workspaceFolder || process . cwd ( ) ;
228272 const cliHost = await getCLIHost ( cwd , loadNativeModule , logFormat === 'text' ) ;
@@ -286,6 +330,7 @@ async function provision({
286330 omitSyntaxDirective,
287331 includeConfig,
288332 includeMergedConfig,
333+ buildSecrets,
289334 } ;
290335
291336 const result = await doProvision ( options , providedIdLabels ) ;
@@ -452,6 +497,7 @@ async function doSetUp({
452497 installCommand : dotfilesInstallCommand ,
453498 targetPath : dotfilesTargetPath ,
454499 } ,
500+ buildSecrets : [ ] ,
455501 } , disposables ) ;
456502
457503 const { common } = params ;
@@ -523,6 +569,7 @@ function buildOptions(y: Argv) {
523569 'additional-features' : { type : 'string' , description : 'Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)' } ,
524570 'skip-feature-auto-mapping' : { type : 'boolean' , default : false , hidden : true , description : 'Temporary option for testing.' } ,
525571 'skip-persisting-customizations-from-features' : { type : 'boolean' , default : false , hidden : true , description : 'Do not save customizations from referenced Features as image metadata' } ,
572+ 'build-secret' : { type : 'string' , description : 'Build secrets in the format id=<id>,src=<path>, id=<id>,env=<var>, or id=<id> (assumes env=<ID>). These will be passed as Docker build secrets to feature installation steps.' } ,
526573 'experimental-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Write lockfile' } ,
527574 'experimental-frozen-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Ensure lockfile remains unchanged' } ,
528575 'omit-syntax-directive' : { type : 'boolean' , default : false , hidden : true , description : 'Omit Dockerfile syntax directives' } ,
@@ -568,6 +615,7 @@ async function doBuild({
568615 'experimental-lockfile' : experimentalLockfile ,
569616 'experimental-frozen-lockfile' : experimentalFrozenLockfile ,
570617 'omit-syntax-directive' : omitSyntaxDirective ,
618+ 'build-secret' : buildSecret ,
571619} : BuildArgs ) {
572620 const disposables : ( ( ) => Promise < unknown > | undefined ) [ ] = [ ] ;
573621 const dispose = async ( ) => {
@@ -579,6 +627,7 @@ async function doBuild({
579627 const overrideConfigFile : URI | undefined = /* overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : */ undefined ;
580628 const addCacheFroms = addCacheFrom ? ( Array . isArray ( addCacheFrom ) ? addCacheFrom as string [ ] : [ addCacheFrom ] ) : [ ] ;
581629 const additionalFeatures = additionalFeaturesJson ? jsonc . parse ( additionalFeaturesJson ) as Record < string , string | boolean | Record < string , string | boolean > > : { } ;
630+ const buildSecrets = parseBuildSecrets ( buildSecret as string [ ] | undefined ) ;
582631 const params = await createDockerParams ( {
583632 dockerPath,
584633 dockerComposePath,
@@ -617,6 +666,7 @@ async function doBuild({
617666 experimentalLockfile,
618667 experimentalFrozenLockfile,
619668 omitSyntaxDirective,
669+ buildSecrets,
620670 } , disposables ) ;
621671
622672 const { common, dockerComposeCLI } = params ;
@@ -849,6 +899,7 @@ async function doRunUserCommands({
849899 const cwd = workspaceFolder || process . cwd ( ) ;
850900 const cliHost = await getCLIHost ( cwd , loadNativeModule , logFormat === 'text' ) ;
851901 const secretsP = readSecretsFromFile ( { secretsFile, cliHost } ) ;
902+ const buildSecrets : BuildSecret [ ] = [ ] ;
852903
853904 const params = await createDockerParams ( {
854905 dockerPath,
@@ -891,6 +942,7 @@ async function doRunUserCommands({
891942 } ,
892943 containerSessionDataFolder,
893944 secretsP,
945+ buildSecrets,
894946 } , disposables ) ;
895947
896948 const { common } = params ;
@@ -1333,7 +1385,8 @@ export async function doExec({
13331385 buildxOutput : undefined ,
13341386 skipPostAttach : false ,
13351387 skipPersistingCustomizationsFromFeatures : false ,
1336- dotfiles : { }
1388+ dotfiles : { } ,
1389+ buildSecrets : [ ]
13371390 } , disposables ) ;
13381391
13391392 const { common } = params ;
0 commit comments