UNPKG

35.6 kBJavaScriptView Raw
1'use strict';
2
3const util = require('./import/utils');
4const bytes = require('bytes');
5const funignore = require('./package/ignore');
6const definition = require('./definition');
7const promiseRetry = require('./retry');
8const getProfile = require('./profile').getProfile;
9const securityGroup = require('./security-group');
10
11const fs = require('fs-extra');
12const path = require('path');
13const debug = require('debug')('fun:fc');
14const yaml = require('js-yaml');
15const zip = require('./package/zip');
16const vpc = require('./vpc');
17const nas = require('./nas');
18const nasCp = require('./nas/cp');
19
20const { sleep } = require('./time');
21const { getTpl, getBaseDir } = require('./tpl');
22const { green, red, yellow } = require('colors');
23const { getFcClient, getEcsPopClient, getNasPopClient } = require('./client');
24const { addEnv, resolveLibPathsFromLdConf, generateDefaultLibPath } = require('./install/env');
25const { readFileFromNasYml, mergeNasMappingsInNasYml } = require('./nas/support');
26
27const _ = require('lodash');
28
29const {
30 promptForConfirmContinue,
31 promptForMountTargets,
32 promptForMountPoints,
33 promptForFileSystems,
34 promptForSecurityGroup
35} = require('./init/prompt');
36
37const FUN_GENERATED_SERVICE = 'fun-generated-default-service';
38
39const SYSTEM_DEPENDENCY_PATH = path.join('.fun', 'root');
40
41const SUPPORT_RUNTIMES = ['nodejs6', 'nodejs8', 'nodejs10', 'python2.7', 'python3', 'custom'];
42
43const defaultVpcConfig = {
44 securityGroupId: '',
45 vSwitchIds: [],
46 vpcId: ''
47};
48
49const defaultNasConfig = {
50 UserId: -1,
51 GroupId: -1,
52 MountPoints: []
53};
54
55async function generateFunIngore(baseDir, codeUri) {
56 const absCodeUri = path.resolve(baseDir, codeUri);
57 const absBaseDir = path.resolve(baseDir);
58
59 const relative = path.relative(absBaseDir, absCodeUri);
60
61 if (codeUri.startsWith('..') || relative.startsWith('..')) {
62 console.warn(red(`\t\twarning: funignore is not supported for your CodeUri: ${codeUri}`));
63 return null;
64 }
65
66 return await funignore(baseDir);
67}
68
69const runtimeTypeMapping = {
70 'nodejs6': 'node_modules',
71 'nodejs8': 'node_modules',
72 'nodejs10': 'node_modules',
73 'python2.7': ['.egg-info', '.dist-info', '.fun'],
74 'python3': ['.egg-info', '.dist-info', '.fun'],
75 'php7.2': ['extension', 'vendor']
76};
77
78async function detectLibraryFolders(dirName, libraryFolders, childDir, wrap, functionName) {
79 if (Array.isArray(libraryFolders)) {
80 for (const iterator of libraryFolders) {
81 for (const name of childDir) {
82 if (_.endsWith(name, iterator)) {
83 console.warn(red(`${wrap}Fun detected that the library directory '${name}' is not included in function '${functionName}' CodeUri.\n\t\tPlease make sure if it is the right configuration. if yes, ignore please.`));
84 return;
85 }
86 }
87 }
88 } else {
89 if (childDir.includes(libraryFolders)) {
90 console.warn(red(`${wrap}Fun detected that the library directory '${libraryFolders}' is not included in function '${functionName}' CodeUri.\n\t\tPlease make sure if it is the right configuration. if yes, ignore please.`));
91 } else {
92
93 const funDir = childDir.filter(p => p === '.fun');
94 if (Array.isArray(funDir) && funDir.length > 0) {
95
96 const childFun = await fs.readdir(path.join(dirName, '.fun'));
97
98 if (childFun.includes('root')) {
99
100 console.warn(red(`${wrap}Fun detected that the library directory '.fun/root' is not included in function '${functionName}' CodeUri.\n\t\tPlease make sure if it is the right configuration. if yes, ignore please.`));
101
102 }
103 }
104 }
105 }
106}
107
108async function detectLibrary(codeUri, runtime, baseDir, functionName, wrap = '') {
109 const absoluteCodePath = path.resolve(baseDir, codeUri);
110
111 const stats = await fs.lstat(absoluteCodePath);
112 if (stats.isFile()) {
113 let libraryFolders = runtimeTypeMapping[runtime];
114
115 const dirName = path.dirname(absoluteCodePath);
116 const childDir = await fs.readdir(dirName);
117
118 await detectLibraryFolders(dirName, libraryFolders, childDir, wrap, functionName);
119 }
120}
121
122function extractOssCodeUri(ossUri) {
123 const prefixLength = 'oss://'.length;
124
125 const index = ossUri.indexOf('/', prefixLength);
126
127 return {
128 ossBucketName: ossUri.substring(prefixLength, index),
129 ossObjectName: ossUri.substring(index + 1)
130 };
131}
132
133async function zipCode(baseDir, codeUri, runtime, functionName) {
134 let codeAbsPath;
135
136 if (codeUri) {
137 codeAbsPath = path.resolve(baseDir, codeUri);
138
139 if (codeUri.endsWith('.zip') || codeUri.endsWith('.jar') || codeUri.endsWith('.war')) {
140 return { base64: Buffer.from(await fs.readFile(codeAbsPath)).toString('base64') };
141 }
142 } else {
143 codeAbsPath = path.resolve(baseDir, './');
144 }
145
146 const ignore = await generateFunIngore(baseDir, codeAbsPath);
147
148 await detectLibrary(codeAbsPath, runtime, baseDir, functionName, '\t\t');
149
150 return await zip.pack(codeAbsPath, ignore);
151}
152
153const NODE_RUNTIME_MAPPING = {
154 'localDir': 'node_modules',
155 'remoteDir': 'node_modules',
156 'env': 'NODE_PATH'
157};
158
159const PYTHON_RUNTIME_MAPPING = {
160 'localDir': '.fun/python',
161 'remoteDir': 'python',
162 'env': 'PYTHONUSERBASE'
163};
164
165const runtimeDependencyMappings = {
166 'nodejs6': [ NODE_RUNTIME_MAPPING ],
167 'nodejs8': [ NODE_RUNTIME_MAPPING ],
168 'nodejs10': [ NODE_RUNTIME_MAPPING ],
169 'python2.7': [ PYTHON_RUNTIME_MAPPING ],
170 'python3': [ PYTHON_RUNTIME_MAPPING ],
171 'custom': [ NODE_RUNTIME_MAPPING, PYTHON_RUNTIME_MAPPING ]
172};
173
174async function saveNasMappings(baseDir, nasMappings) {
175
176 if (_.isEmpty(nasMappings)) { return {}; }
177
178 const nasYmlPath = path.resolve(baseDir, '.nas.yml');
179
180 const contentObj = await readFileFromNasYml(baseDir);
181
182 const mergedNasMappings = await mergeNasMappingsInNasYml(baseDir, nasMappings);
183
184 contentObj.nasMappings = mergedNasMappings;
185
186 await fs.writeFile(nasYmlPath, yaml.dump(contentObj));
187
188 return mergedNasMappings;
189}
190
191async function updateEnvironmentsInTpl({ tplPath, tpl, envs,
192 serviceName,
193 functionName
194}) {
195 const updatedTplContent = _.cloneDeep(tpl);
196
197 const { functionRes } = definition.findFunctionByServiceAndFunctionName(updatedTplContent.Resources, serviceName, functionName);
198
199 const customizer = (objValue, srcValue) => {
200 if (objValue) {
201 const spliceEnvs = objValue + ':' + srcValue;
202 const uniqEnvs = _.uniq(spliceEnvs.split(':'));
203 return _.join(uniqEnvs, ':');
204 }
205 return srcValue;
206 };
207
208 const functionProp = (functionRes.Properties || {});
209 const formerEnvs = (functionProp.EnvironmentVariables) || {};
210 const mergedEnvs = _.mergeWith(formerEnvs, envs, customizer);
211
212 if (_.isEmpty(functionRes['Properties'])) {
213 functionRes.Properties = {
214 'EnvironmentVariables': mergedEnvs
215 };
216 } else {
217 functionRes.Properties.EnvironmentVariables = mergedEnvs;
218 }
219
220 util.outputTemplateFile(tplPath, updatedTplContent);
221
222 console.log(green(`Fun add environment variables to your template.yml`));
223
224 return updatedTplContent;
225}
226
227function generateBackupTplPath(tplPath) {
228 const tplDir = path.dirname(tplPath);
229 const tplName = path.basename(tplPath);
230 const newTplName = `.${tplName}.backup`;
231 return path.join(tplDir, newTplName);
232}
233
234function updateNasAutoConfigureInTpl(tplPath, tpl, nasServiceName) {
235 const updatedTplContent = _.cloneDeep(tpl);
236
237 const { serviceRes } = definition.findServiceByServiceName(updatedTplContent.Resources, nasServiceName);
238
239 if (_.isEmpty(serviceRes['Properties'])) {
240 serviceRes.Properties = {
241 'NasConfig': 'Auto'
242 };
243 } else {
244 serviceRes.Properties.NasConfig = 'Auto';
245 }
246
247 util.outputTemplateFile(tplPath, updatedTplContent);
248
249 console.log(green(`Fun add 'NasConfig: Auto' configuration to your template.yml.`));
250
251 return updatedTplContent;
252}
253
254function updateNasAndVpcInTpl(tplPath, tpl, nasServiceName, nasAndVpcConfig) {
255 const updatedTplContent = _.cloneDeep(tpl);
256
257 const { serviceRes } = definition.findServiceByServiceName(updatedTplContent.Resources, nasServiceName);
258
259 if (_.isEmpty(serviceRes['Properties'])) {
260 serviceRes.Properties = nasAndVpcConfig;
261 } else {
262 serviceRes.Properties.VpcConfig = nasAndVpcConfig.VpcConfig;
263 serviceRes.Properties.NasConfig = nasAndVpcConfig.NasConfig;
264 }
265
266 console.log(green(`Fun add 'NasConfig' and 'VpcConfig' configuration to your template.yml.`));
267
268 util.outputTemplateFile(tplPath, updatedTplContent);
269 return updatedTplContent;
270}
271
272async function generateNasMappingsAndEnvs({
273 baseDir,
274 serviceName,
275 runtime,
276 codeUri,
277 nasConfig
278}) {
279 const envs = {};
280
281 const nasMappings = {};
282 const nasMapping = [];
283
284 const prefix = parseMountDirPrefix(nasConfig);
285 // used for log
286 const nasMappingPath = path.resolve(baseDir, '.nas.yml');
287 const localSystemDependency = path.resolve(codeUri, SYSTEM_DEPENDENCY_PATH);
288
289 if (await fs.pathExists(localSystemDependency)) { // system dependence
290 const remoteNasDir = `${prefix}root`;
291
292 nasMapping.push({
293 localNasDir: localSystemDependency,
294 remoteNasDir
295 });
296
297 nasMappings[serviceName] = nasMapping;
298
299 Object.assign(envs, generateSystemNasEnvs(remoteNasDir));
300
301 outputNasMappingLog(baseDir, nasMappingPath, localSystemDependency);
302 }
303
304 const dependencyMappings = runtimeDependencyMappings[runtime];
305
306 for (const mapping of dependencyMappings) {
307 const localDir = path.join(codeUri, mapping.localDir);
308
309 if (await fs.pathExists(localDir)) { // language local dependencies dir exist
310 const remoteDir = `${prefix}${mapping.remoteDir}`;
311
312 nasMapping.push({
313 localNasDir: localDir,
314 remoteNasDir: remoteDir
315 });
316
317 Object.assign(envs, {
318 [mapping.env]: remoteDir
319 });
320
321 outputNasMappingLog(baseDir, nasMappingPath, localDir);
322 }
323 }
324
325 nasMappings[serviceName] = nasMapping;
326
327 return {
328 envs,
329 nasMappings
330 };
331}
332
333function parseMountDirPrefix(nasConfig) {
334 if (definition.isNasAutoConfig(nasConfig)) {
335 return '/mnt/auto/';
336 }
337 const mountPoints = nasConfig.MountPoints;
338 ensureOnlyOneMountPoinExists(mountPoints);
339
340 const mountPoint = _.head(mountPoints).MountDir;
341 if (_.endsWith(mountPoint, '/')) {
342 return mountPoint;
343 }
344 return mountPoint + '/';
345}
346
347// node_modules has been correctly added to /Users/ellison/fun/examples/datahub/.fun/nasMappings.json.
348function outputNasMappingLog(baseDir, nasMappingPath, localNasDir) {
349 console.log(green(`${path.relative(baseDir, localNasDir)} has been correctly added to ${nasMappingPath}`));
350}
351
352function generateSystemNasEnvs(rootEnvPrefix) {
353 return {
354 'LD_LIBRARY_PATH': `${generateDefaultLibPath(rootEnvPrefix)}`
355 };
356}
357
358async function nasCpFromlocalNasDirToRemoteNasDir(tpl, tplPath, baseDir, nasServiceName, nasMappings) {
359 const localNasTmpDir = path.join(baseDir, '.fun', 'tmp', 'nas', 'cp');
360
361 const errors = [];
362
363 for (const { localNasDir, remoteNasDir } of nasMappings) {
364 const srcPath = localNasDir;
365 const dstPath = `nas://${nasServiceName}${remoteNasDir}/`;
366
367 console.log(yellow(`\nstarting upload ${srcPath} to ${dstPath}`));
368 try {
369 await nasCp(srcPath, dstPath, true, false, localNasTmpDir, tpl, tplPath, baseDir, false, true);
370 } catch (error) {
371 errors.push(`Upload ${srcPath} To ${dstPath} ${error}`);
372 }
373 }
374
375 if (errors.length) {
376 console.log();
377 _.forEach(errors, (error) => {
378 console.log(red(`${error}\n`));
379 });
380 }
381}
382
383async function processNasAutomationConfiguration({ tpl, tplPath, baseDir, runtime, codeUri, convertedNasConfig,
384 nasServiceName,
385 nasFunctionName
386}) {
387
388 const { serviceRes } = definition.findFunctionByServiceAndFunctionName(tpl.Resources, nasServiceName, nasFunctionName);
389
390 const nasConfig = (serviceRes.Properties || {}).NasConfig;
391
392 const { envs, nasMappings } = await generateNasMappingsAndEnvs({
393 baseDir,
394 serviceName: nasServiceName,
395 runtime,
396 codeUri,
397 nasConfig: convertedNasConfig || nasConfig
398 });
399
400 const localDirs = _.map(runtimeDependencyMappings[runtime], mapping => path.join(codeUri, mapping.localDir));
401
402 if (_.isEmpty(nasMappings)) {
403 throw new Error(red(`\nFun detects that your dependencies are not included in path ${localDirs} or ${path.resolve(codeUri, SYSTEM_DEPENDENCY_PATH)}`));
404 }
405
406 const nasMappingsObj = await saveNasMappings(baseDir, nasMappings);
407
408 const updatedTplContent = await updateEnvironmentsInTpl({
409 tplPath, tpl, envs,
410 serviceName: nasServiceName,
411 functionName: nasFunctionName
412 });
413
414 // fun nas cp
415 await nasCpFromlocalNasDirToRemoteNasDir(tpl, tplPath, baseDir, nasServiceName, nasMappingsObj[nasServiceName]);
416
417 console.log(yellow(`\nFun has automatically uploaded your code dependency to NAS, then fun will use 'fun deploy ${nasServiceName}/${nasFunctionName}' to redeploy.`));
418
419 console.log(`Waiting for service ${nasServiceName} to be deployed...`);
420
421 const partialDeploy = await require('./deploy/deploy-by-tpl').partialDeployment(`${nasServiceName}/${nasFunctionName}`, updatedTplContent);
422
423 if (partialDeploy.serviceName) {
424 await require('./deploy/deploy-by-tpl').deployService(baseDir, partialDeploy.serviceName, partialDeploy.serviceRes, false, tplPath, true);
425 }
426}
427
428async function backupTemplateFile(tplPath) {
429 const newPath = generateBackupTplPath(tplPath);
430 await fs.copy(tplPath, newPath);
431 console.log(green(`\nFun automatically backups the original ${path.basename(tplPath)} file to ${newPath}`));
432}
433
434function ensureMountTargetsExist(mountTargets) {
435 if (_.isEmpty(mountTargets)) {
436 throw new Error(red('Nas has not configured the mountTarget yet, please go to the console https://nas.console.aliyun.com/ to manually create the mountTarget.'));
437 }
438}
439
440function ensureSecurityGroupsExist(securityGroups) {
441 if (_.isEmpty(securityGroups)) {
442 throw new Error(red(`\nThere is no SecurityGroup available. You need to login to the vpc console https://ecs.console.aliyun.com/ to create one and then use 'fun deploy' to deploy your resources again.`));
443 }
444}
445
446function ensureNasFileSystemsExist(nasFileSystems) {
447 if (_.isEmpty(nasFileSystems)) {
448 throw new Error(red(`\nThere is no NAS file system available. You need to login to the nas console http://nas.console.aliyun.com to create one and then use 'fun deploy' to deploy your resources again.`));
449 }
450}
451
452function ensureOnlyOneMountPoinExists(mountPoints) {
453 if (mountPoints.length > 1) {
454 throw new Error(red(`More than one 'NasConfig' configuration in template.yml.`));
455 }
456}
457
458async function getSecurityGroups(vpcId) {
459 const ecsClient = await getEcsPopClient();
460 const profile = await getProfile();
461 return await securityGroup.describeSecurityGroups(ecsClient, profile.defaultRegion, vpcId, undefined);
462}
463
464async function processNasSelection() {
465 const nasClient = await getNasPopClient();
466 const nasFileSystems = await nas.getAvailableNasFileSystems(nasClient);
467 ensureNasFileSystemsExist(nasFileSystems);
468
469 const nasAnswer = await promptForFileSystems(nasFileSystems);
470 const nasSelected = nasFileSystems.filter(f => f.fileSystemId === nasAnswer.fileSystemId);
471 const mountTargets = _.head(nasSelected).mountTargets;
472 ensureMountTargetsExist(mountTargets);
473
474 const mountTargetAnswer = await promptForMountTargets(mountTargets);
475 const mountTargetSelected = mountTargets.filter(f => f.MountTargetDomain === mountTargetAnswer.mountTargetDomain);
476 const mountTarget = _.head(mountTargetSelected);
477
478 const securityGroups = await getSecurityGroups(mountTarget.VpcId);
479 ensureSecurityGroupsExist(securityGroups);
480
481 const securityGroupAnswer = await promptForSecurityGroup(securityGroups);
482 const securityGroupId = securityGroupAnswer.securityGroupId;
483
484 return {
485 mountTarget,
486 securityGroupId
487 };
488}
489
490function replaceNasConfig(nasConfig, mountDir) {
491 const cloneNasConfig = _.cloneDeep(nasConfig);
492 cloneNasConfig.MountPoints = cloneNasConfig.MountPoints.filter(f => f.MountDir === mountDir);
493 return cloneNasConfig;
494}
495
496async function nasAutomationConfigurationIfNecessary({ tplPath, runtime, baseDir, codeUri, nasConfig, vpcConfig,
497 compressedSize,
498 nasFunctionName,
499 nasServiceName
500}) {
501
502 let stop = false;
503
504 if (compressedSize > 52428800 && _.includes(SUPPORT_RUNTIMES, runtime)) { // 50M
505 console.log(red(`\nFun detected that your function ${nasServiceName}/${nasFunctionName} sizes exceed 50M. It is recommended that using the nas service to manage your function dependencies.`));
506
507 if (await promptForConfirmContinue(`Do you want to let fun to help you automate the configuration?`)) {
508 const tpl = await getTpl(tplPath);
509 const onlyOneNas = definition.onlyOneNASExists(nasConfig);
510
511 if (definition.isNasAutoConfig(nasConfig)) {
512 const yes = await promptForConfirmContinue(`You have already configured 'NasConfig: Auto’. We want to use this configuration to store your function dependencies.`);
513 if (yes) {
514 await backupTemplateFile(tplPath); // backup tpl
515 await processNasAutomationConfiguration({
516 tpl, tplPath, baseDir, runtime, codeUri,
517 nasServiceName,
518 nasFunctionName
519 });
520
521 stop = true;
522 } else {
523 throw new Error(red(`\nIf 'NasConfig: Auto' is configured, only the configuration store function dependency is currently supported.`));
524 }
525 } else if (!_.isEmpty(vpcConfig) && _.isEmpty(nasConfig)) {
526
527 throw new Error(red(`\nFun has detected that you only have VPC configuration. This scenario is not supported at this time. You also need to manually configure the NAS service. You can refer to: https://github.com/alibaba/funcraft/blob/master/docs/specs/2018-04-03-zh-cn.md#nas-%E9%85%8D%E7%BD%AE%E5%AF%B9%E8%B1%A1 and https://nas.console.aliyun.com/`));
528 } else if (!_.isEmpty(vpcConfig) && !_.isEmpty(nasConfig)) {
529 if (onlyOneNas) {
530 const yes = await promptForConfirmContinue(`We have detected that you already have a NAS configuration. Do you directly use this NAS storage function dependencies.`);
531 if (yes) {
532 await backupTemplateFile(tplPath);
533
534 await processNasAutomationConfiguration({
535 tpl, tplPath, baseDir, runtime, codeUri,
536 nasServiceName,
537 nasFunctionName
538 });
539 } else {
540 throw new Error(red(`If your yml has been already configured with 'NasConfig', fun only supports to use this 'NasConfig' to process your function dependencies. Otherwise you need to handle the dependencies by yourself.\n\nRefer to https://yq.aliyun.com/articles/712700 for more help.`));
541 }
542 } else {
543 const answer = await promptForMountPoints(nasConfig.MountPoints);
544 const convertedNasConfig = replaceNasConfig(nasConfig, answer.mountDir);
545 await backupTemplateFile(tplPath);
546 await processNasAutomationConfiguration({
547 tpl, tplPath, baseDir, runtime, codeUri, convertedNasConfig,
548 nasServiceName,
549 nasFunctionName
550 });
551 }
552 stop = true;
553 } else if (_.isEmpty(vpcConfig) && _.isEmpty(nasConfig)) {
554 const yes = await promptForConfirmContinue(`We recommend using the 'NasConfig: Auto' configuration to manage your function dependencies.`);
555 if (yes) {
556
557 await backupTemplateFile(tplPath);
558 // write back to yml
559 const updatedTpl = updateNasAutoConfigureInTpl(tplPath, tpl, nasServiceName);
560
561 await processNasAutomationConfiguration({
562 tpl: updatedTpl, tplPath, baseDir, runtime, codeUri,
563 nasServiceName,
564 nasFunctionName
565 });
566 } else {
567 // list available NAS
568 const { mountTarget, securityGroupId } = await processNasSelection();
569
570 await backupTemplateFile(tplPath); // backup tpl
571
572 const nasAndVpcConfig = generateNasAndVpcConfig(mountTarget, securityGroupId, nasServiceName);
573 const updatedTpl = updateNasAndVpcInTpl(tplPath, tpl, nasServiceName, nasAndVpcConfig);
574
575 await processNasAutomationConfiguration({
576 tpl: updatedTpl, tplPath, baseDir, runtime, codeUri,
577 nasServiceName,
578 nasFunctionName
579 });
580 }
581 stop = true;
582 }
583 }
584 }
585 return stop;
586}
587
588function generateNasAndVpcConfig(mountTarget, securityGroupId, serviceName) {
589 const nasConfig = {
590 'UserId': 10003,
591 'GroupId': 10003,
592 'MountPoints': [
593 {
594 'ServerAddr': `${mountTarget.MountTargetDomain}:/${serviceName}`,
595 'MountDir': '/mnt/nas'
596 }
597 ]
598 };
599
600 const vpcConfig = {
601 'VpcId': mountTarget.VpcId,
602 'VSwitchIds': [mountTarget.VswId],
603 'SecurityGroupId': securityGroupId
604 };
605
606 return {
607 'VpcConfig': vpcConfig,
608 'NasConfig': nasConfig
609 };
610}
611
612async function makeFunction(baseDir, {
613 serviceName,
614 functionName,
615 description = '',
616 handler,
617 initializer = '',
618 timeout = 3,
619 initializationTimeout = 3,
620 memorySize = 128,
621 runtime = 'nodejs6',
622 codeUri,
623 environmentVariables = {},
624 instanceConcurrency,
625 nasConfig,
626 vpcConfig
627}, onlyConfig, tplPath) {
628 const fc = await getFcClient();
629
630 var fn;
631 try {
632 fn = await fc.getFunction(serviceName, functionName);
633 } catch (ex) {
634 if (ex.code !== 'FunctionNotFound') {
635 throw ex;
636 }
637 }
638
639 if (!fn && onlyConfig) {
640 throw new Error(`\nFunction '` + `${serviceName}` + '/' + `${functionName}` + `' was detected as the first deployment, and the code package had to be uploaded when creating the function. You can ` + yellow(`either`) + ` re-execute the command to remove the -u(--update-config)` + ` option ` + yellow(`or`) + ` execute 'fun deploy ${serviceName}/${functionName}' before doing so.`);
641 }
642
643 let code;
644
645 if (!onlyConfig) { // ignore code
646
647 if (codeUri && codeUri.startsWith('oss://')) { // oss://my-bucket/function.zip
648 code = extractOssCodeUri(codeUri);
649 } else {
650 console.log(`\t\tWaiting for packaging function ${functionName} code...`);
651 const { base64, count, compressedSize } = await zipCode(baseDir, codeUri, runtime, functionName);
652
653 const stop = await nasAutomationConfigurationIfNecessary({
654 compressedSize, tplPath, baseDir: getBaseDir(tplPath), runtime, nasConfig, vpcConfig,
655 nasFunctionName: functionName,
656 nasServiceName: serviceName,
657 codeUri: path.resolve(baseDir, codeUri)
658 });
659
660 if (stop) { return; }
661
662 const convertedSize = bytes(compressedSize, {
663 unitSeparator: ' '
664 });
665
666 if (!count || !compressedSize) {
667 console.log(green(`\t\tThe function ${functionName} has been packaged.`));
668 } else {
669 console.log(green(`\t\tThe function ${functionName} has been packaged. A total of ` + yellow(`${count}`) + `${count === 1 ? ' file' : ' files'}` + ` files were compressed and the final size was` + yellow(` ${convertedSize}`)));
670 }
671
672 code = {
673 zipFile: base64
674 };
675 }
676 }
677
678 const confEnv = await resolveLibPathsFromLdConf(baseDir, codeUri);
679
680 Object.assign(environmentVariables, confEnv);
681
682 const params = {
683 description,
684 handler,
685 initializer,
686 timeout,
687 initializationTimeout,
688 memorySize,
689 runtime,
690 code,
691 environmentVariables: addEnv(environmentVariables, nasConfig),
692 instanceConcurrency
693 };
694
695 for (let i in params.environmentVariables) {
696 if (!isNaN(params.environmentVariables[i])) {
697 debug(`the value in environmentVariables:${params.environmentVariables[i]} cast String Done`);
698 params.environmentVariables[i] = params.environmentVariables[i] + '';
699 }
700 }
701
702 try {
703 if (!fn) {
704 // create
705 params['functionName'] = functionName;
706 fn = await fc.createFunction(serviceName, params);
707 } else {
708 // update
709 fn = await fc.updateFunction(serviceName, functionName, params);
710 }
711 } catch (ex) {
712
713 if (ex.message.indexOf('timeout') !== -1) {
714 throw new Error(`\nError message: ${ex.message}.\n\n` + red(`This error may be caused by network latency. You can set the client timeout to a larger value through 'fun config' and try again.`));
715 }
716 throw ex;
717 }
718 return fn;
719}
720
721async function makeService({
722 serviceName,
723 role,
724 description,
725 internetAccess = true,
726 logConfig = {},
727 vpcConfig,
728 nasConfig
729}) {
730 const fc = await getFcClient();
731
732 var service;
733 await promiseRetry(async (retry, times) => {
734 try {
735 service = await fc.getService(serviceName);
736 } catch (ex) {
737
738 if (ex.code === 'AccessDenied' || !ex.code || ex.code === 'ENOTFOUND') {
739
740 if (ex.message.indexOf('the caller is not authorized to perform') !== -1) {
741
742 console.error(red(`\nMaybe you need grant AliyunRAMFullAccess policy to the subuser or use the primary account. You can refer to Chinese doc https://github.com/aliyun/fun/blob/master/docs/usage/faq-zh.md#nopermissionerror-you-are-not-authorized-to-do-this-action-resource-acsramxxxxxxxxxxrole-action-ramgetrole or English doc https://github.com/aliyun/fun/blob/master/docs/usage/faq.md#nopermissionerror-you-are-not-authorized-to-do-this-action-resource-acsramxxxxxxxxxxrole-action-ramgetrole for help.\n\nIf you don’t want use the AliyunRAMFullAccess policy or primary account, you can also specify the Role property for Service. You can refer to Chinese doc https://github.com/aliyun/fun/blob/master/docs/specs/2018-04-03-zh-cn.md#aliyunserverlessservice or English doc https://github.com/aliyun/fun/blob/master/docs/specs/2018-04-03.md#aliyunserverlessservice for help.\n`));
743
744 } else if (ex.message.indexOf('FC service is not enabled for current user') !== -1) {
745
746 console.error(red(`\nFC service is not enabled for current user. Please enable FC service before using fun.\nYou can enable FC service on this page https://www.aliyun.com/product/fc .\n`));
747
748 } else {
749 console.error(red(`\nThe accountId you entered is incorrect. You can only use the primary account id, whether or not you use a sub-account or a primary account ak. You can get primary account ID on this page https://account.console.aliyun.com/#/secure .\n`));
750 }
751
752 throw ex;
753 } else if (ex.code !== 'ServiceNotFound') {
754 debug('error when getService, serviceName is %s, error is: \n%O', serviceName, ex);
755
756 console.log(red(`\tretry ${times} times`));
757 retry(ex);
758 }
759 }
760 });
761
762 const options = {
763 description,
764 role,
765 logConfig: {
766 project: logConfig.Project || '',
767 logstore: logConfig.Logstore || ''
768 }
769 };
770
771 if (internetAccess !== null) {
772 // vpc feature is not supported in some region
773 Object.assign(options, {
774 internetAccess
775 });
776 }
777
778 const isNasAuto = definition.isNasAutoConfig(nasConfig);
779 const isVpcAuto = definition.isVpcAutoConfig(vpcConfig);
780
781 if (!_.isEmpty(vpcConfig) || isNasAuto) {
782
783 if (isVpcAuto || (_.isEmpty(vpcConfig) && isNasAuto)) {
784 console.log('\tusing \'VpcConfig: Auto\', Fun will try to generate related vpc resources automatically');
785 vpcConfig = await vpc.createDefaultVpcIfNotExist();
786 console.log(green('\tgenerated auto VpcConfig done: ', JSON.stringify(vpcConfig)));
787
788 debug('generated vpcConfig: %j', vpcConfig);
789 }
790 }
791
792 Object.assign(options, {
793 vpcConfig: vpcConfig || defaultVpcConfig
794 });
795
796 if (isNasAuto) {
797
798 const vpcId = vpcConfig.vpcId;
799 const vswitchId = _.head(vpcConfig.vswitchIds);
800
801 console.log('\tusing \'NasConfig: Auto\', Fun will try to generate related nas file system automatically');
802 nasConfig = await nas.generateAutoNasConfig(serviceName, vpcId, vswitchId, nasConfig.UserId, nasConfig.GroupId);
803 console.log(green('\tgenerated auto NasConfig done: ', JSON.stringify(nasConfig)));
804 }
805
806 Object.assign(options, {
807 nasConfig: nasConfig || defaultNasConfig
808 });
809
810 await promiseRetry(async (retry, times) => {
811 try {
812 if (!service) {
813 debug('create service %s, options is %j', serviceName, options);
814 service = await fc.createService(serviceName, options);
815 } else {
816 debug('update service %s, options is %j', serviceName, options);
817 service = await fc.updateService(serviceName, options);
818 }
819 } catch (ex) {
820 debug('error when createService or updateService, serviceName is %s, options is %j, error is: \n%O', serviceName, options, ex);
821
822 console.log(red(`\tretry ${times} times`));
823 retry(ex);
824 }
825 });
826
827 // make sure nas dir exist
828 if (serviceName !== FUN_GENERATED_SERVICE
829 && !_.isEmpty(nasConfig)
830 && !_.isEmpty(nasConfig.MountPoints)) {
831
832 await ensureNasDirExist({
833 role, vpcConfig, nasConfig
834 });
835 }
836
837 return service;
838}
839
840function mapMountPointDir(mountPoints, func) {
841 let resolvedMountPoints = _.map(mountPoints, (mountPoint) => {
842 const serverAddr = mountPoint.ServerAddr;
843
844 const index = _.lastIndexOf(serverAddr, ':');
845 if (index >= 0) {
846 const mountPointDomain = serverAddr.substring(0, index);
847 const remoteDir = serverAddr.substring(index + 1);
848 const mountDir = mountPoint.MountDir;
849
850 debug('remoteDir is: %s', remoteDir);
851
852 return func(mountPointDomain, remoteDir, mountDir);
853 }
854 });
855
856 resolvedMountPoints = _.compact(resolvedMountPoints);
857
858 return resolvedMountPoints;
859}
860
861const EXTREME_PATH_PREFIX = '/share';
862
863function checkMountPointDomainIsExtremeNas(mountPointDomain, remoteDir) {
864 const isExtremeNAS = mountPointDomain.indexOf('.extreme.nas.aliyuncs.com') !== -1;
865
866 if (isExtremeNAS && (remoteDir !== EXTREME_PATH_PREFIX && !remoteDir.startsWith(EXTREME_PATH_PREFIX + '/'))) {
867 throw new Error('Extreme nas mount point must start with /share. Please refer to https://nas.console.aliyun.com/#/extreme for more help.');
868 }
869
870 return isExtremeNAS;
871}
872
873async function ensureNasDirExist({
874 role,
875 vpcConfig,
876 nasConfig
877}) {
878 const mountPoints = nasConfig.MountPoints;
879 const modifiedNasConfig = _.cloneDeep(nasConfig);
880
881 modifiedNasConfig.MountPoints = mapMountPointDir(mountPoints, (mountPointDomain, remoteDir, mountDir) => {
882
883 if (checkMountPointDomainIsExtremeNas(mountPointDomain, remoteDir)) {
884 // 极速 nas
885 return {
886 ServerAddr: `${mountPointDomain}:${EXTREME_PATH_PREFIX}`,
887 MountDir: `${mountDir}`
888 };
889 } else if (remoteDir !== '/') {
890 return {
891 ServerAddr: `${mountPointDomain}:/`,
892 MountDir: `${mountDir}`
893 };
894 } return null;
895 });
896
897 const nasMountDirs = mapMountPointDir(mountPoints, (mountPointDomain, remoteDir, mountDir) => {
898 if (checkMountPointDomainIsExtremeNas(mountPointDomain, remoteDir)) {
899 if (remoteDir !== EXTREME_PATH_PREFIX) {
900 return { mountDir, remoteDir, isExtreme: true };
901 }
902 } else if (remoteDir !== '/') {
903 return { mountDir, remoteDir, isExtreme: false };
904 }
905 return null;
906 });
907
908 debug('dirs need to check: %s', nasMountDirs);
909
910 if (!_.isEmpty(nasMountDirs)) {
911 let nasRemoteDirs = [];
912 let nasDirsNeedToCheck = [];
913 for (let nasMountDir of nasMountDirs) {
914 nasRemoteDirs.push(nasMountDir.remoteDir);
915 if (nasMountDir.isExtreme) {
916 // 002aab55-fbdt.cn-hangzhou.extreme.nas.aliyuncs.com:/share
917 nasDirsNeedToCheck.push(path.posix.join(nasMountDir.mountDir, nasMountDir.remoteDir.substring(EXTREME_PATH_PREFIX.length)));
918 } else {
919 nasDirsNeedToCheck.push(path.posix.join(nasMountDir.mountDir, nasMountDir.remoteDir));
920 }
921 }
922
923 console.log(`\tChecking if nas directories ${nasRemoteDirs} exists, if not, it will be created automatically`);
924
925 const utilFunctionName = await makeFcUtilsFunctionNasDirChecker(role, vpcConfig, modifiedNasConfig);
926 await sleep(1000);
927 await invokeFcUtilsFunction({
928 functionName: utilFunctionName,
929 event: JSON.stringify(nasDirsNeedToCheck)
930 });
931
932 console.log(green('\tChecking nas directories done', JSON.stringify(nasRemoteDirs)));
933 }
934}
935
936async function makeFcUtilsService(role, vpcConfig, nasConfig) {
937 return await makeService({
938 serviceName: FUN_GENERATED_SERVICE,
939 role,
940 description: 'generated by Funcraft',
941 vpcConfig,
942 nasConfig
943 });
944}
945
946async function makeFcUtilsFunction({
947 serviceName,
948 functionName,
949 codes,
950 description = '',
951 handler,
952 timeout = 60,
953 memorySize = 128,
954 runtime = 'nodejs8'
955}) {
956 const fc = await getFcClient();
957
958 var fn;
959 try {
960 fn = await fc.getFunction(serviceName, functionName);
961 } catch (ex) {
962 if (ex.code !== 'FunctionNotFound') {
963 throw ex;
964 }
965 }
966
967 const base64 = await zip.packFromJson(codes);
968
969 let code = {
970 zipFile: base64
971 };
972
973 const params = {
974 description,
975 handler,
976 initializer: '',
977 timeout,
978 memorySize,
979 runtime,
980 code
981 };
982
983 if (!fn) {
984 // create
985 params['functionName'] = functionName;
986 fn = await fc.createFunction(serviceName, params);
987 } else {
988 // update
989 fn = await fc.updateFunction(serviceName, functionName, params);
990 }
991
992 return fn;
993}
994
995async function invokeFcUtilsFunction({
996 functionName,
997 event
998}) {
999 const fc = await getFcClient();
1000 const rs = await fc.invokeFunction(FUN_GENERATED_SERVICE, functionName, event, {
1001 'X-Fc-Log-Type': 'Tail'
1002 });
1003
1004 if (rs.data !== 'OK') {
1005 const log = rs.headers['x-fc-log-result'];
1006
1007 if (log) {
1008 const decodedLog = Buffer.from(log, 'base64');
1009 if ((decodedLog.toString().toLowerCase()).includes('permission denied')) {
1010 throw new Error(`fc utils function ${functionName} invoke error, error message is: ${decodedLog}\n${red('May be UserId and GroupId in NasConfig don\'t have enough \
1011permission, more information please refer to https://github.com/alibaba/funcraft/blob/master/docs/usage/faq-zh.md')}`);
1012 }
1013 throw new Error(`fc utils function ${functionName} invoke error, error message is: ${decodedLog}`);
1014 }
1015 }
1016}
1017
1018async function getFcUtilsFunctionCode(filename) {
1019 return await fs.readFile(path.join(__dirname, 'utils', filename));
1020}
1021
1022async function makeFcUtilsFunctionNasDirChecker(role, vpcConfig, nasConfig) {
1023 await makeFcUtilsService(role, vpcConfig, nasConfig);
1024
1025 const functionName = 'nas_dir_checker';
1026
1027 const functionCode = await getFcUtilsFunctionCode('nas-dir-check.js');
1028
1029 const codes = {
1030 'index.js': functionCode
1031 };
1032
1033 await makeFcUtilsFunction({
1034 serviceName: FUN_GENERATED_SERVICE,
1035 functionName: 'nas_dir_checker',
1036 codes,
1037 description: 'used for fun to ensure nas remote dir exist',
1038 handler: 'index.handler'
1039 });
1040
1041 return functionName;
1042}
1043
1044
1045async function invokeFunction({
1046 serviceName,
1047 functionName,
1048 event,
1049 invocationType
1050}) {
1051
1052 var rs;
1053 const fc = await getFcClient();
1054
1055 if (invocationType === 'Sync') {
1056
1057 rs = await fc.invokeFunction(serviceName, functionName, event, {
1058 'X-Fc-Log-Type': 'Tail',
1059 'X-Fc-Invocation-Type': invocationType
1060 });
1061
1062 const log = rs.headers['x-fc-log-result'];
1063
1064 if (log) {
1065
1066 console.log(yellow('========= FC invoke Logs begin ========='));
1067 const decodedLog = Buffer.from(log, 'base64');
1068 console.log(decodedLog.toString());
1069 console.log(yellow('========= FC invoke Logs end ========='));
1070
1071 console.log(green('\nFC Invoke Result:'));
1072 console.log(rs.data);
1073 }
1074 } else {
1075
1076 rs = await fc.invokeFunction(serviceName, functionName, event, {
1077 'X-Fc-Invocation-Type': invocationType
1078 });
1079
1080 console.log(green('✔ ') + `${serviceName}/${functionName} async invoke success.`);
1081 }
1082
1083 return rs;
1084}
1085
1086module.exports = {
1087 invokeFcUtilsFunction,
1088 makeFcUtilsFunctionNasDirChecker,
1089 FUN_GENERATED_SERVICE,
1090 makeService,
1091 makeFunction,
1092 zipCode,
1093 detectLibrary,
1094 getFcUtilsFunctionCode,
1095 invokeFunction,
1096 generateFunIngore
1097};
\No newline at end of file