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