diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index ae3a3c3a..e0094202 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -258,6 +258,7 @@ echo '${optionsIndented}' echo =========================================================================== set -a +. ../devcontainer-features.builtin.env . ./devcontainer-features.env set +a @@ -274,8 +275,12 @@ function escapeQuotesForShell(input: string) { return input.replace(new RegExp(`'`, 'g'), `'\\''`); } -export function getFeatureLayers(featuresConfig: FeaturesConfig) { - let result = ''; +export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: string, remoteUser: string) { + let result = `RUN \\ +echo "_CONTAINER_USER_HOME=$(getent passwd ${containerUser} | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env && \\ +echo "_REMOTE_USER_HOME=$(getent passwd ${remoteUser} | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env + +`; // Features version 1 const folders = (featuresConfig.featureSets || []).filter(y => y.internalVersion !== '2').map(x => x.features[0].consecutiveId); diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index 2597c986..14c13878 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -15,7 +15,7 @@ import { readLocalFile } from '../spec-utils/pfs'; import { includeAllConfiguredFeatures } from '../spec-utils/product'; import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig } from './utils'; import { isEarlierVersion, parseVersion } from '../spec-common/commonUtils'; -import { getDevcontainerMetadata, getDevcontainerMetadataLabel, getImageBuildInfoFromImage, ImageBuildInfo, MergedDevContainerConfig } from './imageMetadata'; +import { getDevcontainerMetadata, getDevcontainerMetadataLabel, getImageBuildInfoFromImage, ImageBuildInfo, ImageMetadataEntry, MergedDevContainerConfig } from './imageMetadata'; import { supportsBuildContexts } from './dockerfileUtils'; // Escapes environment variable keys. @@ -34,7 +34,7 @@ export async function extendImage(params: DockerResolverParameters, config: Subs const { cliHost, output } = common; const imageBuildInfo = await getImageBuildInfoFromImage(params, imageName, config.substitute, common.experimentalImageMetadata); - const extendImageDetails = await getExtendImageBuildInfo(params, config, imageName, imageBuildInfo, additionalFeatures); + const extendImageDetails = await getExtendImageBuildInfo(params, config, imageName, imageBuildInfo, undefined, additionalFeatures); if (!extendImageDetails || !extendImageDetails.featureBuildInfo) { // no feature extensions - return return { @@ -94,7 +94,7 @@ export async function extendImage(params: DockerResolverParameters, config: Subs }; } -export async function getExtendImageBuildInfo(params: DockerResolverParameters, config: SubstitutedConfig, baseName: string, imageBuildInfo: ImageBuildInfo, additionalFeatures: Record>) { +export async function getExtendImageBuildInfo(params: DockerResolverParameters, config: SubstitutedConfig, baseName: string, imageBuildInfo: ImageBuildInfo, composeServiceUser: string | undefined, additionalFeatures: Record>) { // Creates the folder where the working files will be setup. const dstFolder = await createFeaturesTempFolder(params.common); @@ -109,7 +109,7 @@ export async function getExtendImageBuildInfo(params: DockerResolverParameters, } // Generates the end configuration. - const featureBuildInfo = await getFeaturesBuildOptions(params, config, featuresConfig, baseName, imageBuildInfo); + const featureBuildInfo = await getFeaturesBuildOptions(params, config, featuresConfig, baseName, imageBuildInfo, composeServiceUser); if (!featureBuildInfo) { return undefined; } @@ -191,7 +191,7 @@ function getImageBuildOptions(params: DockerResolverParameters, config: Substitu dstFolder, dockerfileContent: ` FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage -${getDevcontainerMetadataLabel(imageBuildInfo.metadata, config, { featureSets: [] }, params.common.experimentalImageMetadata)} +${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata, config, { featureSets: [] }), params.common.experimentalImageMetadata)} `, overrideTarget: 'dev_containers_target_stage', dockerfilePrefixContent: ` @@ -204,7 +204,7 @@ ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder }; } -async function getFeaturesBuildOptions(params: DockerResolverParameters, devContainerConfig: SubstitutedConfig, featuresConfig: FeaturesConfig, baseName: string, imageBuildInfo: ImageBuildInfo): Promise { +async function getFeaturesBuildOptions(params: DockerResolverParameters, devContainerConfig: SubstitutedConfig, featuresConfig: FeaturesConfig, baseName: string, imageBuildInfo: ImageBuildInfo, composeServiceUser: string | undefined): Promise { const { common } = params; const { cliHost, output } = common; const { dstFolder } = featuresConfig; @@ -239,6 +239,15 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont const useBuildKitBuildContexts = buildKitVersionParsed ? !isEarlierVersion(buildKitVersionParsed, minRequiredVersion) : false; const buildContentImageName = 'dev_container_feature_content_temp'; + const imageMetadata = getDevcontainerMetadata(imageBuildInfo.metadata, devContainerConfig, featuresConfig); + const { containerUser, remoteUser } = findContainerUsers(imageMetadata, composeServiceUser, imageBuildInfo.user); + const builtinVariables = [ + `_CONTAINER_USER=${containerUser}`, + `_REMOTE_USER=${remoteUser}`, + ]; + const envPath = cliHost.path.join(dstFolder, 'devcontainer-features.builtin.env'); + await cliHost.writeFile(envPath, Buffer.from(builtinVariables.join('\n') + '\n')); + // When copying via buildkit, the content is accessed via '.' (i.e. in the context root) // When copying via temp image, the content is in '/tmp/build-features' const contentSourceRootPath = useBuildKitBuildContexts ? '.' : '/tmp/build-features/'; @@ -246,10 +255,10 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont .replace('#{nonBuildKitFeatureContentFallback}', useBuildKitBuildContexts ? '' : `FROM ${buildContentImageName} as dev_containers_feature_content_source`) .replace('{contentSourceRootPath}', contentSourceRootPath) .replace('#{featureBuildStages}', getFeatureBuildStages(featuresConfig, buildStageScripts, contentSourceRootPath)) - .replace('#{featureLayer}', getFeatureLayers(featuresConfig)) + .replace('#{featureLayer}', getFeatureLayers(featuresConfig, containerUser, remoteUser)) .replace('#{containerEnv}', generateContainerEnvs(featuresConfig)) .replace('#{copyFeatureBuildStages}', getCopyFeatureBuildStages(featuresConfig, buildStageScripts)) - .replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageBuildInfo.metadata, devContainerConfig, featuresConfig, common.experimentalImageMetadata)) + .replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata, common.experimentalImageMetadata)) ; const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; const dockerfilePrefixContent = `${useBuildKitBuildContexts && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? @@ -338,6 +347,13 @@ ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder }; } +export function findContainerUsers(imageMetadata: SubstitutedConfig, composeServiceUser: string | undefined, imageUser: string) { + const reversed = imageMetadata.config.slice().reverse(); + const containerUser = reversed.find(entry => entry.containerUser)?.containerUser || composeServiceUser || imageUser; + const remoteUser = reversed.find(entry => entry.remoteUser)?.remoteUser || containerUser; + return { containerUser, remoteUser }; +} + function getFeatureBuildStages(featuresConfig: FeaturesConfig, buildStageScripts: Record[], contentSourceRootPath: string) { return ([] as string[]).concat(...featuresConfig.featureSets .map((featureSet, i) => featureSet.features diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index 1fdbbcf1..35c13251 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -182,7 +182,7 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf // determine whether we need to extend with features const noBuildKitParams = { ...params, buildKitVersion: null }; // skip BuildKit -> can't set additional build contexts with compose const imageBuildInfo = await getImageBuildInfoFromDockerfile(params, originalDockerfile, serviceInfo.build?.args || {}, serviceInfo.build?.target, configWithRaw.substitute, common.experimentalImageMetadata); - const extendImageBuildInfo = await getExtendImageBuildInfo(noBuildKitParams, configWithRaw, baseName, imageBuildInfo, additionalFeatures); + const extendImageBuildInfo = await getExtendImageBuildInfo(noBuildKitParams, configWithRaw, baseName, imageBuildInfo, composeService.user, additionalFeatures); let overrideImageName: string | undefined; let buildOverrideContent = ''; diff --git a/src/spec-node/imageMetadata.ts b/src/spec-node/imageMetadata.ts index ceaa1272..ca03a960 100644 --- a/src/spec-node/imageMetadata.ts +++ b/src/spec-node/imageMetadata.ts @@ -359,11 +359,11 @@ function internalGetImageMetadata0(imageDetails: ImageDetails | ContainerDetails return []; } -export function getDevcontainerMetadataLabel(baseImageMetadata: SubstitutedConfig, devContainerConfig: SubstitutedConfig, featuresConfig: FeaturesConfig, experimentalImageMetadata: boolean) { +export function getDevcontainerMetadataLabel(devContainerMetadata: SubstitutedConfig, experimentalImageMetadata: boolean) { if (!experimentalImageMetadata) { return ''; } - const metadata = getDevcontainerMetadata(baseImageMetadata, devContainerConfig, featuresConfig).raw; + const metadata = devContainerMetadata.raw; if (!metadata.length) { return ''; } diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index e4b97bb8..db9ce59a 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -147,7 +147,7 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config } const imageBuildInfo = await getImageBuildInfoFromDockerfile(buildParams, originalDockerfile, config.build?.args || {}, config.build?.target, configWithRaw.substitute, buildParams.common.experimentalImageMetadata); - const extendImageBuildInfo = await getExtendImageBuildInfo(buildParams, configWithRaw, baseName, imageBuildInfo, additionalFeatures); + const extendImageBuildInfo = await getExtendImageBuildInfo(buildParams, configWithRaw, baseName, imageBuildInfo, undefined, additionalFeatures); let finalDockerfilePath = dockerfilePath; const additionalBuildArgs: string[] = []; diff --git a/src/test/container-features/featureHelpers.test.ts b/src/test/container-features/featureHelpers.test.ts index edcad6cd..350b5f4c 100644 --- a/src/test/container-features/featureHelpers.test.ts +++ b/src/test/container-features/featureHelpers.test.ts @@ -3,7 +3,9 @@ import * as path from 'path'; import { DevContainerFeature } from '../../spec-configuration/configuration'; import { OCIRef } from '../../spec-configuration/containerCollectionsOCI'; import { Feature, FeatureSet, getBackwardCompatibleFeatureId, getFeatureInstallWrapperScript, processFeatureIdentifier } from '../../spec-configuration/containerFeaturesConfiguration'; -import { getSafeId } from '../../spec-node/containerFeatures'; +import { getSafeId, findContainerUsers } from '../../spec-node/containerFeatures'; +import { ImageMetadataEntry } from '../../spec-node/imageMetadata'; +import { SubstitutedConfig } from '../../spec-node/utils'; import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); @@ -487,6 +489,7 @@ echo '' echo =========================================================================== set -a +. ../devcontainer-features.builtin.env . ./devcontainer-features.env set +a @@ -566,6 +569,7 @@ echo ' VERSION=latest echo =========================================================================== set -a +. ../devcontainer-features.builtin.env . ./devcontainer-features.env set +a @@ -576,4 +580,67 @@ chmod +x ./install.sh const actual = getFeatureInstallWrapperScript(feature, set, options); assert.equal(actual, expected); }); -}); \ No newline at end of file +}); + +describe('findContainerUsers', () => { + it('returns last metadata containerUser as containerUser and remoteUser', () => { + assert.deepEqual(findContainerUsers(configWithRaw([ + { + containerUser: 'metadataTestUser1', + }, + { + containerUser: 'metadataTestUser2', + }, + ]), 'composeTestUser', 'imageTestUser'), { + containerUser: 'metadataTestUser2', + remoteUser: 'metadataTestUser2', + }); + }); + it('returns compose service user as containerUser and remoteUser', () => { + assert.deepEqual(findContainerUsers(configWithRaw([ + { + remoteEnv: { foo: 'bar' }, + }, + { + remoteEnv: { bar: 'baz' }, + }, + ]), 'composeTestUser', 'imageTestUser'), { + containerUser: 'composeTestUser', + remoteUser: 'composeTestUser', + }); + }); + it('returns image user as containerUser and remoteUser', () => { + assert.deepEqual(findContainerUsers(configWithRaw([ + { + remoteEnv: { foo: 'bar' }, + }, + { + remoteEnv: { bar: 'baz' }, + }, + ]), undefined, 'imageTestUser'), { + containerUser: 'imageTestUser', + remoteUser: 'imageTestUser', + }); + }); + it('returns last metadata remoteUser', () => { + assert.deepEqual(findContainerUsers(configWithRaw([ + { + remoteUser: 'metadataTestUser1', + }, + { + remoteUser: 'metadataTestUser2', + }, + ]), 'composeTestUser', 'imageTestUser'), { + containerUser: 'composeTestUser', + remoteUser: 'metadataTestUser2', + }); + }); +}); + +function configWithRaw(config: T): SubstitutedConfig { + return { + config, + raw: config, + substitute: config => config, + }; +} diff --git a/src/test/container-features/generateFeaturesConfig.test.ts b/src/test/container-features/generateFeaturesConfig.test.ts index 2d7fec01..e2843ee6 100644 --- a/src/test/container-features/generateFeaturesConfig.test.ts +++ b/src/test/container-features/generateFeaturesConfig.test.ts @@ -68,8 +68,12 @@ describe('validate generateFeaturesConfig()', function () { // assert.strictEqual(actualEnvs, expectedEnvs); // getFeatureLayers - const actualLayers = getFeatureLayers(featuresConfig); - const expectedLayers = `RUN cd /tmp/build-features/first_1 \\ + const actualLayers = getFeatureLayers(featuresConfig, 'testContainerUser', 'testRemoteUser'); + const expectedLayers = `RUN \\ +echo "_CONTAINER_USER_HOME=$(getent passwd testContainerUser | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env && \\ +echo "_REMOTE_USER_HOME=$(getent passwd testRemoteUser | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env + +RUN cd /tmp/build-features/first_1 \\ && chmod +x ./install.sh \\ && ./install.sh @@ -122,8 +126,12 @@ RUN cd /tmp/build-features/second_2 \\ // -- Test containerFeatures.ts helper functions // getFeatureLayers - const actualLayers = getFeatureLayers(featuresConfig); - const expectedLayers = ` + const actualLayers = getFeatureLayers(featuresConfig, 'testContainerUser', 'testRemoteUser'); + const expectedLayers = `RUN \\ +echo "_CONTAINER_USER_HOME=$(getent passwd testContainerUser | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env && \\ +echo "_REMOTE_USER_HOME=$(getent passwd testRemoteUser | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env + + RUN cd /tmp/build-features/color_3 \\ && chmod +x ./devcontainer-features-install.sh \\ && ./devcontainer-features-install.sh diff --git a/src/test/imageMetadata.test.ts b/src/test/imageMetadata.test.ts index 91d5c5e8..54447833 100644 --- a/src/test/imageMetadata.test.ts +++ b/src/test/imageMetadata.test.ts @@ -180,7 +180,7 @@ describe('Image Metadata', function () { }); it('should create label for Dockerfile', () => { - const label = getDevcontainerMetadataLabel(configWithRaw([ + const label = getDevcontainerMetadataLabel(getDevcontainerMetadata(configWithRaw([ { id: 'baseFeature', } @@ -194,7 +194,7 @@ describe('Image Metadata', function () { value: 'someValue', included: true, } - ]), true); + ])), true); const expected = [ { id: 'baseFeature',