UNPKG

17.4 kBJavaScriptView Raw
1'use strict';
2
3const { getFcClient } = require('./client');
4const vpc = require('./vpc');
5const nas = require('./nas');
6
7const fs = require('fs-extra');
8const path = require('path');
9const debug = require('debug')('fun:fc');
10const zip = require('./package/zip');
11const { green, red, yellow } = require('colors');
12const { addEnv, resolveLibPathsFromLdConf } = require('./install/env');
13const funignore = require('./package/ignore');
14const _ = require('lodash');
15const bytes = require('bytes');
16const { sleep } = require('./time');
17
18const definition = require('./definition');
19
20const promiseRetry = require('./retry');
21
22const FUN_GENERATED_SERVICE = 'fun-generated-default-service';
23
24const defaultVpcConfig = {
25 securityGroupId: '',
26 vSwitchIds: [],
27 vpcId: ''
28};
29
30const defaultNasConfig = {
31 UserId: -1,
32 GroupId: -1,
33 MountPoints: []
34};
35
36function generateFunIngore(baseDir, codeUri) {
37 const absCodeUri = path.resolve(baseDir, codeUri);
38 const absBaseDir = path.resolve(baseDir);
39
40 const relative = path.relative(absBaseDir, absCodeUri);
41
42 if (codeUri.startsWith('..') || relative.startsWith('..')) {
43 console.warn(red(`\t\twarning: funignore is not supported for your CodeUri: ${codeUri}`));
44 return null;
45 }
46
47 return funignore(baseDir);
48}
49
50const runtimeTypeMapping = {
51 'nodejs6': 'node_modules',
52 'nodejs8': 'node_modules',
53 'python2.7': ['.egg-info', '.dist-info', '.fun'],
54 'python3': ['.egg-info', '.dist-info', '.fun'],
55 'php7.2': ['extension', 'vendor']
56};
57
58async function detectLibraryFolders(dirName, libraryFolders, childDir, wrap, functionName) {
59 if (Array.isArray(libraryFolders)) {
60 for (const iterator of libraryFolders) {
61 for (const name of childDir) {
62 if (_.endsWith(name, iterator)) {
63 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.`));
64 return;
65 }
66 }
67 }
68 } else {
69 if (childDir.includes(libraryFolders)) {
70 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.`));
71 } else {
72
73 const funDir = childDir.filter(p => p === '.fun');
74 if (Array.isArray(funDir) && funDir.length > 0) {
75
76 const childFun = await fs.readdir(path.join(dirName, '.fun'));
77
78 if (childFun.includes('root')) {
79
80 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.`));
81
82 }
83 }
84 }
85 }
86}
87
88async function detectLibrary(codeUri, runtime, baseDir, functionName, wrap = '') {
89 const absoluteCodePath = path.resolve(baseDir, codeUri);
90
91 const stats = await fs.lstat(absoluteCodePath);
92 if (stats.isFile()) {
93 let libraryFolders = runtimeTypeMapping[runtime];
94
95 const dirName = path.dirname(absoluteCodePath);
96 const childDir = await fs.readdir(dirName);
97
98 await detectLibraryFolders(dirName, libraryFolders, childDir, wrap, functionName);
99 }
100}
101
102function extractOssCodeUri(ossUri) {
103 const prefixLength = 'oss://'.length;
104
105 const index = ossUri.indexOf('/', prefixLength);
106
107 return {
108 ossBucketName: ossUri.substring(prefixLength, index),
109 ossObjectName: ossUri.substring(index + 1)
110 };
111}
112
113async function zipCode(baseDir, codeUri, runtime, functionName) {
114 let codeAbsPath;
115
116 if (codeUri) {
117 codeAbsPath = path.resolve(baseDir, codeUri);
118
119 if (codeUri.endsWith('.zip') || codeUri.endsWith('.jar') || codeUri.endsWith('.war')) {
120 return { base64: Buffer.from(await fs.readFile(codeAbsPath)).toString('base64') };
121 }
122 } else {
123 codeAbsPath = path.resolve(baseDir, './');
124 }
125
126 const ignore = generateFunIngore(baseDir, codeAbsPath);
127
128 await detectLibrary(codeAbsPath, runtime, baseDir, functionName, '\t\t');
129
130 return await zip.pack(codeAbsPath, ignore);
131}
132
133async function makeFunction(baseDir, {
134 serviceName,
135 functionName,
136 description = '',
137 handler,
138 initializer = '',
139 timeout = 3,
140 initializationTimeout = 3,
141 memorySize = 128,
142 runtime = 'nodejs6',
143 codeUri,
144 environmentVariables = {},
145 nasConfig
146}, onlyConfig) {
147 const fc = await getFcClient();
148
149 var fn;
150 try {
151 fn = await fc.getFunction(serviceName, functionName);
152 } catch (ex) {
153 if (ex.code !== 'FunctionNotFound') {
154 throw ex;
155 }
156 }
157
158 if (!fn && onlyConfig) {
159
160 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.`);
161 }
162
163 let code;
164
165 if (!onlyConfig) { // ignore code
166
167 if (codeUri && codeUri.startsWith('oss://')) { // oss://my-bucket/function.zip
168 code = extractOssCodeUri(codeUri);
169 } else {
170 console.log(`\t\tWaiting for packaging function ${functionName} code...`);
171 const { base64, count, compressedSize } = await zipCode(baseDir, codeUri, runtime, functionName);
172
173 const convertedSize = bytes(compressedSize, {
174 unitSeparator: ' '
175 });
176
177 if (!count || !compressedSize) {
178 console.log(green(`\t\tThe function ${functionName} has been packaged.`));
179 } else {
180 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}`)));
181 }
182
183 code = {
184 zipFile: base64
185 };
186 }
187 }
188
189 const confEnv = await resolveLibPathsFromLdConf(baseDir, codeUri);
190
191 Object.assign(environmentVariables, confEnv);
192
193 const params = {
194 description,
195 handler,
196 initializer,
197 timeout,
198 initializationTimeout,
199 memorySize,
200 runtime,
201 code,
202 environmentVariables: addEnv(environmentVariables, nasConfig)
203 };
204
205 for (let i in params.environmentVariables) {
206 if (!isNaN(params.environmentVariables[i])) {
207 debug(`the value in environmentVariables:${params.environmentVariables[i]} cast String Done`);
208 params.environmentVariables[i] = params.environmentVariables[i] + '';
209 }
210 }
211
212 try {
213
214 if (!fn) {
215 // create
216 params['functionName'] = functionName;
217 fn = await fc.createFunction(serviceName, params);
218 } else {
219 // update
220 fn = await fc.updateFunction(serviceName, functionName, params);
221 }
222 } catch (ex) {
223
224 if (ex.message.indexOf('timeout') !== -1) {
225
226 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.`));
227 }
228 throw ex;
229 }
230
231 return fn;
232}
233
234async function makeService({
235 serviceName,
236 role,
237 description,
238 internetAccess = true,
239 logConfig = {},
240 vpcConfig,
241 nasConfig
242}) {
243 const fc = await getFcClient();
244
245 var service;
246 await promiseRetry(async (retry, times) => {
247 try {
248 service = await fc.getService(serviceName);
249 } catch (ex) {
250
251 if (ex.code === 'AccessDenied' || !ex.code || ex.code === 'ENOTFOUND') {
252
253 if (ex.message.indexOf('the caller is not authorized to perform') !== -1) {
254
255 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 Chinease doc https://github.com/aliyun/fun/blob/master/docs/specs/2018-04-03-zh-cn.md#aliyunserverlessservice or Enaglish doc https://github.com/aliyun/fun/blob/master/docs/specs/2018-04-03.md#aliyunserverlessservice for help.\n`));
256
257 } else if (ex.message.indexOf('FC service is not enabled for current user') !== -1) {
258
259 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`));
260
261 } else {
262 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`));
263 }
264
265 throw ex;
266 } else if (ex.code !== 'ServiceNotFound') {
267 debug('error when getService, serviceName is %s, error is: \n%O', serviceName, ex);
268
269 console.log(red(`\tretry ${times} times`));
270 retry(ex);
271 }
272 }
273 });
274
275 const options = {
276 description,
277 role,
278 logConfig: {
279 project: logConfig.Project || '',
280 logstore: logConfig.Logstore || ''
281 }
282 };
283
284 if (internetAccess !== null) {
285 // vpc feature is not supported in some region
286 Object.assign(options, {
287 internetAccess
288 });
289 }
290
291 const isNasAuto = definition.isNasAutoConfig(nasConfig);
292 const isVpcAuto = definition.isVpcAutoConfig(vpcConfig);
293
294 if (!_.isEmpty(vpcConfig) || isNasAuto) {
295
296 if (isVpcAuto || (_.isEmpty(vpcConfig) && isNasAuto)) {
297 console.log('\tusing \'VpcConfig: Auto\', Fun will try to generate related vpc resources automatically');
298 vpcConfig = await vpc.createDefaultVpcIfNotExist();
299 console.log(green('\tgenerated auto VpcConfig done: ', JSON.stringify(vpcConfig)));
300
301 debug('generated vpcConfig: %j', vpcConfig);
302 }
303 }
304
305 Object.assign(options, {
306 vpcConfig: vpcConfig || defaultVpcConfig
307 });
308
309 if (isNasAuto) {
310
311 const vpcId = vpcConfig.vpcId;
312 const vswitchId = _.head(vpcConfig.vswitchIds);
313
314 console.log('\tusing \'NasConfig: Auto\', Fun will try to generate related nas file system automatically');
315 nasConfig = await nas.generateAutoNasConfig(serviceName, vpcId, vswitchId);
316 console.log(green('\tgenerated auto NasConfig done: ', JSON.stringify(nasConfig)));
317 }
318
319 Object.assign(options, {
320 nasConfig: nasConfig || defaultNasConfig
321 });
322
323 await promiseRetry(async (retry, times) => {
324 try {
325 if (!service) {
326 debug('create service %s, options is %j', serviceName, options);
327 service = await fc.createService(serviceName, options);
328 } else {
329 debug('update service %s, options is %j', serviceName, options);
330 service = await fc.updateService(serviceName, options);
331 }
332 } catch (ex) {
333 debug('error when createService or updateService, serviceName is %s, options is %j, error is: \n%O', serviceName, options, ex);
334
335 console.log(red(`\tretry ${times} times`));
336 retry(ex);
337 }
338 });
339
340 // make sure nas dir exist
341 if (serviceName !== FUN_GENERATED_SERVICE
342 && !_.isEmpty(nasConfig)
343 && !_.isEmpty(nasConfig.MountPoints)) {
344
345 await ensureNasDirExist({
346 role, vpcConfig, nasConfig
347 });
348 }
349
350 return service;
351}
352
353function mapMountPointDir(mountPoints, func) {
354 let resolvedMountPoints = _.map(mountPoints, (mountPoint) => {
355 const serverAddr = mountPoint.ServerAddr;
356
357 const index = _.lastIndexOf(serverAddr, ':');
358 if (index >= 0) {
359 const mountPointDomain = serverAddr.substring(0, index);
360 const remoteDir = serverAddr.substring(index + 1);
361 const mountDir = mountPoint.MountDir;
362
363 debug('remoteDir is: %s', remoteDir);
364
365 return func(mountPointDomain, remoteDir, mountDir);
366 }
367 });
368
369 resolvedMountPoints = _.compact(resolvedMountPoints);
370
371 return resolvedMountPoints;
372}
373
374async function ensureNasDirExist({
375 role,
376 vpcConfig,
377 nasConfig
378}) {
379 const mountPoints = nasConfig.MountPoints;
380 const modifiedNasConfig = _.cloneDeep(nasConfig);
381
382 modifiedNasConfig.MountPoints = mapMountPointDir(mountPoints, (mountPointDomain, remoteDir, mountDir) => {
383 if (remoteDir !== '/') {
384 return {
385 ServerAddr: `${mountPointDomain}:/`,
386 MountDir: `${mountDir}`
387 };
388 } return null;
389 });
390
391 const nasMountDirs = mapMountPointDir(mountPoints, (mountPointDomain, remoteDir, mountDir) => {
392 if (remoteDir !== '/') {
393 return { mountDir, remoteDir };
394 }
395 return null;
396 });
397
398 debug('dirs need to check: %s', nasMountDirs);
399
400 if (!_.isEmpty(nasMountDirs)) {
401 let nasRemoteDirs = [];
402 let nasDirsNeedToCheck = [];
403 for (let nasMountDir of nasMountDirs) {
404 nasRemoteDirs.push(nasMountDir.remoteDir);
405 nasDirsNeedToCheck.push(path.posix.join(nasMountDir.mountDir, nasMountDir.remoteDir));
406 }
407 console.log(`\tChecking if nas directories ${nasRemoteDirs} exists, if not, it will be created automatically`);
408
409 const utilFunctionName = await makeFcUtilsFunctionNasDirChecker(role, vpcConfig, modifiedNasConfig);
410 await sleep(1000);
411 await invokeFcUtilsFunction({
412 functionName: utilFunctionName,
413 event: JSON.stringify(nasDirsNeedToCheck)
414 });
415
416 console.log(green('\tChecking nas directories done', JSON.stringify(nasRemoteDirs)));
417 }
418}
419
420async function makeFcUtilsService(role, vpcConfig, nasConfig) {
421 return await makeService({
422 serviceName: FUN_GENERATED_SERVICE,
423 role,
424 description: 'generated by Funcraft',
425 vpcConfig,
426 nasConfig
427 });
428}
429
430async function makeFcUtilsFunction({
431 serviceName,
432 functionName,
433 codes,
434 description = '',
435 handler,
436 timeout = 60,
437 memorySize = 128,
438 runtime = 'nodejs8'
439}) {
440 const fc = await getFcClient();
441
442 var fn;
443 try {
444 fn = await fc.getFunction(serviceName, functionName);
445 } catch (ex) {
446 if (ex.code !== 'FunctionNotFound') {
447 throw ex;
448 }
449 }
450
451 const base64 = await zip.packFromJson(codes);
452
453 let code = {
454 zipFile: base64
455 };
456
457 const params = {
458 description,
459 handler,
460 initializer: '',
461 timeout,
462 memorySize,
463 runtime,
464 code
465 };
466
467 if (!fn) {
468 // create
469 params['functionName'] = functionName;
470 fn = await fc.createFunction(serviceName, params);
471 } else {
472 // update
473 fn = await fc.updateFunction(serviceName, functionName, params);
474 }
475
476 return fn;
477}
478
479async function invokeFcUtilsFunction({
480 functionName,
481 event
482}) {
483 const fc = await getFcClient();
484 const rs = await fc.invokeFunction(FUN_GENERATED_SERVICE, functionName, event, {
485 'X-Fc-Log-Type': 'Tail'
486 });
487
488 if (rs.data !== 'OK') {
489 const log = rs.headers['x-fc-log-result'];
490
491 if (log) {
492 const decodedLog = Buffer.from(log, 'base64');
493 if ((decodedLog.toString().toLowerCase()).includes('permission denied')) {
494 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 \
495permission, more information please refer to https://github.com/alibaba/funcraft/blob/master/docs/usage/faq-zh.md')}`);
496 }
497 throw new Error(`fc utils function ${functionName} invoke error, error message is: ${decodedLog}`);
498 }
499 }
500}
501
502async function getFcUtilsFunctionCode(filename) {
503 return await fs.readFile(path.join(__dirname, 'utils', filename));
504}
505
506async function makeFcUtilsFunctionNasDirChecker(role, vpcConfig, nasConfig) {
507 await makeFcUtilsService(role, vpcConfig, nasConfig);
508
509 const functionName = 'nas_dir_checker';
510
511 const functionCode = await getFcUtilsFunctionCode('nas-dir-check.js');
512
513 const codes = {
514 'index.js': functionCode
515 };
516
517 await makeFcUtilsFunction({
518 serviceName: FUN_GENERATED_SERVICE,
519 functionName: 'nas_dir_checker',
520 codes,
521 description: 'used for fun to ensure nas remote dir exist',
522 handler: 'index.handler'
523 });
524
525 return functionName;
526}
527
528
529async function invokeFunction({
530 serviceName,
531 functionName,
532 event,
533 invocationType
534}) {
535
536 var rs;
537 const fc = await getFcClient();
538
539 if (invocationType === 'Sync') {
540
541 rs = await fc.invokeFunction(serviceName, functionName, event, {
542 'X-Fc-Log-Type': 'Tail',
543 'X-Fc-Invocation-Type': invocationType
544 });
545
546 const log = rs.headers['x-fc-log-result'];
547
548 if (log) {
549
550 console.log(yellow('========= FC invoke Logs begin ========='));
551 const decodedLog = Buffer.from(log, 'base64');
552 console.log(decodedLog.toString());
553 console.log(yellow('========= FC invoke Logs end ========='));
554
555 console.log(green('\nFC Invoke Result:'));
556 console.log(rs.data);
557 }
558 } else {
559
560 rs = await fc.invokeFunction(serviceName, functionName, event, {
561 'X-Fc-Invocation-Type': invocationType
562 });
563
564 console.log(green('✔ ') + `${serviceName}/${functionName} async invoke success.`);
565 }
566
567 return rs;
568}
569
570module.exports = {
571 invokeFcUtilsFunction,
572 makeFcUtilsFunctionNasDirChecker,
573 FUN_GENERATED_SERVICE,
574 makeService,
575 makeFunction,
576 zipCode,
577 detectLibrary,
578 getFcUtilsFunctionCode,
579 invokeFunction,
580 generateFunIngore
581};
\No newline at end of file