diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 662ed3b65..132727861 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -289,7 +289,17 @@ function escapeQuotesForShell(input: string) { return input.replace(new RegExp(`'`, 'g'), `'\\''`); } -export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: string, remoteUser: string, useBuildKitBuildContexts = false, contentSourceRootPath = '/tmp/build-features') { +export interface BuildSecret { + id: string; + file?: string; + env?: string; +} + +function getSecretMounts(buildSecrets: BuildSecret[]): string { + return buildSecrets.map(secret => `--mount=type=secret,id=${secret.id}`).join(' '); +} + +export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: string, remoteUser: string, useBuildKitBuildContexts = false, contentSourceRootPath = '/tmp/build-features', buildSecrets: BuildSecret[] = []) { const builtinsEnvFile = `${path.posix.join(FEATURES_CONTAINER_TEMP_DEST_FOLDER, 'devcontainer-features.builtin.env')}`; let result = `RUN \\ @@ -312,8 +322,10 @@ RUN chmod -R 0755 ${dest} \\ `; } else { - result += `RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder} \\ - cp -ar /tmp/build-features-src/${folder} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ + const secretMounts = getSecretMounts(buildSecrets); + const runPrefix = secretMounts ? `RUN ${secretMounts} ` : 'RUN '; + result += `${runPrefix}--mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder} \\ + cp -ar /tmp/build-features-src/${folder} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ && chmod -R 0755 ${dest} \\ && cd ${dest} \\ && chmod +x ./install.sh \\ @@ -339,9 +351,11 @@ RUN chmod -R 0755 ${dest} \\ `; } else { + const secretMounts = getSecretMounts(buildSecrets); + const runPrefix = secretMounts ? `RUN ${secretMounts} ` : 'RUN '; result += ` -RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${feature.consecutiveId} \\ - cp -ar /tmp/build-features-src/${feature.consecutiveId} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ +${runPrefix}--mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${feature.consecutiveId} \\ + cp -ar /tmp/build-features-src/${feature.consecutiveId} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ && chmod -R 0755 ${dest} \\ && cd ${dest} \\ && chmod +x ./devcontainer-features-install.sh \\ diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index c7e42a56a..579885e69 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -96,6 +96,14 @@ export async function extendImage(params: DockerResolverParameters, config: Subs for (const securityOpt of featureBuildInfo.securityOpts) { args.push('--security-opt', securityOpt); } + + for (const secret of params.buildSecrets) { + if (secret.file) { + args.push('--secret', `id=${secret.id},src=${secret.file}`); + } else if (secret.env) { + args.push('--secret', `id=${secret.id},env=${secret.env}`); + } + } } else { // Not using buildx args.push( @@ -257,7 +265,7 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont const contentSourceRootPath = useBuildKitBuildContexts ? '.' : '/tmp/build-features/'; const dockerfile = getContainerFeaturesBaseDockerFile(contentSourceRootPath) .replace('#{nonBuildKitFeatureContentFallback}', useBuildKitBuildContexts ? '' : `FROM ${buildContentImageName} as dev_containers_feature_content_source`) - .replace('#{featureLayer}', getFeatureLayers(featuresConfig, containerUser, remoteUser, useBuildKitBuildContexts, contentSourceRootPath)) + .replace('#{featureLayer}', getFeatureLayers(featuresConfig, containerUser, remoteUser, useBuildKitBuildContexts, contentSourceRootPath, params.buildSecrets)) .replace('#{containerEnv}', generateContainerEnvsV1(featuresConfig)) .replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata)) .replace('#{containerEnvMetadata}', generateContainerEnvs(devContainerConfig.config.containerEnv, true)) diff --git a/src/spec-node/devContainers.ts b/src/spec-node/devContainers.ts index 493d0dc22..32ccb40d9 100644 --- a/src/spec-node/devContainers.ts +++ b/src/spec-node/devContainers.ts @@ -8,7 +8,7 @@ import * as crypto from 'crypto'; import * as os from 'os'; import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI'; -import { DockerResolverParameters, DevContainerAuthority, UpdateRemoteUserUIDDefault, BindMountConsistency, getCacheFolder, GPUAvailability } from './utils'; +import { DockerResolverParameters, DevContainerAuthority, UpdateRemoteUserUIDDefault, BindMountConsistency, getCacheFolder, GPUAvailability, BuildSecret } from './utils'; import { createNullLifecycleHook, finishBackgroundTasks, ResolverParameters, UserEnvProbe } from '../spec-common/injectHeadless'; import { GoARCH, GoOS, getCLIHost, loadNativeModule } from '../spec-common/commonUtils'; import { resolve } from './configContainer'; @@ -73,6 +73,7 @@ export interface ProvisionOptions { omitSyntaxDirective?: boolean; includeConfig?: boolean; includeMergedConfig?: boolean; + buildSecrets: BuildSecret[]; } export async function launch(options: ProvisionOptions, providedIdLabels: string[] | undefined, disposables: (() => Promise | undefined)[]) { @@ -233,7 +234,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables: additionalLabels: options.additionalLabels, buildxOutput: common.buildxOutput, buildxCacheTo: common.buildxCacheTo, - platformInfo + platformInfo, + buildSecrets: options.buildSecrets }; } diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 59136695d..c22554ae5 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -10,7 +10,7 @@ import textTable from 'text-table'; import * as jsonc from 'jsonc-parser'; import { createDockerParams, createLog, launch, ProvisionOptions } from './devContainers'; -import { SubstitutedConfig, createContainerProperties, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution, findContainerAndIdLabels, getCacheFolder, runAsyncHandler } from './utils'; +import { SubstitutedConfig, createContainerProperties, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution, findContainerAndIdLabels, getCacheFolder, runAsyncHandler, BuildSecret } from './utils'; import { URI } from 'vscode-uri'; import { ContainerError } from '../spec-common/errors'; import { Log, LogDimensions, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log'; @@ -50,6 +50,43 @@ const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell'; const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,external=(true|false))?$/; +function parseBuildSecrets(buildSecretsArg: string[] | undefined): BuildSecret[] { + if (!buildSecretsArg) { + return []; + } + const secrets = Array.isArray(buildSecretsArg) ? buildSecretsArg : [buildSecretsArg]; + return secrets.map(secret => { + // Support shorthand: id=name (assumes env=name) + const shorthandMatch = secret.match(/^id=([^,]+)$/); + if (shorthandMatch) { + return { + id: shorthandMatch[1], + env: shorthandMatch[1].toUpperCase() + }; + } + + // Support file format: id=name,src=path + const fileMatch = secret.match(/^id=([^,]+),src=(.+)$/); + if (fileMatch) { + return { + id: fileMatch[1], + file: path.resolve(process.cwd(), fileMatch[2]) + }; + } + + // Support env format: id=name,env=VAR + const envMatch = secret.match(/^id=([^,]+),env=(.+)$/); + if (envMatch) { + return { + id: envMatch[1], + env: envMatch[2] + }; + } + + throw new Error(`Invalid build-secret format: ${secret}. Supported formats are: "id=,src=", "id=,env=", or "id=" (which assumes env=).`); + }); +} + (async () => { const packageFolder = path.join(__dirname, '..', '..'); @@ -137,6 +174,7 @@ function provisionOptions(y: Argv) { 'container-session-data-folder': { type: 'string', description: 'Folder to cache CLI data, for example userEnvProbe results' }, 'omit-config-remote-env-from-metadata': { type: 'boolean', default: false, hidden: true, description: 'Omit remoteEnv from devcontainer.json for container metadata label' }, 'secrets-file': { type: 'string', description: 'Path to a json file containing secret environment variables as key-value pairs.' }, + 'build-secret': { type: 'string', description: 'Build secrets in the format id=,src=, id=,env=, or id= (assumes env=). These will be passed as Docker build secrets to feature installation steps.' }, 'experimental-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Write lockfile' }, 'experimental-frozen-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Ensure lockfile remains unchanged' }, 'omit-syntax-directive': { type: 'boolean', default: false, hidden: true, description: 'Omit Dockerfile syntax directives' }, @@ -162,6 +200,10 @@ function provisionOptions(y: Argv) { if (remoteEnvs?.some(remoteEnv => !/.+=.*/.test(remoteEnv))) { throw new Error('Unmatched argument format: remote-env must match ='); } + const buildSecrets = (argv['build-secret'] && (Array.isArray(argv['build-secret']) ? argv['build-secret'] : [argv['build-secret']])) as string[] | undefined; + if (buildSecrets?.some(buildSecret => !/^id=[^,]+(,src=.+|,env=.+)?$/.test(buildSecret))) { + throw new Error('Unmatched argument format: build-secret must match id=,src=, id=,env=, or id='); + } return true; }); } @@ -211,6 +253,7 @@ async function provision({ 'container-session-data-folder': containerSessionDataFolder, 'omit-config-remote-env-from-metadata': omitConfigRemotEnvFromMetadata, 'secrets-file': secretsFile, + 'build-secret': buildSecret, 'experimental-lockfile': experimentalLockfile, 'experimental-frozen-lockfile': experimentalFrozenLockfile, 'omit-syntax-directive': omitSyntaxDirective, @@ -223,6 +266,7 @@ async function provision({ const addCacheFroms = addCacheFrom ? (Array.isArray(addCacheFrom) ? addCacheFrom as string[] : [addCacheFrom]) : []; const additionalFeatures = additionalFeaturesJson ? jsonc.parse(additionalFeaturesJson) as Record> : {}; const providedIdLabels = idLabel ? Array.isArray(idLabel) ? idLabel as string[] : [idLabel] : undefined; + const buildSecrets = parseBuildSecrets(buildSecret as string[] | undefined); const cwd = workspaceFolder || process.cwd(); const cliHost = await getCLIHost(cwd, loadNativeModule, logFormat === 'text'); @@ -286,6 +330,7 @@ async function provision({ omitSyntaxDirective, includeConfig, includeMergedConfig, + buildSecrets, }; const result = await doProvision(options, providedIdLabels); @@ -452,6 +497,7 @@ async function doSetUp({ installCommand: dotfilesInstallCommand, targetPath: dotfilesTargetPath, }, + buildSecrets: [], }, disposables); const { common } = params; @@ -523,6 +569,7 @@ function buildOptions(y: Argv) { 'additional-features': { type: 'string', description: 'Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)' }, 'skip-feature-auto-mapping': { type: 'boolean', default: false, hidden: true, description: 'Temporary option for testing.' }, 'skip-persisting-customizations-from-features': { type: 'boolean', default: false, hidden: true, description: 'Do not save customizations from referenced Features as image metadata' }, + 'build-secret': { type: 'string', description: 'Build secrets in the format id=,src=, id=,env=, or id= (assumes env=). These will be passed as Docker build secrets to feature installation steps.' }, 'experimental-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Write lockfile' }, 'experimental-frozen-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Ensure lockfile remains unchanged' }, 'omit-syntax-directive': { type: 'boolean', default: false, hidden: true, description: 'Omit Dockerfile syntax directives' }, @@ -568,6 +615,7 @@ async function doBuild({ 'experimental-lockfile': experimentalLockfile, 'experimental-frozen-lockfile': experimentalFrozenLockfile, 'omit-syntax-directive': omitSyntaxDirective, + 'build-secret': buildSecret, }: BuildArgs) { const disposables: (() => Promise | undefined)[] = []; const dispose = async () => { @@ -579,6 +627,7 @@ async function doBuild({ const overrideConfigFile: URI | undefined = /* overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : */ undefined; const addCacheFroms = addCacheFrom ? (Array.isArray(addCacheFrom) ? addCacheFrom as string[] : [addCacheFrom]) : []; const additionalFeatures = additionalFeaturesJson ? jsonc.parse(additionalFeaturesJson) as Record> : {}; + const buildSecrets = parseBuildSecrets(buildSecret as string[] | undefined); const params = await createDockerParams({ dockerPath, dockerComposePath, @@ -617,6 +666,7 @@ async function doBuild({ experimentalLockfile, experimentalFrozenLockfile, omitSyntaxDirective, + buildSecrets, }, disposables); const { common, dockerComposeCLI } = params; @@ -849,6 +899,7 @@ async function doRunUserCommands({ const cwd = workspaceFolder || process.cwd(); const cliHost = await getCLIHost(cwd, loadNativeModule, logFormat === 'text'); const secretsP = readSecretsFromFile({ secretsFile, cliHost }); + const buildSecrets: BuildSecret[] = []; const params = await createDockerParams({ dockerPath, @@ -891,6 +942,7 @@ async function doRunUserCommands({ }, containerSessionDataFolder, secretsP, + buildSecrets, }, disposables); const { common } = params; @@ -1333,7 +1385,8 @@ export async function doExec({ buildxOutput: undefined, skipPostAttach: false, skipPersistingCustomizationsFromFeatures: false, - dotfiles: {} + dotfiles: {}, + buildSecrets: [] }, disposables); const { common } = params; diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index 4c20ebd68..0a6467b07 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -6,7 +6,7 @@ import * as yaml from 'js-yaml'; import * as shellQuote from 'shell-quote'; -import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, DockerResolverParameters, inspectDockerImage, getEmptyContextFolder, getFolderImageName, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError } from './utils'; +import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, DockerResolverParameters, inspectDockerImage, getEmptyContextFolder, getFolderImageName, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError, BuildSecret } from './utils'; import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec-common/injectHeadless'; import { ContainerError } from '../spec-common/errors'; import { Workspace } from '../spec-utils/workspaces'; @@ -243,6 +243,13 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf buildOverrideContent += ` - ${buildKitContext}=${featureBuildInfo.buildKitContexts[buildKitContext]}\n`; } } + + if (params.buildSecrets.length > 0) { + buildOverrideContent += ' secrets:\n'; + for (const secret of params.buildSecrets) { + buildOverrideContent += ` - ${secret.id}\n`; + } + } } // Generate the docker-compose override and build @@ -253,10 +260,27 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf await cliHost.mkdirp(composeFolder); const composeOverrideFile = cliHost.path.join(composeFolder, `${overrideFilePrefix}-${Date.now()}.yml`); const cacheFromOverrideContent = (additionalCacheFroms && additionalCacheFroms.length > 0) ? ` cache_from:\n${additionalCacheFroms.map(cacheFrom => ` - ${cacheFrom}\n`).join('\n')}` : ''; + const secretsOverrideContent = generateSecretsOverrideContent(params.buildSecrets); + + function generateSecretsOverrideContent(buildSecrets: BuildSecret[]): string { + if (!buildSecrets || buildSecrets.length === 0) { + return ''; + } + let content = 'secrets:\n'; + for (const secret of buildSecrets) { + if (secret.file) { + content += ` ${secret.id}:\n file: ${secret.file}\n`; + } else if (secret.env) { + content += ` ${secret.id}:\n environment: ${secret.env}\n`; + } + } + return content; + } const composeOverrideContent = `${versionPrefix}services: ${config.service}: ${buildOverrideContent?.trimEnd()} ${cacheFromOverrideContent} +${secretsOverrideContent} `; output.write(`Docker Compose override file for building image:\n${composeOverrideContent}`); await cliHost.writeFile(composeOverrideFile, Buffer.from(composeOverrideContent)); diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 07d111cc2..d1c047999 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -200,7 +200,7 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config if (buildParams.buildxPush) { args.push('--push'); } else { - if (buildParams.buildxOutput) { + if (buildParams.buildxOutput) { args.push('--output', buildParams.buildxOutput); } else { args.push('--load'); // (short for --output=docker, i.e. load into normal 'docker images' collection) @@ -210,6 +210,14 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config args.push('--cache-to', buildParams.buildxCacheTo); } args.push('--build-arg', 'BUILDKIT_INLINE_CACHE=1'); + + for (const secret of buildParams.buildSecrets) { + if (secret.file) { + args.push('--secret', `id=${secret.id},src=${secret.file}`); + } else if (secret.env) { + args.push('--secret', `id=${secret.id},env=${secret.env}`); + } + } } else { args.push('build'); } diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index 2fe141320..78fb5a1bd 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -37,6 +37,12 @@ export type BindMountConsistency = 'consistent' | 'cached' | 'delegated' | undef export type GPUAvailability = 'all' | 'detect' | 'none'; +export interface BuildSecret { + id: string; + file?: string; + env?: string; +} + // Generic retry function export async function retry(fn: () => Promise, options: { retryIntervalMilliseconds: number; maxRetries: number; output: Log }): Promise { const { retryIntervalMilliseconds, maxRetries, output } = options; @@ -123,6 +129,7 @@ export interface DockerResolverParameters { buildxOutput: string | undefined; buildxCacheTo: string | undefined; platformInfo: PlatformInfo; + buildSecrets: BuildSecret[]; } export interface ResolverResult { diff --git a/src/test/cli.build.test.ts b/src/test/cli.build.test.ts index 0dfae0427..a0357dec4 100644 --- a/src/test/cli.build.test.ts +++ b/src/test/cli.build.test.ts @@ -53,7 +53,7 @@ describe('Dev Containers CLI', function () { await shellExec(`${cli} build --workspace-folder ${testFolder} --image-name demo:v1`); const tags = await shellExec(`docker images --format "{{.Tag}}" demo`); const imageTags = tags.stdout.trim().split('\n').filter(tag => tag !== ''); - assert.equal(imageTags.length, 1, 'There should be only one tag for demo:v1'); + assert.equal(imageTags.length, 1, 'There should be only one tag for demo:v1'); } catch (error) { assert.equal(error.code, 'ERR_ASSERTION', 'Should fail with ERR_ASSERTION'); } @@ -433,5 +433,55 @@ describe('Dev Containers CLI', function () { const details = JSON.parse((await shellExec(`docker inspect ${response.imageName}`)).stdout)[0] as ImageDetails; assert.strictEqual(details.Config.Labels?.test_build_options, 'success'); }); + + it('should build with Docker Compose and build secrets', async () => { + // Create a temporary secret file + const secretContent = 'My super important secret'; + const secretFile = path.join(os.tmpdir(), 'test_secret.txt'); + fs.writeFileSync(secretFile, secretContent); + + const testFolder = `${__dirname}/configs/compose-build-secret-feature`; + const imageName = 'test-compose-image'; + + try { + const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --build-secret id=compose_file,src=${secretFile} --image-name ${imageName} --no-cache`); + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'success'); + + // Verify the secret content was correctly written to the image + const secretCheck = await shellExec(`docker run --rm --entrypoint="cat" ${imageName} /secret_file.txt`); + assert.equal(secretCheck.stdout.trim(), secretContent); + const secretCheckDockerfile = await shellExec(`docker run --rm --entrypoint="cat" ${imageName} /secret_test_output_dockerfile.txt`); + assert.equal(secretCheckDockerfile.stdout.trim(), secretContent); + } finally { + // Cleanup + fs.unlinkSync(secretFile); + await shellExec(`docker rmi -f ${imageName}`).catch(() => { }); + } + }); + + it('should build with image and build secrets', async () => { + // Create a temporary secret file + const secretContent = 'My super important secret'; + const secretFile = path.join(os.tmpdir(), 'test_secret.txt'); + fs.writeFileSync(secretFile, secretContent); + + const testFolder = `${__dirname}/configs/build-secret-feature`; + const imageName = 'test-build-secrets-image'; + + try { + const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --build-secret id=compose_file,src=${secretFile} --image-name ${imageName} --no-cache`); + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'success'); + + // Verify the secret content was correctly written to the image + const secretCheck = await shellExec(`docker run --rm --entrypoint="cat" ${imageName} /secret_file.txt`); + assert.equal(secretCheck.stdout.trim(), secretContent); + } finally { + // Cleanup + fs.unlinkSync(secretFile); + await shellExec(`docker rmi -f ${imageName}`).catch(() => { }); + } + }); }); }); diff --git a/src/test/configs/build-secret-feature/.devcontainer/devcontainer.json b/src/test/configs/build-secret-feature/.devcontainer/devcontainer.json new file mode 100644 index 000000000..c8039894d --- /dev/null +++ b/src/test/configs/build-secret-feature/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "Test Feature Secrets", + "image": "debian:trixie-slim", + "features": { + "./secret_test": {} + } +} \ No newline at end of file diff --git a/src/test/configs/build-secret-feature/.devcontainer/secret_test/devcontainer-feature.json b/src/test/configs/build-secret-feature/.devcontainer/secret_test/devcontainer-feature.json new file mode 100644 index 000000000..51d49e8c5 --- /dev/null +++ b/src/test/configs/build-secret-feature/.devcontainer/secret_test/devcontainer-feature.json @@ -0,0 +1,6 @@ +{ + "id": "secret_test", + "name": "Secret Test Feature", + "description": "A test feature to demonstrate secrets in Docker Buildx", + "version": "1.0.0" +} \ No newline at end of file diff --git a/src/test/configs/build-secret-feature/.devcontainer/secret_test/install.sh b/src/test/configs/build-secret-feature/.devcontainer/secret_test/install.sh new file mode 100755 index 000000000..b62e73c66 --- /dev/null +++ b/src/test/configs/build-secret-feature/.devcontainer/secret_test/install.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +if [ -f /run/secrets/compose_file ]; then + echo "Contents of secret file:" + cat /run/secrets/compose_file + echo "Writing secret file contents to /secret_file.txt" + cat /run/secrets/compose_file > /secret_file.txt +else + echo "Secret file not found!" + exit 1 +fi diff --git a/src/test/configs/compose-build-secret-feature/.devcontainer/Dockerfile b/src/test/configs/compose-build-secret-feature/.devcontainer/Dockerfile new file mode 100644 index 000000000..0347efcab --- /dev/null +++ b/src/test/configs/compose-build-secret-feature/.devcontainer/Dockerfile @@ -0,0 +1,2 @@ +FROM debian:trixie-slim +RUN --mount=type=secret,id=compose_file cat /run/secrets/compose_file > /secret_test_output_dockerfile.txt diff --git a/src/test/configs/compose-build-secret-feature/.devcontainer/devcontainer.json b/src/test/configs/compose-build-secret-feature/.devcontainer/devcontainer.json new file mode 100644 index 000000000..eabd819bb --- /dev/null +++ b/src/test/configs/compose-build-secret-feature/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "name": "Test Docker Compose Feature Secrets", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces", + "features": { + "./secret_test": {} + } +} \ No newline at end of file diff --git a/src/test/configs/compose-build-secret-feature/.devcontainer/docker-compose.yml b/src/test/configs/compose-build-secret-feature/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..14b9ba6a5 --- /dev/null +++ b/src/test/configs/compose-build-secret-feature/.devcontainer/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.8' +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ..:/workspaces:cached + command: sleep infinity \ No newline at end of file diff --git a/src/test/configs/compose-build-secret-feature/.devcontainer/secret_test/devcontainer-feature.json b/src/test/configs/compose-build-secret-feature/.devcontainer/secret_test/devcontainer-feature.json new file mode 100644 index 000000000..51d49e8c5 --- /dev/null +++ b/src/test/configs/compose-build-secret-feature/.devcontainer/secret_test/devcontainer-feature.json @@ -0,0 +1,6 @@ +{ + "id": "secret_test", + "name": "Secret Test Feature", + "description": "A test feature to demonstrate secrets in Docker Buildx", + "version": "1.0.0" +} \ No newline at end of file diff --git a/src/test/configs/compose-build-secret-feature/.devcontainer/secret_test/install.sh b/src/test/configs/compose-build-secret-feature/.devcontainer/secret_test/install.sh new file mode 100755 index 000000000..b62e73c66 --- /dev/null +++ b/src/test/configs/compose-build-secret-feature/.devcontainer/secret_test/install.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +if [ -f /run/secrets/compose_file ]; then + echo "Contents of secret file:" + cat /run/secrets/compose_file + echo "Writing secret file contents to /secret_file.txt" + cat /run/secrets/compose_file > /secret_file.txt +else + echo "Secret file not found!" + exit 1 +fi