Skip to content

Commit

Permalink
Fallback when getent is not available
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Aug 21, 2023
1 parent 413c5f7 commit 7eb981b
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 9 deletions.
9 changes: 9 additions & 0 deletions src/spec-common/commonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { StringDecoder } from 'string_decoder';
import { toErrorText } from './errors';
import { Disposable, Event, NodeEventEmitter } from '../spec-utils/event';
import { isLocalFile } from '../spec-utils/pfs';
import { escapeRegExCharacters } from '../spec-utils/strings';
import { Log, nullLog } from '../spec-utils/log';
import { ShellServer } from './shellServer';

Expand Down Expand Up @@ -581,3 +582,11 @@ export async function getLocalUsername() {
}
return localUsername;
}

export function getEntPasswdShellCommand(userNameOrId: string) {
const escapedForShell = userNameOrId.replace(/['\\]/g, '\\$&');
const escapedForRexExp = escapeRegExCharacters(userNameOrId)
.replaceAll('\'', '\\\'');
// Leading space makes sure we don't concatenate to arithmetic expansion (https://tldp.org/LDP/abs/html/dblparens.html).
return ` (command -v getent >/dev/null 2>&1 && getent passwd '${escapedForShell}' || grep -E '^${escapedForRexExp}|^[^:]*:[^:]*:${escapedForRexExp}:' /etc/passwd)`;
}
4 changes: 2 additions & 2 deletions src/spec-common/injectHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as crypto from 'crypto';

import { ContainerError, toErrorText, toWarningText } from './errors';
import { launch, ShellServer } from './shellServer';
import { ExecFunction, CLIHost, PtyExecFunction, isFile, Exec, PtyExec } from './commonUtils';
import { ExecFunction, CLIHost, PtyExecFunction, isFile, Exec, PtyExec, getEntPasswdShellCommand } from './commonUtils';
import { Disposable, Event, NodeEventEmitter } from '../spec-utils/event';
import { PackageConfiguration } from '../spec-utils/product';
import { URI } from 'vscode-uri';
Expand Down Expand Up @@ -286,7 +286,7 @@ async function getUserShell(containerEnv: NodeJS.ProcessEnv, passwdUser: PasswdU
}

export async function getUserFromPasswdDB(shellServer: ShellServer, userNameOrId: string) {
const { stdout } = await shellServer.exec(`getent passwd ${userNameOrId}`, { logOutput: false });
const { stdout } = await shellServer.exec(getEntPasswdShellCommand(userNameOrId), { logOutput: false });
return parseUserInPasswdDB(stdout);
}

Expand Down
5 changes: 3 additions & 2 deletions src/spec-configuration/containerFeaturesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getPublishedVersi
import { Lockfile, readLockfile, writeLockfile } from './lockfile';
import { computeDependsOnInstallationOrder } from './containerFeaturesOrder';
import { logFeatureAdvisories } from './featureAdvisories';
import { getEntPasswdShellCommand } from '../spec-common/commonUtils';

// v1
const V1_ASSET_NAME = 'devcontainer-features.tgz';
Expand Down Expand Up @@ -323,8 +324,8 @@ export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser:

const builtinsEnvFile = `${path.posix.join(FEATURES_CONTAINER_TEMP_DEST_FOLDER, 'devcontainer-features.builtin.env')}`;
let result = `RUN \\
echo "_CONTAINER_USER_HOME=$(getent passwd ${containerUser} | cut -d: -f6)" >> ${builtinsEnvFile} && \\
echo "_REMOTE_USER_HOME=$(getent passwd ${remoteUser} | cut -d: -f6)" >> ${builtinsEnvFile}
echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand(containerUser)} | cut -d: -f6)" >> ${builtinsEnvFile} && \\
echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand(remoteUser)} | cut -d: -f6)" >> ${builtinsEnvFile}
`;

Expand Down
3 changes: 2 additions & 1 deletion src/spec-shutdown/dockerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toErrorText } from '../spec-common/errors';
import * as ptyType from 'node-pty';
import { Log, makeLog } from '../spec-utils/log';
import { Event } from '../spec-utils/event';
import { escapeRegExCharacters } from '../spec-utils/strings';

export interface ContainerDetails {
Id: string;
Expand Down Expand Up @@ -351,7 +352,7 @@ function replacingDockerExecLog(original: Log, cmd: string, args: string[]) {
}

function replacingLog(original: Log, search: string, replace: string) {
const searchR = new RegExp(search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
const searchR = new RegExp(escapeRegExCharacters(search), 'g');
const wrapped = makeLog({
...original,
get dimensions() {
Expand Down
9 changes: 9 additions & 0 deletions src/spec-utils/strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/


export function escapeRegExCharacters(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
9 changes: 5 additions & 4 deletions src/test/container-features/generateFeaturesConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DevContainerConfig } from '../../spec-configuration/configuration';
import { URI } from 'vscode-uri';
import { getLocalCacheFolder } from '../../spec-node/utils';
import { shellExec } from '../testUtils';
import { getEntPasswdShellCommand } from '../../spec-common/commonUtils';

export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace));

Expand Down Expand Up @@ -78,8 +79,8 @@ ENV MYKEYTWO="MY RESULT TWO"`;
// getFeatureLayers
const actualLayers = getFeatureLayers(featuresConfig, 'testContainerUser', 'testRemoteUser');
const expectedLayers = `RUN \\
echo "_CONTAINER_USER_HOME=$(getent passwd testContainerUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\
echo "_REMOTE_USER_HOME=$(getent passwd testRemoteUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env
echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand('testContainerUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\
echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand('testRemoteUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env
COPY --chown=root:root --from=dev_containers_feature_content_source /tmp/build-features/first_0 /tmp/dev-container-features/first_0
RUN chmod -R 0755 /tmp/dev-container-features/first_0 \\
Expand Down Expand Up @@ -140,8 +141,8 @@ RUN chmod -R 0755 /tmp/dev-container-features/second_1 \\
// getFeatureLayers
const actualLayers = getFeatureLayers(featuresConfig, 'testContainerUser', 'testRemoteUser');
const expectedLayers = `RUN \\
echo "_CONTAINER_USER_HOME=$(getent passwd testContainerUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\
echo "_REMOTE_USER_HOME=$(getent passwd testRemoteUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env
echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand('testContainerUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\
echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand('testRemoteUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env
COPY --chown=root:root --from=dev_containers_feature_content_source /tmp/build-features/color_0 /tmp/dev-container-features/color_0
Expand Down
66 changes: 66 additions & 0 deletions src/test/getEntPasswd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert';
import { shellExec, output } from './testUtils';
import { dockerExecFunction } from '../spec-shutdown/dockerUtils';
import { plainExec } from '../spec-common/commonUtils';
import { launch } from '../spec-common/shellServer';
import { getUserFromPasswdDB } from '../spec-common/injectHeadless';

describe('getEntPasswdShellCommand', function () {
this.timeout('20s');

[
{
image: 'busybox',
getentPath: undefined,
addUserOptions: '-D -h',
userName: 'foo\\bar',
},
{
image: 'debian',
getentPath: '/usr/bin/getent',
addUserOptions: '--disabled-password --allow-all-names --gecos "" --home',
userName: 'foo\\bar',
},
{
image: 'alpine',
getentPath: '/usr/bin/getent',
addUserOptions: '-D -h',
userName: 'foo_bar', // Note: Alpine doesn't support backslash in user names.
},
].forEach(({ image, getentPath, addUserOptions, userName }) => {
it(`should work with ${image} ${getentPath ? 'with' : 'without'} getent command`, async () => {
const res = await shellExec(`docker run -d ${image} sleep inf`);
const containerId = res.stdout.trim();
const exec = dockerExecFunction({
exec: plainExec(undefined),
cmd: 'docker',
env: {},
output,
}, containerId, 'root');
const shellServer = await launch(exec, output);

const which = await shellServer.exec('command -v getent')
.catch(() => undefined);
assert.strictEqual(which?.stdout.trim(), getentPath);

await shellServer.exec(`adduser ${addUserOptions} /home/foo ${userName.replaceAll('\\', '\\\\')}`);

const userByName = await getUserFromPasswdDB(shellServer, userName);
assert.ok(userByName);
assert.strictEqual(userByName.name, userName);
assert.strictEqual(userByName.home, '/home/foo');

const userById = await getUserFromPasswdDB(shellServer, userByName.uid);
assert.ok(userById);
assert.strictEqual(userById.name, userName);
assert.strictEqual(userById.home, '/home/foo');

await shellExec(`docker rm -f ${containerId}`);
});
});
});

0 comments on commit 7eb981b

Please sign in to comment.