@@ -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 , '..' , '..' ) ;
@@ -138,6 +175,7 @@ function provisionOptions(y: Argv) {
138175 'container-session-data-folder' : { type : 'string' , description : 'Folder to cache CLI data, for example userEnvProbe results' } ,
139176 'omit-config-remote-env-from-metadata' : { type : 'boolean' , default : false , hidden : true , description : 'Omit remoteEnv from devcontainer.json for container metadata label' } ,
140177 'secrets-file' : { type : 'string' , description : 'Path to a json file containing secret environment variables as key-value pairs.' } ,
178+ '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.' } ,
141179 'experimental-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Write lockfile' } ,
142180 'experimental-frozen-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Ensure lockfile remains unchanged' } ,
143181 'omit-syntax-directive' : { type : 'boolean' , default : false , hidden : true , description : 'Omit Dockerfile syntax directives' } ,
@@ -161,6 +199,10 @@ function provisionOptions(y: Argv) {
161199 if ( remoteEnvs ?. some ( remoteEnv => ! / .+ = .* / . test ( remoteEnv ) ) ) {
162200 throw new Error ( 'Unmatched argument format: remote-env must match <name>=<value>' ) ;
163201 }
202+ const buildSecrets = ( argv [ 'build-secret' ] && ( Array . isArray ( argv [ 'build-secret' ] ) ? argv [ 'build-secret' ] : [ argv [ 'build-secret' ] ] ) ) as string [ ] | undefined ;
203+ if ( buildSecrets ?. some ( buildSecret => ! / ^ i d = [ ^ , ] + ( , s r c = .+ | , e n v = .+ ) ? $ / . test ( buildSecret ) ) ) {
204+ throw new Error ( 'Unmatched argument format: build-secret must match id=<id>,src=<path>, id=<id>,env=<var>, or id=<id>' ) ;
205+ }
164206 return true ;
165207 } ) ;
166208}
@@ -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' ) ;
@@ -287,6 +331,7 @@ async function provision({
287331 omitSyntaxDirective,
288332 includeConfig,
289333 includeMergedConfig,
334+ buildSecrets,
290335 } ;
291336
292337 const result = await doProvision ( options , providedIdLabels ) ;
@@ -454,6 +499,7 @@ async function doSetUp({
454499 installCommand : dotfilesInstallCommand ,
455500 targetPath : dotfilesTargetPath ,
456501 } ,
502+ buildSecrets : [ ] ,
457503 } , disposables ) ;
458504
459505 const { common } = params ;
@@ -525,6 +571,7 @@ function buildOptions(y: Argv) {
525571 'additional-features' : { type : 'string' , description : 'Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)' } ,
526572 'skip-feature-auto-mapping' : { type : 'boolean' , default : false , hidden : true , description : 'Temporary option for testing.' } ,
527573 'skip-persisting-customizations-from-features' : { type : 'boolean' , default : false , hidden : true , description : 'Do not save customizations from referenced Features as image metadata' } ,
574+ '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.' } ,
528575 'experimental-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Write lockfile' } ,
529576 'experimental-frozen-lockfile' : { type : 'boolean' , default : false , hidden : true , description : 'Ensure lockfile remains unchanged' } ,
530577 'omit-syntax-directive' : { type : 'boolean' , default : false , hidden : true , description : 'Omit Dockerfile syntax directives' } ,
@@ -570,6 +617,7 @@ async function doBuild({
570617 'experimental-lockfile' : experimentalLockfile ,
571618 'experimental-frozen-lockfile' : experimentalFrozenLockfile ,
572619 'omit-syntax-directive' : omitSyntaxDirective ,
620+ 'build-secret' : buildSecret ,
573621} : BuildArgs ) {
574622 const disposables : ( ( ) => Promise < unknown > | undefined ) [ ] = [ ] ;
575623 const dispose = async ( ) => {
@@ -581,6 +629,7 @@ async function doBuild({
581629 const overrideConfigFile : URI | undefined = /* overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : */ undefined ;
582630 const addCacheFroms = addCacheFrom ? ( Array . isArray ( addCacheFrom ) ? addCacheFrom as string [ ] : [ addCacheFrom ] ) : [ ] ;
583631 const additionalFeatures = additionalFeaturesJson ? jsonc . parse ( additionalFeaturesJson ) as Record < string , string | boolean | Record < string , string | boolean > > : { } ;
632+ const buildSecrets = parseBuildSecrets ( buildSecret as string [ ] | undefined ) ;
584633 const params = await createDockerParams ( {
585634 dockerPath,
586635 dockerComposePath,
@@ -620,6 +669,7 @@ async function doBuild({
620669 experimentalLockfile,
621670 experimentalFrozenLockfile,
622671 omitSyntaxDirective,
672+ buildSecrets,
623673 } , disposables ) ;
624674
625675 const { common, dockerComposeCLI } = params ;
@@ -854,6 +904,7 @@ async function doRunUserCommands({
854904 const cwd = workspaceFolder || process . cwd ( ) ;
855905 const cliHost = await getCLIHost ( cwd , loadNativeModule , logFormat === 'text' ) ;
856906 const secretsP = readSecretsFromFile ( { secretsFile, cliHost } ) ;
907+ const buildSecrets : BuildSecret [ ] = [ ] ;
857908
858909 const params = await createDockerParams ( {
859910 dockerPath,
@@ -897,6 +948,7 @@ async function doRunUserCommands({
897948 } ,
898949 containerSessionDataFolder,
899950 secretsP,
951+ buildSecrets,
900952 } , disposables ) ;
901953
902954 const { common } = params ;
@@ -1344,7 +1396,8 @@ export async function doExec({
13441396 buildxOutput : undefined ,
13451397 skipPostAttach : false ,
13461398 skipPersistingCustomizationsFromFeatures : false ,
1347- dotfiles : { }
1399+ dotfiles : { } ,
1400+ buildSecrets : [ ]
13481401 } , disposables ) ;
13491402
13501403 const { common } = params ;
0 commit comments