Skip to content

Commit

Permalink
Automatically set execute bit when running dotfiles install script (d…
Browse files Browse the repository at this point in the history
…evcontainers#541)

* Improvements to dotfiles

- Auto add execute bit
- Fix specifying a dotfiles-install-command

* refactor dotfiles tests

* Reorganize into relative and absolute checks

* clean up tests

* quote more installCommand and echo strings

* update old test
  • Loading branch information
joshspicer authored Jul 3, 2023
1 parent 15fcf14 commit c263250
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 45 deletions.
40 changes: 29 additions & 11 deletions src/spec-common/dotfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,33 @@ export async function installDotfiles(params: ResolverParameters, properties: Co
status: 'running',
});
if (installCommand) {
await shellServer.exec(`# Clone & install dotfiles
await shellServer.exec(`# Clone & install dotfiles via '${installCommand}'
${createFileCommand(markerFile)} || (echo dotfiles marker found && exit 1) || exit 0
command -v git >/dev/null 2>&1 || (echo git not found && exit 1) || exit 0
[ -e ${targetPath} ] || ${allEnv}git clone ${repository} ${targetPath} || exit $?
if [ -x ${installCommand} ]
echo Setting current directory to '${targetPath}'
cd ${targetPath}
if [ -f "./${installCommand}" ]
then
if [ ! -x "./${installCommand}" ]
then
echo Setting './${installCommand}' as executable
chmod +x "./${installCommand}"
fi
echo Executing command './${installCommand}'..\n
${allEnv}"./${installCommand}"
elif [ -f "${installCommand}" ]
then
echo Executing script ${installCommand}
cd ${targetPath} && ${allEnv}${installCommand}
if [ ! -x "${installCommand}" ]
then
echo Setting '${installCommand}' as executable
chmod +x "${installCommand}"
fi
echo Executing command '${installCommand}'...\n
${allEnv}"${installCommand}"
else
echo Error: ${installCommand} not executable
echo Could not locate '${installCommand}'...\n
exit 126
fi
`, { logOutput: 'continuous', logLevel: LogLevel.Info });
Expand All @@ -58,6 +75,7 @@ fi
${createFileCommand(markerFile)} || (echo dotfiles marker found && exit 1) || exit 0
command -v git >/dev/null 2>&1 || (echo git not found && exit 1) || exit 0
[ -e ${targetPath} ] || ${allEnv}git clone ${repository} ${targetPath} || exit $?
echo Setting current directory to ${targetPath}
cd ${targetPath}
for f in ${installCommands.join(' ')}
do
Expand All @@ -78,14 +96,14 @@ then
echo No dotfiles found.
fi
else
if [ -x "$installCommand" ]
if [ ! -x "$installCommand" ]
then
echo Executing script '${targetPath}'/"$installCommand"
${allEnv}./"$installCommand"
else
echo Error: '${targetPath}'/"$installCommand" not executable
exit 126
echo Setting '${targetPath}'/"$installCommand" as executable
chmod +x "$installCommand"
fi
echo Executing command '${targetPath}'/"$installCommand"...\n
${allEnv}./"$installCommand"
fi
`, { logOutput: 'continuous', logLevel: LogLevel.Info });
}
Expand Down
37 changes: 3 additions & 34 deletions src/test/cli.up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as assert from 'assert';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { devContainerDown, devContainerUp, shellExec, UpResult, pathExists } from './testUtils';
import { devContainerDown, devContainerUp, shellExec, UpResult } from './testUtils';

const pkg = require('../../package.json');

Expand All @@ -34,43 +34,12 @@ describe('Dev Containers CLI', function () {
await shellExec(`docker rm -f ${containerId}`);
});

it('should execute successfully with valid config and dotfiles', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository https://github.com/codspace/test-dotfiles`);
it('should execute successfully with valid config with a Feature', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
const containerId: string = response.containerId;
assert.ok(containerId, 'Container id not found.');
const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');
await shellExec(`docker rm -f ${containerId}`);
});

