|
| 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 | +}); |
0 commit comments