Skip to content

Commit 4a6bb5c

Browse files
authored
[Dev tool] Add check-node-versions command (Azure#13828)
* [Dev tool] Add checkNodesVers command * address feedback * rename keep-docker-image to plural * rename a couple more flags to plural * fix
1 parent 90eab8e commit 4a6bb5c

File tree

3 files changed

+309
-1
lines changed

3 files changed

+309
-1
lines changed

common/tools/dev-tool/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ It provides a place to centralize scripts, resources, and processes for developm
2020
- `dev` (link samples to local sources for access to IntelliSense during development)
2121
- `prep` (prepare samples for local source-linked execution)
2222
- `run` (execute a sample or all samples within a directory)
23+
- `check-node-versions` (execute samples with different node versions, typically in preparation for release)
2324

2425
The `dev-tool about` command will print some information about how to use the command. All commands additionally accept the `--help` argument, which will print information about the usage of that specific command. For example, to show help information for the `resolve` command above, issue the command `dev-tool package resolve --help`.
2526

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import fs from "fs-extra";
5+
import path from "path";
6+
import pr from "child_process";
7+
import os from "os";
8+
9+
import { createPrinter } from "../../util/printer";
10+
import { leafCommand, makeCommandInfo } from "../../framework/command";
11+
import { S_IRWXO } from "constants";
12+
import { resolveProject } from "../../util/resolveProject";
13+
14+
const log = createPrinter("check-node-versions-samples");
15+
16+
async function spawnCMD(cmd: string, args: string[], errorMessage?: string): Promise<void> {
17+
const spawnedProcess = pr.spawn(cmd, args);
18+
await new Promise((resolve, reject) => {
19+
spawnedProcess.on("exit", resolve);
20+
spawnedProcess.on("error", (err: Error) => {
21+
log.info(errorMessage);
22+
reject(err);
23+
});
24+
});
25+
}
26+
27+
async function deleteDockerContainers(deleteContainerNames?: string[]): Promise<void> {
28+
if (deleteContainerNames) {
29+
log.info(`Cleanup: deleting ${deleteContainerNames.join(", ")} docker containers`);
30+
await spawnCMD(
31+
"docker",
32+
["rm", ...deleteContainerNames, "-f"],
33+
`Attempted to delete ${deleteContainerNames.join(
34+
", "
35+
)} docker containers but encountered an error doing so`
36+
);
37+
}
38+
}
39+
40+
async function deleteDockerImages(dockerImageNames?: string[]) {
41+
if (dockerImageNames) {
42+
log.info(`Cleanup: deleting ${dockerImageNames.join(", ")} docker images`);
43+
await spawnCMD(
44+
"docker",
45+
["rmi", ...dockerImageNames, "-f"],
46+
`Attempted to delete the ${dockerImageNames.join(
47+
", "
48+
)} docker images but encountered an error doing so`
49+
);
50+
}
51+
}
52+
53+
async function deleteDockerContext(dockerContextDirectory?: string) {
54+
if (dockerContextDirectory) {
55+
log.info(`Cleanup: deleting the ${dockerContextDirectory} docker context directory`);
56+
await spawnCMD("rm", ["-rf", dockerContextDirectory], undefined);
57+
}
58+
}
59+
60+
async function cleanup(
61+
dockerContextDirectory?: string,
62+
dockerContainerNames?: string[],
63+
dockerImageNames?: string[]
64+
) {
65+
await deleteDockerContext(dockerContextDirectory);
66+
await deleteDockerContainers(dockerContainerNames);
67+
await deleteDockerImages(dockerImageNames);
68+
}
69+
70+
function buildRunSamplesScript(
71+
containerWorkspacePath: string,
72+
artifactName: string,
73+
envFileName: string,
74+
logFilePath?: string
75+
) {
76+
function compileCMD(cmd: string, printToScreen?: boolean) {
77+
return printToScreen ? cmd : `${cmd} >> ${logFilePath} 2>&1`;
78+
}
79+
const printToScreen = logFilePath === undefined;
80+
const artifactPath = `${containerWorkspacePath}/${artifactName}`;
81+
const envFilePath = `${containerWorkspacePath}/${envFileName}`;
82+
const javascriptSamplesPath = `${containerWorkspacePath}/samples/javascript`;
83+
const typescriptCompiledSamplesPath = `${containerWorkspacePath}/samples/typescript/dist`;
84+
const scriptContent = `#!/bin/sh
85+
86+
function install_dependencies_helper() {
87+
local samples_path=\$1;
88+
cd \${samples_path};
89+
${compileCMD(`npm install ${artifactPath}`, printToScreen)}
90+
${compileCMD(`npm install`, printToScreen)}
91+
}
92+
93+
function install_packages() {
94+
echo "Using node \$(node -v) to install dependencies";
95+
install_dependencies_helper ${containerWorkspacePath}/samples/javascript
96+
install_dependencies_helper ${containerWorkspacePath}/samples/typescript;
97+
cp ${envFilePath} ${containerWorkspacePath}/samples/javascript/;
98+
}
99+
100+
function run_samples() {
101+
samples_path=\$1;
102+
echo "Using node \$(node -v) to run samples in \${samples_path}";
103+
cd "\${samples_path}";
104+
for SAMPLE in *.js; do
105+
node \${SAMPLE};
106+
done
107+
}
108+
109+
function build_typescript() {
110+
echo "Using node \$(node -v) to build the typescript samples";
111+
cd ${containerWorkspacePath}/samples/typescript
112+
${compileCMD(`npm run build`, printToScreen)}
113+
cp ${envFilePath} ${containerWorkspacePath}/samples/typescript/dist/
114+
}
115+
116+
function main() {
117+
install_packages;
118+
run_samples "${javascriptSamplesPath}";
119+
build_typescript && run_samples "${typescriptCompiledSamplesPath}";
120+
}
121+
122+
main`;
123+
return scriptContent;
124+
}
125+
126+
function createDockerContextDirectory(
127+
dockerContextDirectory: string,
128+
containerWorkspacePath: string,
129+
samples_path: string,
130+
envPath: string,
131+
artifactPath?: string,
132+
logFilePath?: string
133+
): void {
134+
if (artifactPath === undefined) {
135+
throw new Error("artifact_path is a required argument but it was not passed");
136+
} else if (!fs.existsSync(artifactPath)) {
137+
throw new Error(`artifact path passed does not exist: ${artifactPath}`);
138+
}
139+
const artifactName = path.basename(artifactPath);
140+
const envFileName = path.basename(envPath);
141+
fs.copySync(samples_path, path.join(dockerContextDirectory, "samples"));
142+
fs.copyFileSync(artifactPath, path.join(dockerContextDirectory, artifactName));
143+
fs.copyFileSync(envPath, path.join(dockerContextDirectory, envFileName));
144+
fs.writeFileSync(
145+
path.join(dockerContextDirectory, "run_samples.sh"),
146+
buildRunSamplesScript(containerWorkspacePath, artifactName, envFileName, logFilePath),
147+
{ mode: S_IRWXO }
148+
);
149+
}
150+
151+
async function runDockerContainer(
152+
dockerContextDirectory: string,
153+
dockerImageName: string,
154+
dockerContainerName: string,
155+
containerWorkspace: string,
156+
stdoutListener: (chunk: string | Buffer) => void,
157+
stderrListener: (chunk: string | Buffer) => void
158+
): Promise<void> {
159+
const args = [
160+
"run",
161+
"--name",
162+
dockerContainerName,
163+
"--workdir",
164+
containerWorkspace,
165+
"-v",
166+
`${dockerContextDirectory}:${containerWorkspace}`,
167+
dockerImageName,
168+
"./run_samples.sh"
169+
];
170+
const dockerContainerRunProcess = pr.spawn("docker", args, {
171+
cwd: dockerContextDirectory
172+
});
173+
log.info(`Started running the docker container ${dockerContainerName}`);
174+
dockerContainerRunProcess.stdout.on("data", stdoutListener);
175+
dockerContainerRunProcess.stderr.on("data", stderrListener);
176+
const exitCode = await new Promise((resolve, reject) => {
177+
dockerContainerRunProcess.on("exit", resolve);
178+
dockerContainerRunProcess.on("error", reject);
179+
});
180+
if (exitCode === 0) {
181+
log.info(`Docker container ${dockerContainerName} finished running`);
182+
} else {
183+
log.error(`Docker container ${dockerContainerName} encountered an error`);
184+
}
185+
}
186+
187+
export const commandInfo = makeCommandInfo(
188+
"check-node-versions",
189+
"execute samples with different node versions, typically in preparation for release",
190+
{
191+
"artifact-path": {
192+
kind: "string",
193+
description: "Path to the downloaded artifact built by the release pipeline"
194+
},
195+
directory: {
196+
kind: "string",
197+
description: "Base dir, default is process.cwd()",
198+
default: process.cwd()
199+
},
200+
"node-versions": {
201+
kind: "string",
202+
description: "A comma separated list of node versions to use",
203+
default: "8,10,12"
204+
},
205+
"context-directory-path": {
206+
kind: "string",
207+
description: "Absolute path to a directory used for mounting inside docker containers",
208+
default: ""
209+
},
210+
"keep-docker-context": {
211+
kind: "boolean",
212+
description: "Boolean to indicate whether to keep the current docker context directory",
213+
default: false
214+
},
215+
"log-in-file": {
216+
kind: "boolean",
217+
description:
218+
"Boolean to indicate whether to save the the stdout and sterr for npm commands to the log.txt log file",
219+
default: true
220+
},
221+
"use-existing-docker-containers": {
222+
kind: "boolean",
223+
description: "Boolean to indicate whether to use existing docker containers if any",
224+
default: false
225+
},
226+
"keep-docker-containers": {
227+
kind: "boolean",
228+
description: "Boolean to indicate whether to keep docker containers",
229+
default: false
230+
},
231+
"keep-docker-images": {
232+
kind: "boolean",
233+
description: "Boolean to indicate whether to keep the downloaded docker images",
234+
default: false
235+
}
236+
}
237+
);
238+
239+
export default leafCommand(commandInfo, async (options) => {
240+
const nodeVersions = options["node-versions"]?.split(",");
241+
const dockerContextDirectory: string =
242+
options["context-directory-path"] === ""
243+
? await fs.mkdtemp(path.join(os.tmpdir(), "context"))
244+
: options["context-directory-path"];
245+
const pkg = await resolveProject(options.directory);
246+
const samplesPath = path.join(pkg.path, "samples");
247+
const envFilePath = path.join(pkg.path, ".env");
248+
const keepDockerContextDirectory = options["keep-docker-context"];
249+
const dockerImageNames = nodeVersions.map((version: string) => `node:${version}-alpine`);
250+
const dockerContainerNames = nodeVersions.map((version: string) => `${version}-container`);
251+
const containerWorkspace = "/workspace";
252+
const containerLogFilePath = options["log-in-file"] ? `${containerWorkspace}/log.txt` : undefined;
253+
const useExistingDockerContainer = options["use-existing-docker-containers"];
254+
const keepDockerContainers = options["keep-docker-containers"];
255+
const keepDockerImages = options["keep-docker-images"];
256+
const stdoutListener = (chunk: Buffer | string) => log.info(chunk.toString());
257+
const stderrListener = (chunk: Buffer | string) => log.error(chunk.toString());
258+
async function cleanupBefore(): Promise<void> {
259+
const dockerContextDirectoryChildren = await fs.readdir(dockerContextDirectory);
260+
await cleanup(
261+
// If the directory is empty, we will not delete it.
262+
dockerContextDirectoryChildren.length === 0 ? undefined : dockerContextDirectory,
263+
useExistingDockerContainer ? undefined : dockerContainerNames,
264+
// Do not delete the image
265+
undefined
266+
);
267+
}
268+
async function cleanupAfter(): Promise<void> {
269+
await cleanup(
270+
keepDockerContextDirectory ? undefined : dockerContextDirectory,
271+
keepDockerContainers ? undefined : dockerContainerNames,
272+
keepDockerImages ? undefined : dockerImageNames
273+
);
274+
}
275+
function createDockerContextDirectoryThunk(): void {
276+
createDockerContextDirectory(
277+
dockerContextDirectory,
278+
containerWorkspace,
279+
samplesPath,
280+
envFilePath,
281+
options["artifact-path"],
282+
containerLogFilePath
283+
);
284+
}
285+
async function runContainers(): Promise<void> {
286+
const containerRuns = dockerImageNames.map((imageName, containerIndex) => () =>
287+
runDockerContainer(
288+
dockerContextDirectory,
289+
imageName,
290+
dockerContainerNames[containerIndex],
291+
containerWorkspace,
292+
stdoutListener,
293+
stderrListener
294+
)
295+
);
296+
for (const run of containerRuns) {
297+
await run();
298+
}
299+
}
300+
await cleanupBefore();
301+
createDockerContextDirectoryThunk();
302+
await runContainers();
303+
await cleanupAfter();
304+
305+
return true;
306+
});

common/tools/dev-tool/src/commands/samples/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export default subCommand(commandInfo, {
99
dev: () => import("./dev"),
1010
prep: () => import("./prep"),
1111
run: () => import("./run"),
12-
"ts-to-js": () => import("./tsToJs")
12+
"ts-to-js": () => import("./tsToJs"),
13+
"check-node-versions": () => import("./checkNodeVersions")
1314
});

0 commit comments

Comments
 (0)