UNPKG

35.6 kBJavaScriptView Raw
1'use strict';
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 return new (P || (P = Promise))(function (resolve, reject) {
4 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
7 step((generator = generator.apply(thisArg, _arguments || [])).next());
8 });
9};
10const ip = require('ip');
11const fs = require('fs-extra');
12const tar = require('tar-fs');
13const nas = require('./nas');
14const path = require('path');
15const debug = require('debug')('fun:local');
16const Docker = require('dockerode');
17const docker = new Docker();
18const dockerOpts = require('./docker-opts');
19const getVisitor = require('./visitor').getVisitor;
20const getProfile = require('./profile').getProfile;
21const { generatePwdFile } = require('./utils/passwd');
22const { blue, red, yellow } = require('colors');
23const { getRootBaseDir } = require('./tpl');
24const { parseArgsStringToArgv } = require('string-argv');
25const { extractNasMappingsFromNasYml } = require('./nas/support');
26const { addEnv, addInstallTargetEnv, resolveLibPathsFromLdConf } = require('./install/env');
27const { findPathsOutofSharedPaths } = require('./docker-support');
28const { processorTransformFactory } = require('./error-processor');
29const isWin = process.platform === 'win32';
30const _ = require('lodash');
31require('draftlog').into(console);
32var containers = new Set();
33const devnull = require('dev-null');
34// exit container, when use ctrl + c
35function waitingForContainerStopped() {
36 // see https://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js
37 const isRaw = process.isRaw;
38 const kpCallBack = (_char, key) => {
39 if (key & key.ctrl && key.name === 'c') {
40 process.emit('SIGINT');
41 }
42 };
43 if (process.platform === 'win32') {
44 if (process.stdin.isTTY) {
45 process.stdin.setRawMode(isRaw);
46 }
47 process.stdin.on('keypress', kpCallBack);
48 }
49 let stopping = false;
50 process.on('SIGINT', () => __awaiter(this, void 0, void 0, function* () {
51 debug('containers length: ', containers.length);
52 if (stopping) {
53 return;
54 }
55 // Just fix test on windows
56 // Because process.emit('SIGINT') in test/docker.test.js will not trigger rl.on('SIGINT')
57 // And when listening to stdin the process never finishes until you send a SIGINT signal explicitly.
58 process.stdin.destroy();
59 if (!containers.size) {
60 return;
61 }
62 stopping = true;
63 console.log(`\nreceived canncel request, stopping running containers.....`);
64 const jobs = [];
65 for (let container of containers) {
66 try {
67 if (container.destroy) { // container stream
68 container.destroy();
69 }
70 else {
71 const c = docker.getContainer(container);
72 console.log(`stopping container ${container}`);
73 jobs.push(c.kill().catch(ex => debug('kill container instance error, error is', ex)));
74 }
75 }
76 catch (error) {
77 debug('get container instance error, ignore container to stop, error is', error);
78 }
79 }
80 try {
81 yield Promise.all(jobs);
82 console.log('all containers stopped');
83 }
84 catch (error) {
85 console.error(error);
86 process.exit(-1); // eslint-disable-line
87 }
88 }));
89 return () => {
90 process.stdin.removeListener('keypress', kpCallBack);
91 if (process.stdin.isTTY) {
92 process.stdin.setRawMode(isRaw);
93 }
94 };
95}
96const goThrough = waitingForContainerStopped();
97const { generateVscodeDebugConfig, generateDebugEnv } = require('./debug');
98// todo: add options for pull latest image
99const skipPullImage = true;
100function resolveNasConfigToMounts(baseDir, serviceName, nasConfig, nasBaseDir) {
101 return __awaiter(this, void 0, void 0, function* () {
102 const nasMappings = yield nas.convertNasConfigToNasMappings(nasBaseDir, nasConfig, serviceName);
103 return convertNasMappingsToMounts(getRootBaseDir(baseDir), nasMappings);
104 });
105}
106function resolveTmpDirToMount(absTmpDir) {
107 return __awaiter(this, void 0, void 0, function* () {
108 if (!absTmpDir) {
109 return {};
110 }
111 return {
112 Type: 'bind',
113 Source: absTmpDir,
114 Target: '/tmp',
115 ReadOnly: false
116 };
117 });
118}
119function resolveDebuggerPathToMount(debuggerPath) {
120 return __awaiter(this, void 0, void 0, function* () {
121 if (!debuggerPath) {
122 return {};
123 }
124 const absDebuggerPath = path.resolve(debuggerPath);
125 return {
126 Type: 'bind',
127 Source: absDebuggerPath,
128 Target: '/tmp/debugger_files',
129 ReadOnly: false
130 };
131 });
132}
133// todo: 当前只支持目录以及 jar。code uri 还可能是 oss 地址、目录、jar、zip?
134function resolveCodeUriToMount(absCodeUri, readOnly = true) {
135 return __awaiter(this, void 0, void 0, function* () {
136 let target = null;
137 const stats = yield fs.lstat(absCodeUri);
138 if (stats.isDirectory()) {
139 target = '/code';
140 }
141 else {
142 // could not use path.join('/code', xxx)
143 // in windows, it will be translate to \code\xxx, and will not be recorgnized as a valid path in linux container
144 target = path.posix.join('/code', path.basename(absCodeUri));
145 }
146 // Mount the code directory as read only
147 return {
148 Type: 'bind',
149 Source: absCodeUri,
150 Target: target,
151 ReadOnly: readOnly
152 };
153 });
154}
155function resolvePasswdMount() {
156 return __awaiter(this, void 0, void 0, function* () {
157 if (process.platform === 'linux') {
158 return {
159 Type: 'bind',
160 Source: yield generatePwdFile(),
161 Target: '/etc/passwd',
162 ReadOnly: true
163 };
164 }
165 return null;
166 });
167}
168function convertNasMappingsToMounts(baseDir, nasMappings) {
169 return nasMappings.map(nasMapping => {
170 console.log('mounting local nas mock dir %s into container %s\n', nasMapping.localNasDir, nasMapping.remoteNasDir);
171 return {
172 Type: 'bind',
173 Source: path.resolve(baseDir, nasMapping.localNasDir),
174 Target: nasMapping.remoteNasDir,
175 ReadOnly: false
176 };
177 });
178}
179function resolveNasYmlToMount(baseDir, serviceName) {
180 return __awaiter(this, void 0, void 0, function* () {
181 const nasMappings = yield extractNasMappingsFromNasYml(baseDir, serviceName);
182 return convertNasMappingsToMounts(getRootBaseDir(baseDir), nasMappings);
183 });
184}
185function conventInstallTargetsToMounts(installTargets) {
186 if (!installTargets) {
187 return [];
188 }
189 const mounts = [];
190 _.forEach(installTargets, (target) => {
191 const { hostPath, containerPath } = target;
192 if (!(fs.pathExistsSync(hostPath))) {
193 fs.ensureDirSync(hostPath);
194 }
195 mounts.push({
196 Type: 'bind',
197 Source: hostPath,
198 Target: containerPath,
199 ReadOnly: false
200 });
201 });
202 return mounts;
203}
204function imageExist(imageName) {
205 return __awaiter(this, void 0, void 0, function* () {
206 const images = yield docker.listImages({
207 filters: {
208 reference: [imageName]
209 }
210 });
211 return images.length > 0;
212 });
213}
214function listContainers(options) {
215 return __awaiter(this, void 0, void 0, function* () {
216 return yield docker.listContainers(options);
217 });
218}
219function getContainer(containerId) {
220 return __awaiter(this, void 0, void 0, function* () {
221 return yield docker.getContainer(containerId);
222 });
223}
224function renameContainer(container, name) {
225 return __awaiter(this, void 0, void 0, function* () {
226 return yield container.rename({
227 name
228 });
229 });
230}
231// dockerode exec 在 windows 上有问题,用 exec 的 stdin 传递事件,当调用 stream.end() 时,会直接导致 exec 退出,且 ExitCode 为 null
232function generateDockerCmd(functionProps, httpMode, invokeInitializer = true, event = null) {
233 const cmd = ['-h', functionProps.Handler];
234 // 如果提供了 event
235 if (event !== null) {
236 cmd.push('--event', Buffer.from(event).toString('base64'));
237 cmd.push('--event-decode');
238 }
239 else {
240 // always pass event using stdin mode
241 cmd.push('--stdin');
242 }
243 if (httpMode) {
244 cmd.push('--http');
245 }
246 const initializer = functionProps.Initializer;
247 if (initializer && invokeInitializer) {
248 cmd.push('-i', initializer);
249 }
250 const initializationTimeout = functionProps.InitializationTimeout;
251 // initializationTimeout is defined as integer, see lib/validate/schema/function.js
252 if (initializationTimeout) {
253 cmd.push('--initializationTimeout', initializationTimeout.toString());
254 }
255 debug(`docker cmd: ${cmd}`);
256 return cmd;
257}
258function followProgress(stream, onFinished) {
259 const barLines = {};
260 const onProgress = (event) => {
261 let status = event.status;
262 if (event.progress) {
263 status = `${event.status} ${event.progress}`;
264 }
265 if (event.id) {
266 const id = event.id;
267 if (!barLines[id]) {
268 barLines[id] = console.draft();
269 }
270 barLines[id](id + ': ' + status);
271 }
272 else {
273 if (_.has(event, 'aux.ID')) {
274 event.stream = event.aux.ID + '\n';
275 }
276 // If there is no id, the line should be wrapped manually.
277 const out = event.status ? event.status + '\n' : event.stream;
278 process.stdout.write(out);
279 }
280 };
281 docker.modem.followProgress(stream, onFinished, onProgress);
282}
283function pullImage(imageName) {
284 return __awaiter(this, void 0, void 0, function* () {
285 const resolveImageName = yield dockerOpts.resolveImageNameForPull(imageName);
286 // copied from lib/edge/container.js
287 const startTime = new Date();
288 const stream = yield docker.pull(resolveImageName);
289 const visitor = yield getVisitor();
290 visitor.event({
291 ec: 'image',
292 ea: 'pull',
293 el: 'start'
294 }).send();
295 const registry = yield dockerOpts.resolveDockerRegistry();
296 return yield new Promise((resolve, reject) => {
297 console.log(`begin pulling image ${resolveImageName}, you can also use ` + yellow(`'docker pull ${resolveImageName}'`) + ' to pull image by yourself.');
298 const onFinished = (err) => __awaiter(this, void 0, void 0, function* () {
299 containers.delete(stream);
300 const pullDuration = parseInt((new Date() - startTime) / 1000);
301 if (err) {
302 visitor.event({
303 ec: 'image',
304 ea: 'pull',
305 el: 'error'
306 }).send();
307 visitor.event({
308 ec: 'image',
309 ea: `pull from ${registry}`,
310 el: 'error'
311 }).send();
312 visitor.event({
313 ec: `image pull from ${registry}`,
314 ea: `used ${pullDuration}`,
315 el: 'error'
316 }).send();
317 reject(err);
318 return;
319 }
320 visitor.event({
321 ec: 'image',
322 ea: `pull from ${registry}`,
323 el: 'success'
324 }).send();
325 visitor.event({
326 ec: 'image',
327 ea: 'pull',
328 el: 'success'
329 }).send();
330 visitor.event({
331 ec: `image pull from ${registry}`,
332 ea: `used ${pullDuration}`,
333 el: 'success'
334 }).send();
335 for (const r of dockerOpts.DOCKER_REGISTRIES) {
336 if (resolveImageName.indexOf(r) === 0) {
337 const image = yield docker.getImage(resolveImageName);
338 const newImageName = resolveImageName.slice(r.length + 1);
339 const repoTag = newImageName.split(':');
340 // rename
341 yield image.tag({
342 name: resolveImageName,
343 repo: _.first(repoTag),
344 tag: _.last(repoTag)
345 });
346 break;
347 }
348 }
349 resolve(resolveImageName);
350 });
351 containers.add(stream);
352 // pull image progress
353 followProgress(stream, onFinished);
354 });
355 });
356}
357function generateFunctionEnvs(functionProps) {
358 const environmentVariables = functionProps.EnvironmentVariables;
359 if (!environmentVariables) {
360 return {};
361 }
362 return Object.assign({}, environmentVariables);
363}
364function generateRamdomContainerName() {
365 return `fun_local_${new Date().getTime()}_${Math.random().toString(36).substr(2, 7)}`;
366}
367function generateDockerEnvs(baseDir, serviceName, serviceProps, functionName, functionProps, debugPort, httpParams, nasConfig, ishttpTrigger, debugIde, debugArgs) {
368 return __awaiter(this, void 0, void 0, function* () {
369 const envs = {};
370 if (httpParams) {
371 Object.assign(envs, {
372 'FC_HTTP_PARAMS': httpParams
373 });
374 }
375 const confEnv = yield resolveLibPathsFromLdConf(baseDir, functionProps.CodeUri);
376 Object.assign(envs, confEnv);
377 const runtime = functionProps.Runtime;
378 if (debugPort && !debugArgs) {
379 const debugEnv = generateDebugEnv(runtime, debugPort, debugIde);
380 Object.assign(envs, debugEnv);
381 }
382 else if (debugArgs) {
383 Object.assign(envs, {
384 DEBUG_OPTIONS: debugArgs
385 });
386 }
387 if (ishttpTrigger && runtime === 'java8') {
388 envs['fc_enable_new_java_ca'] = 'true';
389 }
390 Object.assign(envs, generateFunctionEnvs(functionProps));
391 const profile = yield getProfile();
392 Object.assign(envs, {
393 'local': true,
394 'FC_ACCESS_KEY_ID': profile.accessKeyId,
395 'FC_ACCESS_KEY_SECRET': profile.accessKeySecret,
396 'FC_ACCOUND_ID': profile.accountId,
397 'FC_REGION': profile.defaultRegion,
398 'FC_FUNCTION_NAME': functionName,
399 'FC_HANDLER': functionProps.Handler,
400 'FC_MEMORY_SIZE': functionProps.MemorySize || 128,
401 'FC_TIMEOUT': functionProps.Timeout || 3,
402 'FC_INITIALIZER': functionProps.Initializer,
403 'FC_INITIALIZATIONIMEOUT': functionProps.InitializationTimeout || 3,
404 'FC_SERVICE_NAME': serviceName,
405 'FC_SERVICE_LOG_PROJECT': ((serviceProps || {}).LogConfig || {}).Project,
406 'FC_SERVICE_LOG_STORE': ((serviceProps || {}).LogConfig || {}).Logstore
407 });
408 return addEnv(envs, nasConfig);
409 });
410}
411function pullImageIfNeed(imageName) {
412 return __awaiter(this, void 0, void 0, function* () {
413 const exist = yield imageExist(imageName);
414 if (!exist || !skipPullImage) {
415 yield pullImage(imageName);
416 }
417 else {
418 debug(`skip pulling image ${imageName}...`);
419 console.log(`skip pulling image ${imageName}...`);
420 }
421 });
422}
423function showDebugIdeTipsForVscode(serviceName, functionName, runtime, codeSource, debugPort) {
424 return __awaiter(this, void 0, void 0, function* () {
425 const vscodeDebugConfig = yield generateVscodeDebugConfig(serviceName, functionName, runtime, codeSource, debugPort);
426 // todo: auto detect .vscode/launch.json in codeuri path.
427 console.log(blue('you can paste these config to .vscode/launch.json, and then attach to your running function'));
428 console.log('///////////////// config begin /////////////////');
429 console.log(JSON.stringify(vscodeDebugConfig, null, 4));
430 console.log('///////////////// config end /////////////////');
431 });
432}
433function showDebugIdeTipsForPycharm(codeSource, debugPort) {
434 return __awaiter(this, void 0, void 0, function* () {
435 const stats = yield fs.lstat(codeSource);
436 if (!stats.isDirectory()) {
437 codeSource = path.dirname(codeSource);
438 }
439 console.log(yellow(`\n========= Tips for PyCharm remote debug =========
440Local host name: ${ip.address()}
441Port : ${yellow(debugPort)}
442Path mappings : ${yellow(codeSource)}=/code
443
444Debug Code needed to copy to your function code:
445
446import pydevd
447pydevd.settrace('${ip.address()}', port=${debugPort}, stdoutToServer=True, stderrToServer=True)
448
449=========================================================================\n`));
450 });
451}
452function writeEventToStreamAndClose(stream, event) {
453 if (event) {
454 stream.write(event);
455 }
456 stream.end();
457}
458function isDockerToolBoxAndEnsureDockerVersion() {
459 return __awaiter(this, void 0, void 0, function* () {
460 const dockerInfo = yield docker.info();
461 yield detectDockerVersion(dockerInfo.ServerVersion || '');
462 const obj = (dockerInfo.Labels || []).map(e => _.split(e, '=', 2))
463 .filter(e => e.length === 2)
464 .reduce((acc, cur) => (acc[cur[0]] = cur[1], acc), {});
465 return process.platform === 'win32' && obj.provider === 'virtualbox';
466 });
467}
468function run(opts, event, outputStream, errorStream, context = {}) {
469 return __awaiter(this, void 0, void 0, function* () {
470 const container = yield createContainer(opts);
471 const attachOpts = {
472 hijack: true,
473 stream: true,
474 stdin: true,
475 stdout: true,
476 stderr: true
477 };
478 const stream = yield container.attach(attachOpts);
479 if (!outputStream) {
480 outputStream = process.stdout;
481 }
482 if (!errorStream) {
483 errorStream = process.stderr;
484 }
485 const errorTransform = processorTransformFactory({
486 serviceName: context.serviceName,
487 functionName: context.functionName,
488 errorStream: errorStream
489 });
490 if (!isWin) {
491 container.modem.demuxStream(stream, outputStream, errorTransform);
492 }
493 yield container.start();
494 // dockerode bugs on windows. attach could not receive output and error
495 if (isWin) {
496 const logStream = yield container.logs({
497 stdout: true,
498 stderr: true,
499 follow: true
500 });
501 container.modem.demuxStream(logStream, outputStream, errorTransform);
502 }
503 containers.add(container.id);
504 writeEventToStreamAndClose(stream, event);
505 // exitRs format: {"Error":null,"StatusCode":0}
506 // see https://docs.docker.com/engine/api/v1.37/#operation/ContainerWait
507 const exitRs = yield container.wait();
508 containers.delete(container.id);
509 return exitRs;
510 });
511}
512function createContainer(opts) {
513 return __awaiter(this, void 0, void 0, function* () {
514 const isWin = process.platform === 'win32';
515 const isMac = process.platform === 'darwin';
516 if (opts && isMac) {
517 if (opts.HostConfig) {
518 const pathsOutofSharedPaths = yield findPathsOutofSharedPaths(opts.HostConfig.Mounts);
519 if (isMac && pathsOutofSharedPaths.length > 0) {
520 throw new Error(red(`Please add directory '${pathsOutofSharedPaths}' to Docker File sharing list, more information please refer to https://github.com/alibaba/funcraft/blob/master/docs/usage/faq-zh.md`));
521 }
522 }
523 }
524 const dockerToolBox = yield isDockerToolBoxAndEnsureDockerVersion();
525 let container;
526 try {
527 // see https://github.com/apocas/dockerode/pull/38
528 container = yield docker.createContainer(opts);
529 }
530 catch (ex) {
531 if (ex.message.indexOf('invalid mount config for type') !== -1 && dockerToolBox) {
532 throw new Error(red(`The default host machine path for docker toolbox is under 'C:\\Users', Please make sure your project is in this directory. If you want to mount other disk paths, please refer to https://github.com/alibaba/funcraft/blob/master/docs/usage/faq-zh.md .`));
533 }
534 if (ex.message.indexOf('drive is not shared') !== -1 && isWin) {
535 throw new Error(red(`${ex.message}More information please refer to https://docs.docker.com/docker-for-windows/#shared-drives`));
536 }
537 throw ex;
538 }
539 return container;
540 });
541}
542function createAndRunContainer(opts) {
543 return __awaiter(this, void 0, void 0, function* () {
544 const container = yield createContainer(opts);
545 containers.add(container.id);
546 yield container.start({});
547 return container;
548 });
549}
550function execContainer(container, opts, outputStream, errorStream) {
551 return __awaiter(this, void 0, void 0, function* () {
552 outputStream = process.stdout;
553 errorStream = process.stderr;
554 const logStream = yield container.logs({
555 stdout: true,
556 stderr: true,
557 follow: true,
558 since: (new Date().getTime() / 1000)
559 });
560 container.modem.demuxStream(logStream, outputStream, errorStream);
561 const exec = yield container.exec(opts);
562 const stream = yield exec.start();
563 // have to wait, otherwise stdin may not be readable
564 yield new Promise(resolve => setTimeout(resolve, 30));
565 container.modem.demuxStream(stream, outputStream, errorStream);
566 yield waitForExec(exec);
567 logStream.destroy();
568 });
569}
570function waitForExec(exec) {
571 return __awaiter(this, void 0, void 0, function* () {
572 return yield new Promise((resolve, reject) => {
573 // stream.on('end') could not receive end event on windows.
574 // so use inspect to check exec exit
575 function waitContainerExec() {
576 exec.inspect((err, data) => {
577 if (data.Running) {
578 setTimeout(waitContainerExec, 100);
579 return;
580 }
581 if (err) {
582 reject(err);
583 }
584 else if (data.ExitCode !== 0) {
585 reject(`${data.ProcessConfig.entrypoint} exited with code ${data.ExitCode}`);
586 }
587 else {
588 resolve(data.ExitCode);
589 }
590 });
591 }
592 waitContainerExec();
593 });
594 });
595}
596// outputStream, errorStream used for http invoke
597// because agent is started when container running and exec could not receive related logs
598function startContainer(opts, outputStream, errorStream, context = {}) {
599 return __awaiter(this, void 0, void 0, function* () {
600 const container = yield createContainer(opts);
601 containers.add(container.id);
602 try {
603 yield container.start({});
604 }
605 catch (err) {
606 console.error(err);
607 }
608 const logs = outputStream || errorStream;
609 if (logs) {
610 if (!outputStream) {
611 outputStream = devnull();
612 }
613 if (!errorStream) {
614 errorStream = devnull();
615 }
616 // dockerode bugs on windows. attach could not receive output and error, must use logs
617 const logStream = yield container.logs({
618 stdout: true,
619 stderr: true,
620 follow: true
621 });
622 container.modem.demuxStream(logStream, outputStream, processorTransformFactory({
623 serviceName: context.serviceName,
624 functionName: context.functionName,
625 errorStream
626 }));
627 }
628 return {
629 stop: () => __awaiter(this, void 0, void 0, function* () {
630 yield container.stop();
631 containers.delete(container.id);
632 }),
633 exec: (cmd, { cwd = '', env = {}, outputStream, errorStream, verbose = false, context = {}, event = null } = {}) => __awaiter(this, void 0, void 0, function* () {
634 const stdin = event ? true : false;
635 const options = {
636 Cmd: cmd,
637 Env: dockerOpts.resolveDockerEnv(env),
638 Tty: false,
639 AttachStdin: stdin,
640 AttachStdout: true,
641 AttachStderr: true,
642 WorkingDir: cwd
643 };
644 // docker exec
645 debug('docker exec opts: ' + JSON.stringify(options, null, 4));
646 const exec = yield container.exec(options);
647 const stream = yield exec.start({ hijack: true, stdin });
648 // todo: have to wait, otherwise stdin may not be readable
649 yield new Promise(resolve => setTimeout(resolve, 30));
650 if (event !== null) {
651 writeEventToStreamAndClose(stream, event);
652 }
653 if (!outputStream) {
654 outputStream = process.stdout;
655 }
656 if (!errorStream) {
657 errorStream = process.stderr;
658 }
659 if (verbose) {
660 container.modem.demuxStream(stream, outputStream, errorStream);
661 }
662 else {
663 container.modem.demuxStream(stream, devnull(), errorStream);
664 }
665 return yield waitForExec(exec);
666 })
667 };
668 });
669}
670function startInstallationContainer({ runtime, imageName, codeUri, targets, context }) {
671 return __awaiter(this, void 0, void 0, function* () {
672 debug(`runtime: ${runtime}`);
673 debug(`codeUri: ${codeUri}`);
674 if (yield isDockerToolBoxAndEnsureDockerVersion()) {
675 throw new Error(red(`\nWe detected that you are using docker toolbox. For a better experience, please upgrade 'docker for windows'.\nYou can refer to Chinese doc https://github.com/alibaba/funcraft/blob/master/docs/usage/installation-zh.md#windows-%E5%AE%89%E8%A3%85-docker or English doc https://github.com/alibaba/funcraft/blob/master/docs/usage/installation.md.`));
676 }
677 if (!imageName) {
678 imageName = yield dockerOpts.resolveRuntimeToDockerImage(runtime, true);
679 if (!imageName) {
680 throw new Error(`invalid runtime name ${runtime}`);
681 }
682 }
683 const codeMount = yield resolveCodeUriToMount(codeUri, false);
684 const installMounts = conventInstallTargetsToMounts(targets);
685 const passwdMount = yield resolvePasswdMount();
686 const mounts = _.compact([codeMount, ...installMounts, passwdMount]);
687 yield pullImageIfNeed(imageName);
688 const envs = addInstallTargetEnv({}, targets);
689 const opts = dockerOpts.generateInstallOpts(imageName, mounts, envs);
690 return yield startContainer(opts);
691 });
692}
693function displaySboxTips(runtime) {
694 console.log(yellow(`\nWelcom to fun sbox environment.\n`));
695 console.log(yellow(`You can install system dependencies like this:`));
696 console.log(yellow(`fun-install apt-get install libxss1\n`));
697 switch (runtime) {
698 case 'nodejs6':
699 case 'nodejs8':
700 case 'nodejs10':
701 console.log(yellow(`You can install node modules like this:`));
702 console.log(yellow(`fun-install npm install puppeteer\n`));
703 break;
704 case 'python2.7':
705 case 'python3':
706 console.log(yellow(`You can install pip dependencies like this:`));
707 console.log(yellow(`fun-install pip install flask`));
708 break;
709 default:
710 break;
711 }
712 console.log(yellow('type \'fun-install --help\' for more help\n'));
713}
714function startSboxContainer({ runtime, imageName, mounts, cmd, envs, isTty, isInteractive }) {
715 return __awaiter(this, void 0, void 0, function* () {
716 debug(`runtime: ${runtime}`);
717 debug(`mounts: ${mounts}`);
718 debug(`isTty: ${isTty}`);
719 debug(`isInteractive: ${isInteractive}`);
720 if (!imageName) {
721 imageName = yield dockerOpts.resolveRuntimeToDockerImage(runtime, true);
722 if (!imageName) {
723 throw new Error(`invalid runtime name ${runtime}`);
724 }
725 }
726 debug(`cmd: ${parseArgsStringToArgv(cmd || '')}`);
727 const container = yield createContainer(dockerOpts.generateSboxOpts({
728 imageName,
729 hostname: `fc-${runtime}`,
730 mounts,
731 envs,
732 cmd: parseArgsStringToArgv(cmd || ''),
733 isTty,
734 isInteractive
735 }));
736 containers.add(container.id);
737 yield container.start();
738 const stream = yield container.attach({
739 logs: true,
740 stream: true,
741 stdin: isInteractive,
742 stdout: true,
743 stderr: true
744 });
745 // show outputs
746 let logStream;
747 if (isTty) {
748 stream.pipe(process.stdout);
749 }
750 else {
751 if (isInteractive || process.platform === 'win32') {
752 // 这种情况很诡异,收不到 stream 的 stdout,使用 log 绕过去。
753 logStream = yield container.logs({
754 stdout: true,
755 stderr: true,
756 follow: true
757 });
758 container.modem.demuxStream(logStream, process.stdout, process.stderr);
759 }
760 else {
761 container.modem.demuxStream(stream, process.stdout, process.stderr);
762 }
763 }
764 if (isInteractive) {
765 displaySboxTips(runtime);
766 // Connect stdin
767 process.stdin.pipe(stream);
768 let previousKey;
769 const CTRL_P = '\u0010', CTRL_Q = '\u0011';
770 process.stdin.on('data', (key) => {
771 // Detects it is detaching a running container
772 const keyStr = key.toString('ascii');
773 if (previousKey === CTRL_P && keyStr === CTRL_Q) {
774 container.stop(() => { });
775 }
776 previousKey = keyStr;
777 });
778 }
779 let resize;
780 const isRaw = process.isRaw;
781 if (isTty) {
782 // fix not exit process in windows
783 goThrough();
784 process.stdin.setRawMode(true);
785 resize = () => __awaiter(this, void 0, void 0, function* () {
786 const dimensions = {
787 h: process.stdout.rows,
788 w: process.stdout.columns
789 };
790 if (dimensions.h !== 0 && dimensions.w !== 0) {
791 yield container.resize(dimensions);
792 }
793 });
794 yield resize();
795 process.stdout.on('resize', resize);
796 // 在不加任何 cmd 的情况下 shell prompt 需要输出一些字符才会显示,
797 // 这里输入一个空格+退格,绕过这个怪异的问题。
798 stream.write(' \b');
799 }
800 yield container.wait();
801 // cleanup
802 if (isTty) {
803 process.stdout.removeListener('resize', resize);
804 process.stdin.setRawMode(isRaw);
805 }
806 if (isInteractive) {
807 process.stdin.removeAllListeners();
808 process.stdin.unpipe(stream);
809 /**
810 * https://stackoverflow.com/questions/31716784/nodejs-process-never-ends-when-piping-the-stdin-to-a-child-process?rq=1
811 * https://github.com/nodejs/node/issues/2276
812 * */
813 process.stdin.destroy();
814 }
815 if (logStream) {
816 logStream.removeAllListeners();
817 }
818 stream.unpipe(process.stdout);
819 // fix not exit process in windows
820 // stream is hackji socks,so need to close
821 stream.destroy();
822 containers.delete(container.id);
823 if (!isTty) {
824 goThrough();
825 }
826 });
827}
828function zipTo(archive, to) {
829 return __awaiter(this, void 0, void 0, function* () {
830 yield fs.ensureDir(to);
831 yield new Promise((resolve, reject) => {
832 archive.pipe(tar.extract(to)).on('error', reject).on('finish', resolve);
833 });
834 });
835}
836function copyFromImage(imageName, from, to) {
837 return __awaiter(this, void 0, void 0, function* () {
838 const container = yield docker.createContainer({
839 Image: imageName
840 });
841 const archive = yield container.getArchive({
842 path: from
843 });
844 yield zipTo(archive, to);
845 yield container.remove();
846 });
847}
848function buildImage(dockerBuildDir, dockerfilePath, imageTag) {
849 return new Promise((resolve, reject) => {
850 var tarStream = tar.pack(dockerBuildDir);
851 docker.buildImage(tarStream, {
852 dockerfile: path.relative(dockerBuildDir, dockerfilePath),
853 t: imageTag
854 }, (error, stream) => {
855 containers.add(stream);
856 if (error) {
857 reject(error);
858 return;
859 }
860 stream.on('error', (e) => {
861 containers.delete(stream);
862 reject(e);
863 return;
864 });
865 stream.on('end', function () {
866 containers.delete(stream);
867 resolve(imageTag);
868 return;
869 });
870 followProgress(stream, (err, res) => err ? reject(err) : resolve(res));
871 });
872 });
873}
874function detectDockerVersion(serverVersion) {
875 return __awaiter(this, void 0, void 0, function* () {
876 let cur = serverVersion.split('.');
877 // 1.13.1
878 if (Number.parseInt(cur[0]) === 1 && Number.parseInt(cur[1]) <= 13) {
879 throw new Error(red(`\nWe detected that your docker version is ${serverVersion}, for a better experience, please upgrade the docker version.`));
880 }
881 });
882}
883module.exports = {
884 imageExist, generateDockerCmd,
885 pullImage,
886 resolveCodeUriToMount, generateFunctionEnvs, run, generateRamdomContainerName,
887 generateDockerEnvs, pullImageIfNeed,
888 showDebugIdeTipsForVscode, resolveNasConfigToMounts,
889 startInstallationContainer, startContainer, isDockerToolBoxAndEnsureDockerVersion,
890 conventInstallTargetsToMounts, startSboxContainer, buildImage, copyFromImage,
891 resolveTmpDirToMount, showDebugIdeTipsForPycharm, resolveDebuggerPathToMount,
892 listContainers, getContainer, createAndRunContainer, execContainer,
893 renameContainer, detectDockerVersion, resolveNasYmlToMount, resolvePasswdMount
894};