1 | 'use strict';
|
2 |
|
3 | const getProfile = require('./profile').getProfile;
|
4 | const fs = require('fs-extra');
|
5 | const path = require('path');
|
6 | const { blue, red, yellow } = require('colors');
|
7 | const debug = require('debug')('fun:local');
|
8 | const Docker = require('dockerode');
|
9 | const docker = new Docker();
|
10 | const dockerOpts = require('./docker-opts');
|
11 | const getVisitor = require('./visitor').getVisitor;
|
12 | const definition = require('./definition');
|
13 | const nas = require('./nas');
|
14 | const { parseArgsStringToArgv } = require('string-argv');
|
15 | const { addEnv, addInstallTargetEnv, resolveLibPathsFromLdConf } = require('./install/env');
|
16 | const { findPathsOutofSharedPaths } = require('./docker-support');
|
17 | const ip = require('ip');
|
18 | const tar = require('tar-fs');
|
19 |
|
20 | const _ = require('lodash');
|
21 |
|
22 | require('draftlog').into(console);
|
23 |
|
24 | var containers = new Set();
|
25 |
|
26 | const devnull = require('dev-null');
|
27 |
|
28 |
|
29 | function waitingForContainerStopped() {
|
30 |
|
31 | const isRaw = process.isRaw;
|
32 | const kpCallBack = (_char, key) => {
|
33 | if (key & key.ctrl && key.name === 'c') {
|
34 | process.emit('SIGINT');
|
35 | }
|
36 | };
|
37 | if (process.platform === 'win32') {
|
38 | if (process.stdin.isTTY) {
|
39 | process.stdin.setRawMode(isRaw);
|
40 | }
|
41 | process.stdin.on('keypress', kpCallBack);
|
42 | }
|
43 |
|
44 | let stopping = false;
|
45 |
|
46 | process.on('SIGINT', async () => {
|
47 |
|
48 | debug('containers length: ', containers.length);
|
49 |
|
50 | if (stopping) {
|
51 | return;
|
52 | }
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | process.stdin.destroy();
|
58 |
|
59 | if (!containers.size) {
|
60 | return;
|
61 | }
|
62 |
|
63 | stopping = true;
|
64 |
|
65 | console.log(`\nreceived canncel request, stopping running containers.....`);
|
66 |
|
67 | const jobs = [];
|
68 |
|
69 | for (let container of containers) {
|
70 | try {
|
71 | if (container.destroy) {
|
72 | container.destroy();
|
73 | } else {
|
74 | const c = docker.getContainer(container);
|
75 |
|
76 | await c.inspect();
|
77 |
|
78 | console.log(`stopping container ${container}`);
|
79 |
|
80 | jobs.push(c.stop());
|
81 | }
|
82 | } catch (error) {
|
83 | debug('get container instance error, ignore container to stop, error is', error);
|
84 | }
|
85 | }
|
86 |
|
87 | try {
|
88 | await Promise.all(jobs);
|
89 | console.log('all containers stopped');
|
90 | } catch (error) {
|
91 | console.error(error);
|
92 | process.exit(-1);
|
93 | }
|
94 | });
|
95 |
|
96 | return () => {
|
97 | process.stdin.removeListener('keypress', kpCallBack);
|
98 | if (process.stdin.isTTY) {
|
99 | process.stdin.setRawMode(isRaw);
|
100 | }
|
101 | };
|
102 | }
|
103 |
|
104 | const goThrough = waitingForContainerStopped();
|
105 |
|
106 | const {
|
107 | generateVscodeDebugConfig, generateDebugEnv
|
108 | } = require('./debug');
|
109 |
|
110 |
|
111 | const skipPullImage = true;
|
112 |
|
113 | async function resolveNasConfigToMounts(serviceName, nasConfig, nasBaseDir) {
|
114 |
|
115 | const nasMappings = await nas.convertNasConfigToNasMappings(nasBaseDir, nasConfig, serviceName);
|
116 |
|
117 | return nasMappings.map(nasMapping => {
|
118 | debug('mounting local nas mock dir %s into container %s\n', nasMapping.localNasDir, nasMapping.remoteNasDir);
|
119 | return {
|
120 | Type: 'bind',
|
121 | Source: nasMapping.localNasDir,
|
122 | Target: nasMapping.remoteNasDir,
|
123 | ReadOnly: false
|
124 | };
|
125 | });
|
126 |
|
127 | }
|
128 |
|
129 | async function resolveTmpDirToMount(absTmpDir) {
|
130 | if (!absTmpDir) { return {}; }
|
131 | return {
|
132 | Type: 'bind',
|
133 | Source: absTmpDir,
|
134 | Target: '/tmp',
|
135 | ReadOnly: false
|
136 | };
|
137 | }
|
138 |
|
139 | async function resolveDebuggerPathToMount(debuggerPath) {
|
140 | if (!debuggerPath) { return {}; }
|
141 | const absDebuggerPath = path.resolve(debuggerPath);
|
142 | return {
|
143 | Type: 'bind',
|
144 | Source: absDebuggerPath,
|
145 | Target: '/tmp/debugger_files',
|
146 | ReadOnly: false
|
147 | };
|
148 | }
|
149 |
|
150 |
|
151 | async function resolveCodeUriToMount(absCodeUri, readOnly = true) {
|
152 | let target = null;
|
153 |
|
154 | const stats = await fs.lstat(absCodeUri);
|
155 |
|
156 | if (stats.isDirectory()) {
|
157 | target = '/code';
|
158 | } else {
|
159 |
|
160 |
|
161 | target = path.posix.join('/code', path.basename(absCodeUri));
|
162 | }
|
163 |
|
164 |
|
165 | return {
|
166 | Type: 'bind',
|
167 | Source: absCodeUri,
|
168 | Target: target,
|
169 | ReadOnly: readOnly
|
170 | };
|
171 | }
|
172 |
|
173 | function conventInstallTargetsToMounts(installTargets) {
|
174 |
|
175 | if (!installTargets) { return []; }
|
176 |
|
177 | const mounts = [];
|
178 |
|
179 | _.forEach(installTargets, (target) => {
|
180 | const { hostPath, containerPath } = target;
|
181 |
|
182 | if (!(fs.pathExistsSync(hostPath))) {
|
183 | fs.ensureDirSync(hostPath);
|
184 | }
|
185 |
|
186 | mounts.push({
|
187 | Type: 'bind',
|
188 | Source: hostPath,
|
189 | Target: containerPath,
|
190 | ReadOnly: false
|
191 | });
|
192 | });
|
193 |
|
194 | return mounts;
|
195 | }
|
196 |
|
197 | async function imageExist(imageName) {
|
198 |
|
199 | const images = await docker.listImages({
|
200 | filters: {
|
201 | reference: [imageName]
|
202 | }
|
203 | });
|
204 |
|
205 | return images.length > 0;
|
206 | }
|
207 |
|
208 | async function listContainers(options) {
|
209 | return await docker.listContainers(options);
|
210 | }
|
211 |
|
212 | async function getContainer(containerId) {
|
213 | return await docker.getContainer(containerId);
|
214 | }
|
215 |
|
216 | async function renameContainer(container, name) {
|
217 | return await container.rename({
|
218 | name
|
219 | });
|
220 | }
|
221 |
|
222 |
|
223 | function generateDockerCmd(functionProps, httpMode, invokeInitializer = true, event = null) {
|
224 | const cmd = ['-h', functionProps.Handler];
|
225 |
|
226 |
|
227 | if (event !== null) {
|
228 | cmd.push('--event', Buffer.from(event).toString('base64'));
|
229 | cmd.push('--event-decode');
|
230 | } else {
|
231 |
|
232 | cmd.push('--stdin');
|
233 | }
|
234 |
|
235 | if (httpMode) {
|
236 | cmd.push('--http');
|
237 | }
|
238 |
|
239 | const initializer = functionProps.Initializer;
|
240 |
|
241 | if (initializer && invokeInitializer) {
|
242 | cmd.push('-i', initializer);
|
243 | }
|
244 |
|
245 | const initializationTimeout = functionProps.InitializationTimeout;
|
246 |
|
247 |
|
248 | if (initializationTimeout) {
|
249 | cmd.push('--initializationTimeout', initializationTimeout.toString());
|
250 | }
|
251 |
|
252 | debug(`docker cmd: ${cmd}`);
|
253 |
|
254 | return cmd;
|
255 | }
|
256 |
|
257 |
|
258 | function followProgress(stream, onFinished) {
|
259 |
|
260 | const barLines = {};
|
261 |
|
262 | const onProgress = (event) => {
|
263 | let status = event.status;
|
264 |
|
265 | if (event.progress) {
|
266 | status = `${event.status} ${event.progress}`;
|
267 | }
|
268 |
|
269 | if (event.id) {
|
270 | const id = event.id;
|
271 |
|
272 | if (!barLines[id]) {
|
273 | barLines[id] = console.draft();
|
274 | }
|
275 | barLines[id](id + ': ' + status);
|
276 | } else {
|
277 | if (_.has(event, 'aux.ID')) {
|
278 | event.stream = event.aux.ID + '\n';
|
279 | }
|
280 |
|
281 | const out = event.status ? event.status + '\n' : event.stream;
|
282 | process.stdout.write(out);
|
283 | }
|
284 | };
|
285 |
|
286 | docker.modem.followProgress(stream, onFinished, onProgress);
|
287 | }
|
288 |
|
289 | async function pullImage(imageName) {
|
290 |
|
291 | const resolveImageName = await dockerOpts.resolveImageNameForPull(imageName);
|
292 |
|
293 |
|
294 | const startTime = new Date();
|
295 |
|
296 | const stream = await docker.pull(resolveImageName);
|
297 |
|
298 | const visitor = await getVisitor();
|
299 |
|
300 | visitor.event({
|
301 | ec: 'image',
|
302 | ea: 'pull',
|
303 | el: 'start'
|
304 | }).send();
|
305 |
|
306 | const registry = await dockerOpts.resolveDockerRegistry();
|
307 |
|
308 | return new Promise((resolve, reject) => {
|
309 |
|
310 | console.log(`begin pulling image ${resolveImageName}, you can also use ` + yellow(`'docker pull ${resolveImageName}'`) + ' to pull image by yourself.');
|
311 |
|
312 | const onFinished = async (err) => {
|
313 | const pullDuration = parseInt((new Date() - startTime) / 1000);
|
314 | if (err) {
|
315 | visitor.event({
|
316 | ec: 'image',
|
317 | ea: 'pull',
|
318 | el: 'error'
|
319 | }).send();
|
320 |
|
321 | visitor.event({
|
322 | ec: 'image',
|
323 | ea: `pull from ${registry}`,
|
324 | el: 'error'
|
325 | }).send();
|
326 |
|
327 | visitor.event({
|
328 | ec: `image pull from ${registry}`,
|
329 | ea: `used ${pullDuration}`,
|
330 | el: 'error'
|
331 | }).send();
|
332 |
|
333 | reject(err);
|
334 | return;
|
335 | }
|
336 |
|
337 | visitor.event({
|
338 | ec: 'image',
|
339 | ea: `pull from ${registry}`,
|
340 | el: 'success'
|
341 | }).send();
|
342 |
|
343 | visitor.event({
|
344 | ec: 'image',
|
345 | ea: 'pull',
|
346 | el: 'success'
|
347 | }).send();
|
348 |
|
349 | visitor.event({
|
350 | ec: `image pull from ${registry}`,
|
351 | ea: `used ${pullDuration}`,
|
352 | el: 'success'
|
353 | }).send();
|
354 |
|
355 | dockerOpts.DOCKER_REGISTRIES;
|
356 | for (const r of dockerOpts.DOCKER_REGISTRIES) {
|
357 | if (resolveImageName.indexOf(r) === 0) {
|
358 | const image = await docker.getImage(resolveImageName);
|
359 |
|
360 | await image.tag({
|
361 | name: resolveImageName,
|
362 | repo: resolveImageName.slice(r.length + 1)
|
363 | });
|
364 | break;
|
365 | }
|
366 | }
|
367 | resolve(resolveImageName);
|
368 | };
|
369 |
|
370 | followProgress(stream, onFinished);
|
371 | });
|
372 | }
|
373 |
|
374 | function generateFunctionEnvs(functionProps) {
|
375 | const environmentVariables = functionProps.EnvironmentVariables;
|
376 |
|
377 | if (!environmentVariables) { return {}; }
|
378 |
|
379 | return Object.assign({}, environmentVariables);
|
380 | }
|
381 |
|
382 | function generateRamdomContainerName() {
|
383 | return `fun_local_${new Date().getTime()}_${Math.random().toString(36).substr(2, 7)}`;
|
384 | }
|
385 |
|
386 | async function generateDockerEnvs(baseDir, functionProps, debugPort, httpParams, nasConfig, ishttpTrigger, debugIde, debugArgs) {
|
387 |
|
388 | const envs = {};
|
389 |
|
390 | if (httpParams) {
|
391 | Object.assign(envs, {
|
392 | 'FC_HTTP_PARAMS': httpParams
|
393 | });
|
394 | }
|
395 |
|
396 | const confEnv = await resolveLibPathsFromLdConf(baseDir, functionProps.CodeUri);
|
397 |
|
398 | Object.assign(envs, confEnv);
|
399 |
|
400 | const runtime = functionProps.Runtime;
|
401 |
|
402 | if (debugPort && !debugArgs) {
|
403 | const debugEnv = generateDebugEnv(runtime, debugPort, debugIde);
|
404 |
|
405 | Object.assign(envs, debugEnv);
|
406 | } else if (debugArgs) {
|
407 | Object.assign(envs, {
|
408 | DEBUG_OPTIONS: debugArgs
|
409 | });
|
410 | }
|
411 |
|
412 | if (ishttpTrigger && runtime === 'java8') {
|
413 | envs['fc_enable_new_java_ca'] = 'true';
|
414 | }
|
415 |
|
416 | Object.assign(envs, generateFunctionEnvs(functionProps));
|
417 |
|
418 | const profile = await getProfile();
|
419 |
|
420 | Object.assign(envs, {
|
421 | 'local': true,
|
422 | 'FC_ACCESS_KEY_ID': profile.accessKeyId,
|
423 | 'FC_ACCESS_KEY_SECRET': profile.accessKeySecret
|
424 | });
|
425 |
|
426 | return addEnv(envs, nasConfig);
|
427 | }
|
428 |
|
429 | async function pullImageIfNeed(imageName) {
|
430 | const exist = await imageExist(imageName);
|
431 |
|
432 | if (!exist || !skipPullImage) {
|
433 |
|
434 | await pullImage(imageName);
|
435 | } else {
|
436 | debug(`skip pulling image ${imageName}...`);
|
437 | console.log(`skip pulling image ${imageName}...`);
|
438 | }
|
439 | }
|
440 |
|
441 | async function showDebugIdeTipsForVscode(serviceName, functionName, runtime, codeSource, debugPort) {
|
442 | const vscodeDebugConfig = await generateVscodeDebugConfig(serviceName, functionName, runtime, codeSource, debugPort);
|
443 |
|
444 |
|
445 | console.log(blue('you can paste these config to .vscode/launch.json, and then attach to your running function'));
|
446 | console.log('///////////////// config begin /////////////////');
|
447 | console.log(JSON.stringify(vscodeDebugConfig, null, 4));
|
448 | console.log('///////////////// config end /////////////////');
|
449 | }
|
450 |
|
451 | async function showDebugIdeTipsForPycharm(codeSource, debugPort) {
|
452 |
|
453 | const stats = await fs.lstat(codeSource);
|
454 |
|
455 | if (!stats.isDirectory()) {
|
456 | codeSource = path.dirname(codeSource);
|
457 | }
|
458 |
|
459 | console.log(yellow(`\n========= Tips for PyCharm remote debug =========
|
460 | Local host name: ${ip.address()}
|
461 | Port : ${yellow(debugPort)}
|
462 | Path mappings : ${yellow(codeSource)}=/code
|
463 |
|
464 | Debug Code needed to copy to your function code:
|
465 |
|
466 | import pydevd
|
467 | pydevd.settrace('${ip.address()}', port=${debugPort}, stdoutToServer=True, stderrToServer=True)
|
468 |
|
469 | =========================================================================\n`));
|
470 | }
|
471 |
|
472 | function writeEventToStreamAndClose(stream, event) {
|
473 |
|
474 | if (event) {
|
475 | stream.write(event);
|
476 | }
|
477 |
|
478 | stream.end();
|
479 | }
|
480 |
|
481 | async function isDockerToolBox() {
|
482 |
|
483 | const dockerInfo = await docker.info();
|
484 |
|
485 | const obj = (dockerInfo.Labels || []).map(e => _.split(e, '=', 2))
|
486 | .filter(e => e.length === 2)
|
487 | .reduce((acc, cur) => (acc[cur[0]] = cur[1], acc), {});
|
488 |
|
489 | return process.platform === 'win32' && obj.provider === 'virtualbox';
|
490 | }
|
491 |
|
492 | async function run(opts, event, outputStream, errorStream) {
|
493 |
|
494 | const container = await createContainer(opts);
|
495 |
|
496 | const attachOpts = {
|
497 | hijack: true,
|
498 | stream: true,
|
499 | stdin: true,
|
500 | stdout: true,
|
501 | stderr: true
|
502 | };
|
503 |
|
504 | const stream = await container.attach(attachOpts);
|
505 |
|
506 | if (!outputStream) {
|
507 | outputStream = process.stdout;
|
508 | }
|
509 |
|
510 | if (!errorStream) {
|
511 | errorStream = process.stderr;
|
512 | }
|
513 |
|
514 | var isWin = process.platform === 'win32';
|
515 | if (!isWin) {
|
516 | container.modem.demuxStream(stream, outputStream, errorStream);
|
517 | }
|
518 |
|
519 | await container.start();
|
520 |
|
521 |
|
522 | if (isWin) {
|
523 | const logStream = await container.logs({
|
524 | stdout: true,
|
525 | stderr: true,
|
526 | follow: true
|
527 | });
|
528 |
|
529 | container.modem.demuxStream(logStream, outputStream, errorStream);
|
530 | }
|
531 |
|
532 | containers.add(container.id);
|
533 |
|
534 | writeEventToStreamAndClose(stream, event);
|
535 |
|
536 |
|
537 |
|
538 | const exitRs = await container.wait();
|
539 |
|
540 | containers.delete(container.id);
|
541 |
|
542 | return exitRs;
|
543 | }
|
544 |
|
545 | function resolveDockerUser(nasConfig) {
|
546 | let uid = 10003;
|
547 | let gid = 10003;
|
548 |
|
549 | const isNasAuto = definition.isNasAutoConfig(nasConfig);
|
550 |
|
551 | if (nasConfig && !isNasAuto) {
|
552 | const userId = nasConfig.UserId;
|
553 | const groupId = nasConfig.GroupId;
|
554 |
|
555 | if (userId !== -1) {
|
556 | uid = userId;
|
557 | }
|
558 |
|
559 | if (groupId !== -1) {
|
560 | gid = groupId;
|
561 | }
|
562 | }
|
563 |
|
564 | return `${uid}:${gid}`;
|
565 | }
|
566 |
|
567 | async function createContainer(opts) {
|
568 | const isWin = process.platform === 'win32';
|
569 | const isMac = process.platform === 'darwin';
|
570 |
|
571 | if (opts && isMac) {
|
572 | if (opts.HostConfig) {
|
573 | const pathsOutofSharedPaths = await findPathsOutofSharedPaths(opts.HostConfig.Mounts);
|
574 | if (isMac && pathsOutofSharedPaths.length > 0) {
|
575 | 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`));
|
576 | }
|
577 | }
|
578 | }
|
579 |
|
580 | let container;
|
581 | try {
|
582 |
|
583 | container = await docker.createContainer(opts);
|
584 | } catch (ex) {
|
585 |
|
586 | if (ex.message.indexOf('invalid mount config for type') !== -1 && await isDockerToolBox()) {
|
587 | 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 .`));
|
588 | }
|
589 | if (ex.message.indexOf('drive is not shared') !== -1 && isWin) {
|
590 | throw new Error(red(`${ex.message}More information please refer to https://docs.docker.com/docker-for-windows/#shared-drives`));
|
591 | }
|
592 | throw ex;
|
593 | }
|
594 | return container;
|
595 | }
|
596 |
|
597 | async function createAndRunContainer(opts) {
|
598 | const container = await createContainer(opts);
|
599 | containers.add(container.id);
|
600 | await container.start({});
|
601 | return container;
|
602 | }
|
603 |
|
604 | async function execContainer(container, opts, outputStream, errorStream) {
|
605 | outputStream = process.stdout;
|
606 | errorStream = process.stderr;
|
607 | const logStream = await container.logs({
|
608 | stdout: true,
|
609 | stderr: true,
|
610 | follow: true,
|
611 | since: (new Date().getTime() / 1000)
|
612 | });
|
613 | container.modem.demuxStream(logStream, outputStream, errorStream);
|
614 | const exec = await container.exec(opts);
|
615 | const stream = (await exec.start()).output;
|
616 |
|
617 | await new Promise(resolve => setTimeout(resolve, 30));
|
618 | container.modem.demuxStream(stream, outputStream, errorStream);
|
619 |
|
620 | await waitForExec(exec);
|
621 | logStream.destroy();
|
622 | }
|
623 |
|
624 | async function waitForExec(exec) {
|
625 | return await new Promise((resolve, reject) => {
|
626 |
|
627 |
|
628 | function waitContainerExec() {
|
629 | exec.inspect((err, data) => {
|
630 | if (data.Running) {
|
631 | setTimeout(waitContainerExec, 100);
|
632 | return;
|
633 | }
|
634 | if (err) {
|
635 | reject(err);
|
636 | } else if (data.ExitCode !== 0) {
|
637 | reject(`${data.ProcessConfig.entrypoint} exited with code ${data.ExitCode}`);
|
638 | } else {
|
639 | resolve(data.ExitCode);
|
640 | }
|
641 | });
|
642 | }
|
643 | waitContainerExec();
|
644 | });
|
645 | }
|
646 |
|
647 |
|
648 |
|
649 | async function startContainer(opts, outputStream, errorStream) {
|
650 |
|
651 | const container = await createContainer(opts);
|
652 |
|
653 | containers.add(container.id);
|
654 |
|
655 | try {
|
656 | await container.start({});
|
657 | } catch (err) {
|
658 | console.error(err);
|
659 | }
|
660 |
|
661 | const logs = outputStream || errorStream;
|
662 |
|
663 | if (logs) {
|
664 | if (!outputStream) {
|
665 | outputStream = devnull();
|
666 | }
|
667 |
|
668 | if (!errorStream) {
|
669 | errorStream = devnull();
|
670 | }
|
671 |
|
672 |
|
673 | const logStream = await container.logs({
|
674 | stdout: true,
|
675 | stderr: true,
|
676 | follow: true
|
677 | });
|
678 |
|
679 | container.modem.demuxStream(logStream, outputStream, errorStream);
|
680 | }
|
681 |
|
682 | return {
|
683 | stop: async () => {
|
684 | await container.stop();
|
685 | containers.delete(container.id);
|
686 | },
|
687 |
|
688 | exec: async (cmd, { cwd = '', env = {}, outputStream, errorStream, verbose = false } = {}) => {
|
689 | const options = {
|
690 | Cmd: cmd,
|
691 | Env: dockerOpts.resolveDockerEnv(env),
|
692 | Tty: false,
|
693 | AttachStdin: false,
|
694 | AttachStdout: true,
|
695 | AttachStderr: true,
|
696 | WorkingDir: cwd
|
697 | };
|
698 |
|
699 |
|
700 | debug('docker exec opts: ' + JSON.stringify(options, null, 4));
|
701 |
|
702 | const exec = await container.exec(options);
|
703 |
|
704 | const stream = (await exec.start({ hijack: true, stdin: false })).output;
|
705 |
|
706 |
|
707 | await new Promise(resolve => setTimeout(resolve, 30));
|
708 |
|
709 | if (!outputStream) {
|
710 | outputStream = process.stdout;
|
711 | }
|
712 |
|
713 | if (!errorStream) {
|
714 | errorStream = process.stderr;
|
715 | }
|
716 |
|
717 | if (verbose) {
|
718 | container.modem.demuxStream(stream, outputStream, errorStream);
|
719 | } else {
|
720 | container.modem.demuxStream(stream, devnull(), errorStream);
|
721 | }
|
722 |
|
723 | return await waitForExec(exec);
|
724 | }
|
725 | };
|
726 | }
|
727 |
|
728 | async function startInstallationContainer({ runtime, imageName, codeUri, targets }) {
|
729 | debug(`runtime: ${runtime}`);
|
730 | debug(`codeUri: ${codeUri}`);
|
731 |
|
732 | if (await isDockerToolBox()) {
|
733 |
|
734 | 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.`));
|
735 | }
|
736 |
|
737 | if (!imageName) {
|
738 | imageName = await dockerOpts.resolveRuntimeToDockerImage(runtime, true);
|
739 | if (!imageName) {
|
740 | throw new Error(`invalid runtime name ${runtime}`);
|
741 | }
|
742 | }
|
743 |
|
744 | const codeMount = await resolveCodeUriToMount(codeUri, false);
|
745 |
|
746 | const installMounts = conventInstallTargetsToMounts(targets);
|
747 |
|
748 | const mounts = [codeMount, ...installMounts];
|
749 |
|
750 | await pullImageIfNeed(imageName);
|
751 |
|
752 | const envs = addInstallTargetEnv({}, targets);
|
753 |
|
754 | const opts = dockerOpts.generateInstallOpts(imageName, mounts, envs);
|
755 |
|
756 | return await startContainer(opts);
|
757 | }
|
758 |
|
759 | async function startSboxContainer({
|
760 | runtime, imageName,
|
761 | mounts, cmd, envs,
|
762 | isTty, isInteractive
|
763 | }) {
|
764 | debug(`runtime: ${runtime}`);
|
765 | debug(`mounts: ${mounts}`);
|
766 | debug(`isTty: ${isTty}`);
|
767 | debug(`isInteractive: ${isInteractive}`);
|
768 |
|
769 | if (!imageName) {
|
770 | imageName = await dockerOpts.resolveRuntimeToDockerImage(runtime, true);
|
771 | if (!imageName) {
|
772 | throw new Error(`invalid runtime name ${runtime}`);
|
773 | }
|
774 | }
|
775 |
|
776 | debug(`cmd: ${parseArgsStringToArgv(cmd || '')}`);
|
777 |
|
778 | const container = await createContainer(dockerOpts.generateSboxOpts({
|
779 | imageName,
|
780 | hostname: `fc-${runtime}`,
|
781 | mounts,
|
782 | envs,
|
783 | cmd: parseArgsStringToArgv(cmd || ''),
|
784 | isTty,
|
785 | isInteractive
|
786 | }));
|
787 |
|
788 | containers.add(container.id);
|
789 |
|
790 | await container.start();
|
791 |
|
792 | const stream = await container.attach({
|
793 | logs: true,
|
794 | stream: true,
|
795 | stdin: isInteractive,
|
796 | stdout: true,
|
797 | stderr: true
|
798 | });
|
799 |
|
800 |
|
801 | let logStream;
|
802 | if (isTty) {
|
803 | stream.pipe(process.stdout);
|
804 | } else {
|
805 | if (isInteractive || process.platform === 'win32') {
|
806 |
|
807 | logStream = await container.logs({
|
808 | stdout: true,
|
809 | stderr: true,
|
810 | follow: true
|
811 | });
|
812 | container.modem.demuxStream(logStream, process.stdout, process.stderr);
|
813 | } else {
|
814 | container.modem.demuxStream(stream, process.stdout, process.stderr);
|
815 | }
|
816 |
|
817 | }
|
818 |
|
819 | if (isInteractive) {
|
820 |
|
821 | process.stdin.pipe(stream);
|
822 |
|
823 | let previousKey;
|
824 | const CTRL_P = '\u0010', CTRL_Q = '\u0011';
|
825 |
|
826 | process.stdin.on('data', (key) => {
|
827 |
|
828 | const keyStr = key.toString('ascii');
|
829 | if (previousKey === CTRL_P && keyStr === CTRL_Q) {
|
830 | container.stop(() => { });
|
831 | }
|
832 | previousKey = keyStr;
|
833 | });
|
834 |
|
835 | }
|
836 |
|
837 | let resize;
|
838 |
|
839 | const isRaw = process.isRaw;
|
840 | if (isTty) {
|
841 |
|
842 | goThrough();
|
843 |
|
844 | process.stdin.setRawMode(true);
|
845 |
|
846 | resize = async () => {
|
847 | const dimensions = {
|
848 | h: process.stdout.rows,
|
849 | w: process.stdout.columns
|
850 | };
|
851 |
|
852 | if (dimensions.h !== 0 && dimensions.w !== 0) {
|
853 | await container.resize(dimensions);
|
854 | }
|
855 | };
|
856 |
|
857 | await resize();
|
858 | process.stdout.on('resize', resize);
|
859 |
|
860 |
|
861 |
|
862 | stream.write(' \b');
|
863 | }
|
864 |
|
865 |
|
866 | await container.wait();
|
867 |
|
868 |
|
869 | if (isTty) {
|
870 | process.stdout.removeListener('resize', resize);
|
871 | process.stdin.setRawMode(isRaw);
|
872 | }
|
873 |
|
874 | if (isInteractive) {
|
875 | process.stdin.removeAllListeners();
|
876 | process.stdin.unpipe(stream);
|
877 |
|
878 | |
879 |
|
880 |
|
881 |
|
882 | process.stdin.destroy();
|
883 | }
|
884 |
|
885 | if (logStream) {
|
886 | logStream.removeAllListeners();
|
887 | }
|
888 |
|
889 | stream.unpipe(process.stdout);
|
890 |
|
891 |
|
892 |
|
893 | stream.destroy();
|
894 |
|
895 | containers.delete(container.id);
|
896 |
|
897 | if (!isTty) {
|
898 | goThrough();
|
899 | process.stdin.destroy();
|
900 | }
|
901 | }
|
902 |
|
903 | async function zipTo(archive, to) {
|
904 |
|
905 | await fs.ensureDir(to);
|
906 |
|
907 | await new Promise((resolve, reject) => {
|
908 | archive.pipe(tar.extract(to)).on('error', reject).on('finish', resolve);
|
909 | });
|
910 | }
|
911 |
|
912 | async function copyFromImage(imageName, from, to) {
|
913 | const container = await docker.createContainer({
|
914 | Image: imageName
|
915 | });
|
916 |
|
917 | const archive = await container.getArchive({
|
918 | path: from
|
919 | });
|
920 |
|
921 | await zipTo(archive, to);
|
922 |
|
923 | await container.remove();
|
924 | }
|
925 |
|
926 | function buildImage(dockerBuildDir, dockerfilePath, imageTag) {
|
927 |
|
928 | return new Promise((resolve, reject) => {
|
929 | var tarStream = tar.pack(dockerBuildDir);
|
930 |
|
931 | docker.buildImage(tarStream, {
|
932 | dockerfile: path.relative(dockerBuildDir, dockerfilePath),
|
933 | t: imageTag
|
934 | }, (error, stream) => {
|
935 | containers.add(stream);
|
936 |
|
937 | if (error) { reject(error); }
|
938 | else {
|
939 | stream.on('error', (e) => {
|
940 | containers.delete(stream);
|
941 | reject(e);
|
942 | });
|
943 | stream.on('end', function () {
|
944 | containers.delete(stream);
|
945 | resolve(imageTag);
|
946 | });
|
947 | }
|
948 | followProgress(stream, (err, res) => err ? reject(err) : resolve(res));
|
949 | });
|
950 | });
|
951 | }
|
952 |
|
953 | module.exports = {
|
954 | imageExist, generateDockerCmd,
|
955 | pullImage,
|
956 | resolveCodeUriToMount, generateFunctionEnvs, run, generateRamdomContainerName,
|
957 | generateDockerEnvs, pullImageIfNeed,
|
958 | showDebugIdeTipsForVscode, resolveDockerUser, resolveNasConfigToMounts,
|
959 | startInstallationContainer, startContainer, isDockerToolBox,
|
960 | conventInstallTargetsToMounts, startSboxContainer, buildImage, copyFromImage,
|
961 | resolveTmpDirToMount, showDebugIdeTipsForPycharm, resolveDebuggerPathToMount,
|
962 | listContainers, getContainer, createAndRunContainer, execContainer,
|
963 | renameContainer
|
964 | }; |
\ | No newline at end of file |