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