it('should execute successfully with valid config and dotfiles with secrets', async () => {
const testFolder = `${__dirname}/configs`;
let containerId: string | null = null;
await shellExec(`rm -f ${testFolder}/*.testMarker`, undefined, undefined, true);

const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository https://github.com/codspace/test-dotfiles --secrets-file ${testFolder}/test-secrets.json --log-level trace --log-format json`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;
assert.ok(containerId, 'Container id not found.');
const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');

// assert file contents to ensure secrets & remoteEnv were available to the command
const catResp = await shellExec(`${cli} exec --workspace-folder ${__dirname}/configs/image-with-git-feature cat /tmp/.dotfileEnvs`);
const { stdout, error } = catResp;
assert.strictEqual(error, null);
assert.match(stdout, /SECRET1=SecretValue1/);
assert.match(stdout, /TEST_REMOTE_ENV=Value 1/);

// assert secret masking
// We log the message `Starting container` from CLI. Since the word `container` is specified as a secret (in test-secrets.json), that should get masked
const logs = res.stderr;
assert.match(logs, /Starting \*\*\*\*\*\*\*\*/);
assert.doesNotMatch(logs, /Starting container/);

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

Expand Down
121 changes: 121 additions & 0 deletions src/test/dotfiles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { shellExec, pathExists } from './testUtils';

const pkg = require('../../package.json');

describe('Dotfiles', function () {

this.timeout('240s');
const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp'));
const cli = `npx --prefix ${tmp} devcontainer`;

before('Install', async () => {
await shellExec(`rm -rf ${tmp}/node_modules`);
await shellExec(`mkdir -p ${tmp}`);
await shellExec(`npm --prefix ${tmp} install devcontainers-cli-${pkg.version}.tgz`);
});

let containerId = '';
this.afterEach('Cleanup', async () => {
assert.ok(containerId, 'In Cleanup: Container id not found.');
await shellExec(`docker rm -f ${containerId}`);
containerId = '';
});

it('should execute successfully with valid config and dotfiles', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository https://github.com/codspace/test-dotfiles`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;

const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');
});

it('should execute successfully with valid config and dotfiles with custom install path as filename', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository https://github.com/codspace/test-dotfiles --dotfiles-install-command install.sh`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;

const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');
});

it('should execute successfully with valid config and dotfiles with custom install path as relative path', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository https://github.com/codspace/test-dotfiles --dotfiles-install-command ./install.sh`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;

const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');
});

it('should execute successfully with valid config and dotfiles with custom install path as absolute path', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository https://github.com/codspace/test-dotfiles --dotfiles-install-command /home/node/dotfiles/install.sh`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;

const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');
});

it('should execute successfully with valid config and dotfiles with non executable install script', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository codspace/test-dotfiles-non-executable --dotfiles-install-command .run-my-dotfiles-script`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;

const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');
});

it('should execute successfully with valid config and dotfiles with non executable absolute path install script', async () => {
const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository codspace/test-dotfiles-non-executable --dotfiles-install-command /home/node/dotfiles/.run-my-dotfiles-script`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;

const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');
});

it('should execute successfully with valid config and dotfiles with secrets', async () => {
const testFolder = `${__dirname}/configs`;
await shellExec(`rm -f ${testFolder}/*.testMarker`, undefined, undefined, true);
const secrets = {
'SECRET1': 'SecretValue1',
'MASK_IT': 'container',
};
await shellExec(`printf '${JSON.stringify(secrets)}' > ${testFolder}/test-secrets-temp.json`, undefined, undefined, true);

const res = await shellExec(`${cli} up --workspace-folder ${__dirname}/configs/image-with-git-feature --dotfiles-repository https://github.com/codspace/test-dotfiles --secrets-file ${testFolder}/test-secrets-temp.json --log-level trace --log-format json`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
containerId = response.containerId;
assert.ok(containerId, 'Container id not found.');
const dotfiles = await pathExists(cli, `${__dirname}/configs/image-with-git-feature`, `/tmp/.dotfilesMarker`);
assert.ok(dotfiles, 'Dotfiles not found.');

// assert file contents to ensure secrets & remoteEnv were available to the command
const catResp = await shellExec(`${cli} exec --workspace-folder ${__dirname}/configs/image-with-git-feature cat /tmp/.dotfileEnvs`);
const { stdout, error } = catResp;
assert.strictEqual(error, null);
assert.match(stdout, /SECRET1=SecretValue1/);
assert.match(stdout, /TEST_REMOTE_ENV=Value 1/);

// assert secret masking
// We log the message `Starting container` from CLI. Since the word `container` is specified as a secret here, that should get masked
const logs = res.stderr;
assert.match(logs, /Starting \*\*\*\*\*\*\*\*/);
assert.doesNotMatch(logs, /Starting container/);
});
});

0 comments on commit c263250

Please sign in to comment.