Merge pull request #7 from 049940/049940-patch-2 #10
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Update devcontainer.jsonimport {spawn as spawnRaw} from 'child_process';
import fs, {fstatSync} from 'fs';
import path from 'path';
import {env} from 'process';
import {promisify} from 'util';
import {ExecFunction} from './exec';
export interface DevContainerCliError {
outcome: 'error';
code: number;
message: string;
description: string;
}
function getSpecCliInfo() {
// // TODO - this is temporary until the CLI is installed via npm
// // TODO - ^ could consider an
npm install
from the folder// const specCLIPath = path.resolve(__dirname, "..", "cli", "cli.js");
// return {
// command:
node ${specCLIPath}
,// };
return {
command: 'devcontainer',
};
}
async function isCliInstalled(exec: ExecFunction, cliVersion: string): Promise {
try {
const {exitCode, stdout} = await exec(getSpecCliInfo().command, ['--version'], {});
return exitCode === 0 && stdout === cliVersion;
} catch (error) {
return false;
}
}
const fstat = promisify(fs.stat);
async function installCli(exec: ExecFunction, cliVersion: string): Promise {
// if we have a local 'cli' folder, then use that as we're testing a private cli build
let cliStat = null;
try {
cliStat = await fstat('./_devcontainer_cli');
} catch {
}
if (cliStat && cliStat.isDirectory()) {
console.log('** Installing local cli');
const {exitCode, stdout, stderr} = await exec('bash', ['-c', 'cd _devcontainer_cli && npm install && npm install -g'], {});
if (exitCode != 0) {
console.log(stdout);
console.error(stderr);
}
return exitCode === 0;
}
console.log(
** Installing @devcontainers/cli@${cliVersion}
);const {exitCode, stdout, stderr} = await exec('bash', ['-c',
npm install -g @devcontainers/cli@${cliVersion}
], {});if (exitCode != 0) {
console.log(stdout);
console.error(stderr);
}
return exitCode === 0;
}
interface SpawnResult {
code: number | null;
}
interface SpawnOptions {
log: (data: string) => void;
err: (data: string) => void;
env: NodeJS.ProcessEnv;
}
function spawn(
command: string,
args: string[],
options: SpawnOptions,
): Promise {
return new Promise((resolve, reject) => {
const proc = spawnRaw(command, args, {env: options.env});
});
}
function parseCliOutput(value: string): T | DevContainerCliError {
if (value === '') {
// TODO - revisit this
throw new Error('Unexpected empty output from CLI');
}
try {
return JSON.parse(value) as T;
} catch (error) {
return {
code: -1,
outcome: 'error' as 'error',
message: 'Failed to parse CLI output',
description:
Failed to parse CLI output as JSON: ${value}\nError: ${error}
,};
}
}
async function runSpecCliJsonCommand(options: {
args: string[];
log: (data: string) => void;
env?: NodeJS.ProcessEnv;
}) {
// For JSON commands, pass stderr on to logging but capture stdout and parse the JSON response
let stdout = '';
const spawnOptions: SpawnOptions = {
log: data => (stdout += data),
err: data => options.log(data),
env: options.env ? {...process.env, ...options.env} : process.env,
};
const command = getSpecCliInfo().command;
console.log(
About to run ${command} ${options.args.join(' ')}
); // TODO - take an output arg to allow GH to use core.infoawait spawn(command, options.args, spawnOptions);
return parseCliOutput(stdout);
}
async function runSpecCliNonJsonCommand(options: {
args: string[];
log: (data: string) => void;
env?: NodeJS.ProcessEnv;
}) {
// For non-JSON commands, pass both stdout and stderr on to logging
const spawnOptions: SpawnOptions = {
log: data => options.log(data),
err: data => options.log(data),
env: options.env ? {...process.env, ...options.env} : process.env,
};
const command = getSpecCliInfo().command;
console.log(
About to run ${command} ${options.args.join(' ')}
); // TODO - take an output arg to allow GH to use core.infoconst result = await spawn(command, options.args, spawnOptions);
return result.code
}
export interface DevContainerCliSuccessResult {
outcome: 'success';
}
export interface DevContainerCliBuildResult
extends DevContainerCliSuccessResult {}
export interface DevContainerCliBuildArgs {
workspaceFolder: string;
imageName?: string[];
platform?: string;
additionalCacheFroms?: string[];
userDataFolder?: string;
output?: string,
noCache?: boolean,
}
async function devContainerBuild(
args: DevContainerCliBuildArgs,
log: (data: string) => void,
): Promise<DevContainerCliBuildResult | DevContainerCliError> {
const commandArgs: string[] = [
'build',
'--workspace-folder',
args.workspaceFolder,
];
if (args.imageName) {
args.imageName.forEach(iName =>
commandArgs.push('--image-name', iName),
);
}
if (args.platform) {
commandArgs.push('--platform', args.platform);
}
if (args.output) {
commandArgs.push('--output', args.output);
}
if (args.userDataFolder) {
commandArgs.push("--user-data-folder", args.userDataFolder);
}
if (args.noCache) {
commandArgs.push("--no-cache");
} else if (args.additionalCacheFroms) {
args.additionalCacheFroms.forEach(cacheFrom =>
commandArgs.push('--cache-from', cacheFrom),
);
}
return await runSpecCliJsonCommand({
args: commandArgs,
log,
env: {DOCKER_BUILDKIT: '1', COMPOSE_DOCKER_CLI_BUILD: '1'},
});
}
export interface DevContainerCliUpResult extends DevContainerCliSuccessResult {
containerId: string;
remoteUser: string;
remoteWorkspaceFolder: string;
}
export interface DevContainerCliUpArgs {
workspaceFolder: string;
additionalCacheFroms?: string[];
skipContainerUserIdUpdate?: boolean;
env?: string[];
userDataFolder?: string;
additionalMounts?: string[];
}
async function devContainerUp(
args: DevContainerCliUpArgs,
log: (data: string) => void,
): Promise<DevContainerCliUpResult | DevContainerCliError> {
const remoteEnvArgs = getRemoteEnvArray(args.env);
const commandArgs: string[] = [
'up',
'--workspace-folder',
args.workspaceFolder,
...remoteEnvArgs,
];
if (args.additionalCacheFroms) {
args.additionalCacheFroms.forEach(cacheFrom =>
commandArgs.push('--cache-from', cacheFrom),
);
}
if (args.userDataFolder) {
commandArgs.push("--user-data-folder", args.userDataFolder);
}
if (args.skipContainerUserIdUpdate) {
commandArgs.push('--update-remote-user-uid-default', 'off');
}
if (args.additionalMounts) {
args.additionalMounts.forEach(mount =>
commandArgs.push('--mount', mount),
);
}
return await runSpecCliJsonCommand({
args: commandArgs,
log,
env: {DOCKER_BUILDKIT: '1', COMPOSE_DOCKER_CLI_BUILD: '1'},
});
}
export interface DevContainerCliExecArgs {
workspaceFolder: string;
command: string[];
env?: string[];
userDataFolder?: string;
}
async function devContainerExec(
args: DevContainerCliExecArgs,
log: (data: string) => void,
): Promise<number | null> {
// const remoteEnvArgs = args.env ? args.env.flatMap(e=> ["--remote-env", e]): []; // TODO - test flatMap again
const remoteEnvArgs = getRemoteEnvArray(args.env);
const commandArgs = ["exec", "--workspace-folder", args.workspaceFolder, ...remoteEnvArgs, ...args.command];
if (args.userDataFolder) {
commandArgs.push("--user-data-folder", args.userDataFolder);
}
return await runSpecCliNonJsonCommand({
args: commandArgs,
log,
env: {DOCKER_BUILDKIT: '1', COMPOSE_DOCKER_CLI_BUILD: '1'},
});
}
function getRemoteEnvArray(env?: string[]): string[] {
if (!env) {
return [];
}
let result = [];
for (let i = 0; i < env.length; i++) {
const envItem = env[i];
result.push('--remote-env', envItem);
}
return result;
}
export const devcontainer = {
build: devContainerBuild,
up: devContainerUp,
exec: devContainerExec,
isCliInstalled,
installCli,
};