1 | 'use strict';
|
2 |
|
3 | const util = require('./import/utils');
|
4 | const bytes = require('bytes');
|
5 | const funignore = require('./package/ignore');
|
6 | const definition = require('./definition');
|
7 | const promiseRetry = require('./retry');
|
8 | const getProfile = require('./profile').getProfile;
|
9 | const securityGroup = require('./security-group');
|
10 |
|
11 | const fs = require('fs-extra');
|
12 | const path = require('path');
|
13 | const debug = require('debug')('fun:fc');
|
14 | const yaml = require('js-yaml');
|
15 | const zip = require('./package/zip');
|
16 | const vpc = require('./vpc');
|
17 | const nas = require('./nas');
|
18 | const nasCp = require('./nas/cp');
|
19 |
|
20 | const { sleep } = require('./time');
|
21 | const { buildFunction } = require('./build/build');
|
22 | const { readJsonFromFile } = require('./utils/file');
|
23 | const { getTpl, getBaseDir, getNasYmlPath, DEFAULT_BUILD_ARTIFACTS_PATH_SUFFIX } = require('./tpl');
|
24 | const { green, red, yellow } = require('colors');
|
25 | const { getFcClient, getEcsPopClient, getNasPopClient } = require('./client');
|
26 | const { addEnv, mergeEnvs, resolveLibPathsFromLdConf, generateDefaultLibPath } = require('./install/env');
|
27 | const { readFileFromNasYml, mergeNasMappingsInNasYml, getNasMappingsFromNasYml, extractNasMappingsFromNasYml } = require('./nas/support');
|
28 |
|
29 | const _ = require('lodash');
|
30 | const _util = require('util');
|
31 |
|
32 | const {
|
33 | promptForConfirmContinue,
|
34 | promptForMountTargets,
|
35 | promptForMountPoints,
|
36 | promptForFileSystems,
|
37 | promptForSecurityGroup,
|
38 | promptForInputContinue
|
39 | } = require('./init/prompt');
|
40 |
|
41 | const FUN_GENERATED_SERVICE = 'fun-generated-default-service';
|
42 |
|
43 | const SYSTEM_DEPENDENCY_PATH = path.join('.fun', 'root');
|
44 |
|
45 | const SUPPORT_RUNTIMES = ['nodejs6', 'nodejs8', 'nodejs10', 'python2.7', 'python3', 'java8', 'custom'];
|
46 |
|
47 | const defaultVpcConfig = {
|
48 | securityGroupId: '',
|
49 | vSwitchIds: [],
|
50 | vpcId: ''
|
51 | };
|
52 |
|
53 | const defaultNasConfig = {
|
54 | UserId: -1,
|
55 | GroupId: -1,
|
56 | MountPoints: []
|
57 | };
|
58 |
|
59 | async function generateFunIngore(baseDir, codeUri, runtime) {
|
60 | const absCodeUri = path.resolve(baseDir, codeUri);
|
61 | const absBaseDir = path.resolve(baseDir);
|
62 |
|
63 | const relative = path.relative(absBaseDir, absCodeUri);
|
64 |
|
65 | if (codeUri.startsWith('..') || relative.startsWith('..')) {
|
66 | console.warn(red(`\t\twarning: funignore is not supported for your CodeUri: ${codeUri}`));
|
67 | return null;
|
68 | }
|
69 |
|
70 | return await funignore(baseDir, runtime);
|
71 | }
|
72 |
|
73 |
|
74 | const runtimeTypeMapping = {
|
75 | 'nodejs6': ['node_modules', '.fun/root'],
|
76 | 'nodejs8': ['node_modules', '.fun/root'],
|
77 | 'nodejs10': ['node_modules', '.fun/root'],
|
78 | 'python2.7': ['.fun/python', '.fun/root'],
|
79 | 'python3': ['.fun/python', '.fun/root'],
|
80 | 'php7.2': ['extension', 'vendor', '.fun/root']
|
81 | };
|
82 |
|
83 | async function detectLibraryFolders(dirName, libraryFolders, wrap, functionName) {
|
84 | if (_.isEmpty(libraryFolders)) { return; }
|
85 |
|
86 | for (const libraryFolder of libraryFolders) {
|
87 | const libraryPath = path.join(dirName, libraryFolder);
|
88 | if (await fs.pathExists(libraryPath)) {
|
89 | console.warn(red(`${wrap}Fun detected that the library directory '${libraryFolder}' is not included in function '${functionName}' CodeUri.\n\t\tPlease make sure if it is the right configuration. if yes, ignore please.`));
|
90 | return;
|
91 | }
|
92 | }
|
93 | }
|
94 |
|
95 | async function detectLibrary(codeUri, runtime, baseDir, functionName, wrap = '') {
|
96 | const absoluteCodePath = path.resolve(baseDir, codeUri);
|
97 |
|
98 | const stats = await fs.lstat(absoluteCodePath);
|
99 | if (stats.isFile()) {
|
100 | let libraryFolders = runtimeTypeMapping[runtime];
|
101 |
|
102 | await detectLibraryFolders(path.dirname(absoluteCodePath), libraryFolders, wrap, functionName);
|
103 | }
|
104 | }
|
105 |
|
106 | function extractOssCodeUri(ossUri) {
|
107 | const prefixLength = 'oss://'.length;
|
108 |
|
109 | const index = ossUri.indexOf('/', prefixLength);
|
110 |
|
111 | return {
|
112 | ossBucketName: ossUri.substring(prefixLength, index),
|
113 | ossObjectName: ossUri.substring(index + 1)
|
114 | };
|
115 | }
|
116 |
|
117 | async function zipCode(baseDir, codeUri, runtime, functionName) {
|
118 | let codeAbsPath;
|
119 |
|
120 | if (codeUri) {
|
121 | codeAbsPath = path.resolve(baseDir, codeUri);
|
122 |
|
123 | if (codeUri.endsWith('.zip') || codeUri.endsWith('.jar') || codeUri.endsWith('.war')) {
|
124 |
|
125 | const lstat = await fs.stat(codeAbsPath);
|
126 | return {
|
127 | base64: Buffer.from(await fs.readFile(codeAbsPath)).toString('base64'),
|
128 | compressedSize: lstat.size
|
129 | };
|
130 | }
|
131 | } else {
|
132 | codeAbsPath = path.resolve(baseDir, './');
|
133 | }
|
134 |
|
135 | const ignore = await generateFunIngore(baseDir, codeAbsPath, runtime);
|
136 |
|
137 | await detectLibrary(codeAbsPath, runtime, baseDir, functionName, '\t\t');
|
138 |
|
139 | return await zip.pack(codeAbsPath, ignore);
|
140 | }
|
141 |
|
142 | const NODE_RUNTIME_MAPPING = {
|
143 | 'localDir': 'node_modules',
|
144 | 'remoteDir': 'node_modules',
|
145 | 'env': 'NODE_PATH',
|
146 | 'defaultEnv': '/usr/local/lib/node_modules'
|
147 | };
|
148 |
|
149 | const PYTHON_RUNTIME_MAPPING = {
|
150 | 'localDir': '.fun/python',
|
151 | 'remoteDir': 'python',
|
152 | 'env': 'PYTHONUSERBASE'
|
153 | };
|
154 |
|
155 | const JAVA_RUNTIME_MAPPING = {
|
156 | 'localDir': '.fun/build/artifacts',
|
157 | 'remoteDir': 'java',
|
158 | 'env': 'JAVA_PATH'
|
159 | };
|
160 |
|
161 | const runtimeDependencyMappings = {
|
162 | 'nodejs6': [ NODE_RUNTIME_MAPPING ],
|
163 | 'nodejs8': [ NODE_RUNTIME_MAPPING ],
|
164 | 'nodejs10': [ NODE_RUNTIME_MAPPING ],
|
165 | 'python2.7': [ PYTHON_RUNTIME_MAPPING ],
|
166 | 'python3': [ PYTHON_RUNTIME_MAPPING ],
|
167 | 'java8': [ JAVA_RUNTIME_MAPPING ],
|
168 | 'custom': [ NODE_RUNTIME_MAPPING, PYTHON_RUNTIME_MAPPING ]
|
169 | };
|
170 |
|
171 | async function saveNasMappings(nasYmlPath, nasMappings) {
|
172 |
|
173 | if (_.isEmpty(nasMappings)) { return {}; }
|
174 |
|
175 | const contentObj = await readFileFromNasYml(nasYmlPath);
|
176 |
|
177 | const mergedNasMappings = await mergeNasMappingsInNasYml(nasYmlPath, nasMappings);
|
178 |
|
179 | contentObj.nasMappings = mergedNasMappings;
|
180 |
|
181 | await fs.writeFile(nasYmlPath, yaml.dump(contentObj));
|
182 |
|
183 | return mergedNasMappings;
|
184 | }
|
185 |
|
186 | async function copyEntryPoint(tplPath, codeUri, packageName) {
|
187 | const packagePaths = packageName.split('.');
|
188 |
|
189 | const asbCodeUri = path.resolve(path.dirname(tplPath), codeUri);
|
190 | const entryPointPath = path.join(asbCodeUri, 'src', 'main', 'java', ...packagePaths, 'Entrypoint.java');
|
191 |
|
192 | const sourcePath = path.resolve(__dirname, './utils/classLoader/Entrypoint.java');
|
193 | const content = await fs.readFile(sourcePath, {
|
194 | encoding: 'utf8'
|
195 | });
|
196 |
|
197 | const updatedContent = _util.format(content, packageName);
|
198 |
|
199 | await fs.writeFile(entryPointPath, updatedContent, {
|
200 | encoding: 'utf8'
|
201 | });
|
202 | }
|
203 |
|
204 |
|
205 | function extractJavaHandlerPackage(handler) {
|
206 | const splitArray = handler.split('::');
|
207 | const packageArray = splitArray[0].split('.').filter((ele, idx, arr) => {
|
208 | return arr.length - 1 !== idx;
|
209 | });
|
210 | return packageArray.join('.');
|
211 | }
|
212 |
|
213 | async function buildOnMeta(baseDir, tpl, tplPath) {
|
214 |
|
215 | const metaPath = path.resolve(baseDir, DEFAULT_BUILD_ARTIFACTS_PATH_SUFFIX, 'meta.json');
|
216 |
|
217 | if (!await fs.pathExists(metaPath)) {
|
218 | console.warn(red(`\nwarning: ${metaPath} does not exist.`));
|
219 | return;
|
220 | }
|
221 |
|
222 | const metaObj = await readJsonFromFile(metaPath);
|
223 |
|
224 | const { buildName, useDocker, verbose } = metaObj.buildOps || {};
|
225 |
|
226 | console.log(yellow(`\nFun will execute 'fun build' to build functions.`));
|
227 |
|
228 |
|
229 | await buildFunction(buildName, tpl, baseDir, useDocker, ['install', 'build'], verbose, tplPath);
|
230 | }
|
231 |
|
232 |
|
233 | async function updateInitializerAndEnvs({tplPath, tpl,
|
234 | serviceName,
|
235 | functionName
|
236 | }) {
|
237 | const updatedTplContent = _.cloneDeep(tpl);
|
238 |
|
239 | const { functionRes } = definition.findFunctionByServiceAndFunctionName(updatedTplContent.Resources, serviceName, functionName);
|
240 |
|
241 | const packageName = extractJavaHandlerPackage(functionRes.Properties.Handler);
|
242 |
|
243 | const originHandler = functionRes.Properties.Handler;
|
244 | const originInitializer = functionRes.Properties.Initializer;
|
245 |
|
246 | functionRes.Properties.Handler = `${packageName}.Entrypoint::handleRequest`;
|
247 |
|
248 | console.log(green(`Fun update Handler variables to ${tplPath}`));
|
249 |
|
250 | const envs = {
|
251 | 'FUN_HANDLER': originHandler
|
252 | };
|
253 |
|
254 | console.log(green(`Fun add environment variables 'FUN_HANDLER' to ${tplPath}`));
|
255 |
|
256 | if (originInitializer) {
|
257 | functionRes.Properties.Initializer = `${packageName}.Entrypoint::initialize`;
|
258 | console.log(green(`Fun update Initializer variables to ${tplPath}`));
|
259 |
|
260 | Object.assign(envs, {
|
261 | 'FUN_INITIALIZER': originInitializer
|
262 | });
|
263 |
|
264 | console.log(green(`Fun add environment variables 'FUN_INITIALIZER' to ${tplPath}`));
|
265 | }
|
266 |
|
267 | functionRes.Properties.EnvironmentVariables = mergeEnvs(functionRes, envs);
|
268 |
|
269 | util.outputTemplateFile(tplPath, updatedTplContent);
|
270 |
|
271 | return {
|
272 | packageName,
|
273 | codeUri: functionRes.Properties.CodeUri,
|
274 | updatedTplContent
|
275 | };
|
276 | }
|
277 |
|
278 | async function processJavaIfNecessary({
|
279 | runtime, baseDir,
|
280 | tplPath, tpl,
|
281 | serviceName,
|
282 | functionName
|
283 | }) {
|
284 |
|
285 | if (runtime !== 'java8' || usingProjectTemplate(tplPath)) { return tpl; }
|
286 |
|
287 | const { projectTpl, projectTplPath } = await getProjectTpl(tplPath);
|
288 |
|
289 | const { packageName, codeUri, updatedTplContent } = await updateInitializerAndEnvs({
|
290 | tplPath: projectTplPath,
|
291 | tpl: projectTpl,
|
292 | serviceName: serviceName,
|
293 | functionName: functionName
|
294 | });
|
295 |
|
296 | await copyEntryPoint(projectTplPath, codeUri, packageName);
|
297 |
|
298 |
|
299 | await buildOnMeta(baseDir, updatedTplContent, projectTplPath);
|
300 |
|
301 | return await getTpl(tplPath);
|
302 | }
|
303 |
|
304 | async function updateEnvironmentsInTpl({ tplPath, tpl, envs,
|
305 | serviceName,
|
306 | functionName
|
307 | }) {
|
308 | const updatedTplContent = _.cloneDeep(tpl);
|
309 |
|
310 | const { functionRes } = definition.findFunctionByServiceAndFunctionName(updatedTplContent.Resources, serviceName, functionName);
|
311 |
|
312 | const mergedEnvs = mergeEnvs(functionRes, envs);
|
313 |
|
314 | if (_.isEmpty(functionRes['Properties'])) {
|
315 | functionRes.Properties = {
|
316 | 'EnvironmentVariables': mergedEnvs
|
317 | };
|
318 | } else {
|
319 | functionRes.Properties.EnvironmentVariables = mergedEnvs;
|
320 | }
|
321 |
|
322 | util.outputTemplateFile(tplPath, updatedTplContent);
|
323 |
|
324 | console.log(green(`Fun add environment variables to '${serviceName}/${functionName}' with path ${tplPath}`));
|
325 |
|
326 | return updatedTplContent;
|
327 | }
|
328 |
|
329 | function generateBackupTplPath(tplPath) {
|
330 | const tplDir = path.dirname(tplPath);
|
331 | const tplName = path.basename(tplPath);
|
332 | const newTplName = `.${tplName}.backup`;
|
333 | return path.join(tplDir, newTplName);
|
334 | }
|
335 |
|
336 | async function getProjectTpl(tplPath) {
|
337 | const projectBaseDir = getBaseDir(tplPath);
|
338 | const projectTplPath = path.resolve(projectBaseDir, path.basename(tplPath));
|
339 | const projectTpl = await getTpl(projectTplPath);
|
340 | return {
|
341 | projectTpl,
|
342 | projectTplPath
|
343 | };
|
344 | }
|
345 |
|
346 | function updateNasAutoConfigureInTpl(tplPath, tpl, serviceName) {
|
347 | const updatedTplContent = _.cloneDeep(tpl);
|
348 |
|
349 | const { serviceRes } = definition.findServiceByServiceName(updatedTplContent.Resources, serviceName);
|
350 |
|
351 | if (_.isEmpty(serviceRes['Properties'])) {
|
352 | serviceRes.Properties = {
|
353 | 'NasConfig': 'Auto'
|
354 | };
|
355 | } else {
|
356 | serviceRes.Properties.NasConfig = 'Auto';
|
357 | }
|
358 |
|
359 | util.outputTemplateFile(tplPath, updatedTplContent);
|
360 |
|
361 | console.log(green(`Fun add 'NasConfig: Auto' configuration to ${tplPath}`));
|
362 |
|
363 | return updatedTplContent;
|
364 | }
|
365 |
|
366 | async function updateNasAutoConfigure(tplPath, tpl, serviceName) {
|
367 | const { projectTpl, projectTplPath } = await getTplInfo(tpl, tplPath);
|
368 |
|
369 | const updatedTplContent = await updateNasAutoConfigureInTpl(projectTplPath, projectTpl, serviceName);
|
370 | return updatedTplContent;
|
371 | }
|
372 |
|
373 | function updateNasAndVpcInTpl(tplPath, tpl, serviceName, nasAndVpcConfig) {
|
374 | const updatedTplContent = _.cloneDeep(tpl);
|
375 |
|
376 | const { serviceRes } = definition.findServiceByServiceName(updatedTplContent.Resources, serviceName);
|
377 |
|
378 | if (_.isEmpty(serviceRes['Properties'])) {
|
379 | serviceRes.Properties = nasAndVpcConfig;
|
380 | } else {
|
381 | serviceRes.Properties.VpcConfig = nasAndVpcConfig.VpcConfig;
|
382 | serviceRes.Properties.NasConfig = nasAndVpcConfig.NasConfig;
|
383 | }
|
384 |
|
385 | console.log(green(`Fun add 'NasConfig' and 'VpcConfig' configuration to your template.yml.`));
|
386 |
|
387 | util.outputTemplateFile(tplPath, updatedTplContent);
|
388 | return updatedTplContent;
|
389 | }
|
390 |
|
391 | async function getTplInfo(tpl, tplPath) {
|
392 | let projectTpl;
|
393 | let projectTplPath;
|
394 |
|
395 | if (usingProjectTemplate(tplPath)) {
|
396 |
|
397 | projectTpl = tpl;
|
398 | projectTplPath = tplPath;
|
399 | } else {
|
400 | const obj = await getProjectTpl(tplPath);
|
401 | projectTpl = obj.projectTpl;
|
402 | projectTplPath = obj.projectTplPath;
|
403 | }
|
404 |
|
405 | return {
|
406 | projectTpl,
|
407 | projectTplPath
|
408 | };
|
409 | }
|
410 |
|
411 | async function updateNasAndVpc(tplPath, tpl, serviceName, nasAndVpcConfig) {
|
412 | const { projectTpl, projectTplPath } = await getTplInfo(tpl, tplPath);
|
413 |
|
414 | const updatedTplContent = updateNasAndVpcInTpl(projectTplPath, projectTpl, serviceName, nasAndVpcConfig);
|
415 |
|
416 | return updatedTplContent;
|
417 | }
|
418 |
|
419 | async function generateNasMappingsAndEnvs({
|
420 | baseDir,
|
421 | serviceName,
|
422 | functionName,
|
423 | runtime,
|
424 | codeUri,
|
425 | nasConfig
|
426 | }) {
|
427 | const envs = {};
|
428 |
|
429 | const nasMappings = {};
|
430 | const nasMapping = [];
|
431 |
|
432 | const prefix = parseMountDirPrefix(nasConfig);
|
433 |
|
434 | const nasMappingPath = path.resolve(baseDir, '.nas.yml');
|
435 | const localSystemDependency = path.resolve(codeUri, SYSTEM_DEPENDENCY_PATH);
|
436 |
|
437 | if (await fs.pathExists(localSystemDependency)) {
|
438 | const remoteNasDir = `${prefix}root`;
|
439 |
|
440 | nasMapping.push({
|
441 | localNasDir: path.relative(baseDir, localSystemDependency),
|
442 | remoteNasDir
|
443 | });
|
444 |
|
445 | nasMappings[serviceName] = nasMapping;
|
446 |
|
447 | Object.assign(envs, generateSystemNasEnvs(remoteNasDir));
|
448 |
|
449 | outputNasMappingLog(baseDir, nasMappingPath, localSystemDependency);
|
450 | }
|
451 |
|
452 | const dependencyMappings = runtimeDependencyMappings[runtime];
|
453 |
|
454 | for (const mapping of dependencyMappings) {
|
455 |
|
456 | const localDir = resolveLocalNasDir(runtime, baseDir, codeUri, mapping.localDir, serviceName, functionName);
|
457 |
|
458 | if (await fs.pathExists(localDir)) {
|
459 |
|
460 | const remoteDir = `${prefix}${mapping.remoteDir}`;
|
461 |
|
462 | nasMapping.push({
|
463 | localNasDir: localDir,
|
464 | remoteNasDir: remoteDir
|
465 | });
|
466 |
|
467 | Object.assign(envs, {
|
468 | [mapping.env]: generateNasEnv(mapping.defaultEnv, remoteDir)
|
469 | });
|
470 |
|
471 | outputNasMappingLog(baseDir, nasMappingPath, localDir);
|
472 | }
|
473 | }
|
474 |
|
475 | nasMappings[serviceName] = nasMapping;
|
476 |
|
477 | return {
|
478 | envs,
|
479 | nasMappings,
|
480 | remoteNasDirPrefix: prefix
|
481 | };
|
482 | }
|
483 |
|
484 | function generateNasEnv(defaultEnv, remoteNasDir) {
|
485 | let nasEnv;
|
486 |
|
487 | if (defaultEnv) {
|
488 | nasEnv = `${remoteNasDir}:${defaultEnv}`;
|
489 | } else {
|
490 | nasEnv = remoteNasDir;
|
491 | }
|
492 | return nasEnv;
|
493 | }
|
494 |
|
495 | function resolveLocalNasDir(runtime, baseDir, codeUri, localDirInNasMappings, serviceName, functionName) {
|
496 | let localDir;
|
497 | if (runtime === 'java8') {
|
498 | localDir = path.relative(baseDir, path.join(localDirInNasMappings, serviceName, functionName, 'lib'));
|
499 | } else {
|
500 | localDir = path.relative(baseDir, path.join(codeUri, localDirInNasMappings));
|
501 | }
|
502 | return localDir;
|
503 | }
|
504 |
|
505 | function parseMountDirPrefix(nasConfig) {
|
506 | if (definition.isNasAutoConfig(nasConfig)) {
|
507 | return '/mnt/auto/';
|
508 | }
|
509 | const mountPoints = nasConfig.MountPoints;
|
510 | ensureOnlyOneMountPoinExists(mountPoints);
|
511 |
|
512 | const mountPoint = _.head(mountPoints).MountDir;
|
513 | if (_.endsWith(mountPoint, '/')) {
|
514 | return mountPoint;
|
515 | }
|
516 | return mountPoint + '/';
|
517 | }
|
518 |
|
519 |
|
520 | function outputNasMappingLog(baseDir, nasMappingPath, localNasDir) {
|
521 | console.log(green(`Fun add ${path.relative(baseDir, localNasDir)} to ${nasMappingPath}`));
|
522 | }
|
523 |
|
524 | function generateSystemNasEnvs(rootEnvPrefix) {
|
525 | return {
|
526 | 'LD_LIBRARY_PATH': `${generateDefaultLibPath(rootEnvPrefix)}`
|
527 | };
|
528 | }
|
529 |
|
530 | async function nasCpFromlocalNasDirToRemoteNasDir(tpl, tplPath, baseDir, nasServiceName, nasMappings) {
|
531 | const localNasTmpDir = path.join(baseDir, '.fun', 'tmp', 'nas', 'cp');
|
532 |
|
533 | for (const { localNasDir, remoteNasDir } of nasMappings) {
|
534 | const srcPath = path.resolve(baseDir, localNasDir);
|
535 | const dstPath = `nas://${nasServiceName}${remoteNasDir}/`;
|
536 |
|
537 | console.log(yellow(`\nstarting upload ${srcPath} to ${dstPath}`));
|
538 |
|
539 | await nasCp(srcPath, dstPath, true, false, localNasTmpDir, tpl, tplPath, baseDir, false, true);
|
540 | }
|
541 | }
|
542 |
|
543 | async function processOtherFunctionsUnderServiceIfNecessary({
|
544 | baseDir, codeUri, runtime, envs, tpl, tplPath,
|
545 | originServiceName, originFunctionName
|
546 | }) {
|
547 |
|
548 | let tplChanged = false;
|
549 |
|
550 | const otherFunctions = definition.findFunctionsInTpl(tpl, (functionName, functionRes) => {
|
551 | return originFunctionName !== functionName;
|
552 | });
|
553 |
|
554 | if (_.isEmpty(otherFunctions)) { return { updatedEnvsTpl: tpl, tplChanged }; }
|
555 |
|
556 | const pendingFuntions = otherFunctions.filter(m => {
|
557 | const functionProp = m.functionRes.Properties;
|
558 |
|
559 | const otherCodeUri = (functionProp || {}).CodeUri;
|
560 | const otherAbsCodeUri = path.resolve(baseDir, otherCodeUri);
|
561 | const otherRuntime = (functionProp || {}).Runtime;
|
562 |
|
563 | return (_.isEqual(runtimeDependencyMappings[runtime], runtimeDependencyMappings[otherRuntime]) && codeUri === otherAbsCodeUri);
|
564 | });
|
565 |
|
566 | if (_.isEmpty(pendingFuntions)) { return { updatedEnvsTpl: tpl, tplChanged }; }
|
567 |
|
568 | for (const pendingFuntion of pendingFuntions) {
|
569 |
|
570 | tpl = await updateEnvironmentsInTpl({ tplPath, tpl, envs,
|
571 | serviceName: originServiceName,
|
572 | functionName: pendingFuntion.functionName
|
573 | });
|
574 | }
|
575 |
|
576 | return {
|
577 | updatedEnvsTpl: tpl,
|
578 | tplChanged: true
|
579 | };
|
580 | }
|
581 |
|
582 | async function processNasMappingsAndEnvs({ tpl, tplPath, runtime, codeUri, baseDir,
|
583 | serviceName,
|
584 | functionName,
|
585 | convertedNasConfig
|
586 | }) {
|
587 |
|
588 | const { serviceRes } = definition.findFunctionByServiceAndFunctionName(tpl.Resources, serviceName, functionName);
|
589 |
|
590 | const { envs, nasMappings, remoteNasDirPrefix } = await generateNasMappingsAndEnvs({
|
591 | baseDir,
|
592 | serviceName: serviceName,
|
593 | functionName: functionName,
|
594 | runtime,
|
595 | codeUri,
|
596 | nasConfig: convertedNasConfig || (serviceRes.Properties || {}).NasConfig
|
597 | });
|
598 |
|
599 | const localDirs = _.map(runtimeDependencyMappings[runtime], mapping => path.join(codeUri, mapping.localDir));
|
600 |
|
601 | if (_.isEmpty(nasMappings)) {
|
602 | throw new Error(red(`\nFun detects that your dependencies are not included in path ${localDirs} or ${path.resolve(codeUri, SYSTEM_DEPENDENCY_PATH)}`));
|
603 | }
|
604 |
|
605 | const nasMappingsObj = await saveNasMappings(getNasYmlPath(tplPath), nasMappings);
|
606 |
|
607 | const { updatedEnvsTpl, tplChanged} = await updateEnvironments({
|
608 | tplPath, tpl, envs, baseDir, codeUri, runtime,
|
609 | serviceName,
|
610 | functionName
|
611 | });
|
612 |
|
613 | return {
|
614 | tplChanged,
|
615 | remoteNasDirPrefix,
|
616 | updatedTpl: updatedEnvsTpl,
|
617 | serviceNasMappings: nasMappingsObj
|
618 | };
|
619 | }
|
620 |
|
621 | async function processNasAutoConfiguration({ tpl, tplPath, runtime, codeUri, convertedNasConfig, stage,
|
622 | serviceName,
|
623 | functionName
|
624 | }) {
|
625 |
|
626 | const baseDir = getBaseDir(tplPath);
|
627 |
|
628 | const rs = await processNasMappingsAndEnvs({ tpl, tplPath, runtime, codeUri, baseDir,
|
629 | serviceName,
|
630 | functionName,
|
631 | convertedNasConfig
|
632 | });
|
633 |
|
634 | if (stage === 'package') { return rs.tplChanged; }
|
635 |
|
636 | const updatedTplContent = await processJavaIfNecessary({ runtime, baseDir, tplPath,
|
637 | tpl: rs.updatedTpl,
|
638 | serviceName: serviceName,
|
639 | functionName: functionName
|
640 | });
|
641 |
|
642 | const serviceNasMappings = await processPythonModelIfNecessary({ codeUri, runtime, baseDir,
|
643 | serviceName: serviceName,
|
644 | nasYmlPath: getNasYmlPath(tplPath),
|
645 | remoteNasDirPrefix: rs.remoteNasDirPrefix,
|
646 | serviceNasMappings: rs.serviceNasMappings
|
647 | });
|
648 |
|
649 |
|
650 | await nasCpFromlocalNasDirToRemoteNasDir(tpl, tplPath, baseDir, serviceName, serviceNasMappings[serviceName]);
|
651 |
|
652 | console.log(yellow(`\nFun has automatically uploaded your code dependency to NAS, then fun will use 'fun deploy ${serviceName}/${functionName}' to redeploy.`));
|
653 |
|
654 | console.log(`Waiting for service ${serviceName} to be deployed...`);
|
655 |
|
656 | const partialDeploy = await require('./deploy/deploy-by-tpl').partialDeployment(`${serviceName}/${functionName}`, updatedTplContent);
|
657 |
|
658 | if (partialDeploy.resourceName) {
|
659 |
|
660 | await require('./deploy/deploy-by-tpl').deployService(path.dirname(tplPath), partialDeploy.resourceName, partialDeploy.resourceRes, false, tplPath, true);
|
661 | }
|
662 |
|
663 | return rs.tplChanged;
|
664 | }
|
665 |
|
666 | async function updateEnvironments({
|
667 | tplPath, tpl, envs, baseDir, codeUri, runtime,
|
668 | serviceName, functionName
|
669 | }) {
|
670 |
|
671 | const { projectTpl, projectTplPath } = await getTplInfo(tpl, tplPath);
|
672 |
|
673 | const updatedTplContent = await updateEnvironmentsInTpl({
|
674 | envs,
|
675 | tpl: projectTpl,
|
676 | tplPath: projectTplPath,
|
677 | serviceName, functionName
|
678 | });
|
679 |
|
680 | return await processOtherFunctionsUnderServiceIfNecessary({
|
681 | tpl: updatedTplContent, tplPath,
|
682 | baseDir, codeUri, runtime, envs,
|
683 | originServiceName: serviceName,
|
684 | originFunctionName: functionName
|
685 | });
|
686 | }
|
687 |
|
688 | async function processPythonModelIfNecessary({ nasYmlPath, codeUri, runtime, baseDir,
|
689 | serviceName,
|
690 | remoteNasDirPrefix,
|
691 | serviceNasMappings
|
692 | }) {
|
693 |
|
694 | if (!_.includes(['python2.7', 'python3'], runtime)) { return serviceNasMappings; }
|
695 |
|
696 | const absModelPath = path.resolve(codeUri, 'model');
|
697 |
|
698 | if (!await fs.pathExists(absModelPath)) { return serviceNasMappings; }
|
699 |
|
700 | const nasMappings = await extractNasMappingsFromNasYml(baseDir, serviceName);
|
701 |
|
702 | const modelMapping = nasMappings.find(arr => {
|
703 | return path.resolve(baseDir, arr.localNasDir) === absModelPath;
|
704 | });
|
705 |
|
706 | if (!_.isEmpty(modelMapping)) { return serviceNasMappings; }
|
707 |
|
708 | const remoteNasDir = `${remoteNasDirPrefix}model`;
|
709 |
|
710 | console.log(`
|
711 | Fun has detected that there is a model folder. It is recommend to synchronize your model folder to NAS.
|
712 | You can add the following configuration to ` + yellow(`'nasMapping.${serviceName}'`) + ` in ` + yellow(`${nasYmlPath}
|
713 | `)
|
714 |
|
715 | + yellow(`
|
716 | - localNasDir: ${absModelPath}
|
717 | remoteNasDir: ${remoteNasDir}
|
718 | `)
|
719 | + `
|
720 | After adding, fun is going to automatically synchronize the ` + yellow(`local`) + ` directory ${absModelPath} to ` + yellow(`remote`) + ` ${remoteNasDir}.
|
721 | If these files ` + yellow('under') + ` model directory are used on your function code, you need to ${remoteNasDir} update these files path manully.
|
722 | `);
|
723 |
|
724 | await promptForInputContinue('Please input enter to continue.');
|
725 |
|
726 | return await getNasMappingsFromNasYml(nasYmlPath);
|
727 | }
|
728 |
|
729 | async function backupTemplateFile(tplPath) {
|
730 | const baseDir = getBaseDir(tplPath);
|
731 | const originTplPath = path.resolve(baseDir, path.basename(tplPath));
|
732 | const newPath = generateBackupTplPath(originTplPath);
|
733 | await fs.copy(originTplPath, newPath);
|
734 | console.log(green(`\nFun automatically backups the original ${path.basename(tplPath)} file to ${newPath}`));
|
735 | }
|
736 |
|
737 | function ensureMountTargetsExist(mountTargets) {
|
738 | if (_.isEmpty(mountTargets)) {
|
739 | 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.'));
|
740 | }
|
741 | }
|
742 |
|
743 | function ensureSecurityGroupsExist(securityGroups) {
|
744 | if (_.isEmpty(securityGroups)) {
|
745 | 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.`));
|
746 | }
|
747 | }
|
748 |
|
749 | function ensureNasFileSystemsExist(nasFileSystems) {
|
750 | if (_.isEmpty(nasFileSystems)) {
|
751 | 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.`));
|
752 | }
|
753 | }
|
754 |
|
755 | function ensureOnlyOneMountPoinExists(mountPoints) {
|
756 | if (mountPoints.length > 1) {
|
757 | throw new Error(red(`More than one 'NasConfig' configuration in template.yml.`));
|
758 | }
|
759 | }
|
760 |
|
761 | async function getSecurityGroups(vpcId) {
|
762 | const ecsClient = await getEcsPopClient();
|
763 | const profile = await getProfile();
|
764 | return await securityGroup.describeSecurityGroups(ecsClient, profile.defaultRegion, vpcId, undefined);
|
765 | }
|
766 |
|
767 | async function processNasSelection() {
|
768 | const nasClient = await getNasPopClient();
|
769 | const nasFileSystems = await nas.getAvailableNasFileSystems(nasClient);
|
770 | ensureNasFileSystemsExist(nasFileSystems);
|
771 |
|
772 | const nasAnswer = await promptForFileSystems(nasFileSystems);
|
773 | const nasSelected = nasFileSystems.filter(f => f.fileSystemId === nasAnswer.fileSystemId);
|
774 | const mountTargets = _.head(nasSelected).mountTargets;
|
775 | ensureMountTargetsExist(mountTargets);
|
776 |
|
777 | const mountTargetAnswer = await promptForMountTargets(mountTargets);
|
778 | const mountTargetSelected = mountTargets.filter(f => f.MountTargetDomain === mountTargetAnswer.mountTargetDomain);
|
779 | const mountTarget = _.head(mountTargetSelected);
|
780 |
|
781 | const securityGroups = await getSecurityGroups(mountTarget.VpcId);
|
782 | ensureSecurityGroupsExist(securityGroups);
|
783 |
|
784 | const securityGroupAnswer = await promptForSecurityGroup(securityGroups);
|
785 | const securityGroupId = securityGroupAnswer.securityGroupId;
|
786 |
|
787 | return {
|
788 | mountTarget,
|
789 | securityGroupId
|
790 | };
|
791 | }
|
792 |
|
793 | function replaceNasConfig(nasConfig, mountDir) {
|
794 | const cloneNasConfig = _.cloneDeep(nasConfig);
|
795 | cloneNasConfig.MountPoints = cloneNasConfig.MountPoints.filter(f => f.MountDir === mountDir);
|
796 | return cloneNasConfig;
|
797 | }
|
798 |
|
799 | async function ensureCodeUriForJava(codeUri, serviceName, functionName) {
|
800 |
|
801 | if (codeUri.endsWith('.zip') || codeUri.endsWith('.jar') || codeUri.endsWith('.war')) {
|
802 | throw new Error(`
|
803 | You can follow these steps:
|
804 | 1. Modify ${serviceName}/${functionName}'s 'CodeUri' property to the directory where 'pom.xml' is located.
|
805 | 2. Execute 'fun build' to build your functions.
|
806 | 3. Execute 'fun deploy' to deploy resources.`);
|
807 | }
|
808 | }
|
809 |
|
810 | async function nasAutoConfigurationIfNecessary({ stage, tplPath, runtime, codeUri, nasConfig, vpcConfig,
|
811 | compressedSize,
|
812 | nasFunctionName,
|
813 | nasServiceName
|
814 | }) {
|
815 |
|
816 | let stop = false;
|
817 | let tplChanged = false;
|
818 | if (compressedSize > 52428800 && _.includes(SUPPORT_RUNTIMES, runtime)) {
|
819 |
|
820 | 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.`));
|
821 | await ensureCodeUriForJava(codeUri, nasServiceName, nasFunctionName);
|
822 |
|
823 | if (await promptForConfirmContinue(`Do you want to let fun to help you automate the configuration?`)) {
|
824 |
|
825 | const packageStage = (stage === 'package');
|
826 | const tpl = await getTpl(tplPath);
|
827 |
|
828 | if (definition.isNasAutoConfig(nasConfig)) {
|
829 | const yes = await promptForConfirmContinue(`You have already configured 'NasConfig: Auto’. We want to use this configuration to store your function dependencies.`);
|
830 | if (yes) {
|
831 |
|
832 | if (packageStage && !_.isEmpty(vpcConfig)) {
|
833 | throw new Error(`When 'NasConfig: Auto' is specified, 'VpcConfig' is not supported.`);
|
834 | }
|
835 | await backupTemplateFile(tplPath);
|
836 |
|
837 | tplChanged = await processNasAutoConfiguration({ tpl, tplPath, runtime, codeUri, stage,
|
838 | serviceName: nasServiceName,
|
839 | functionName: nasFunctionName
|
840 | });
|
841 |
|
842 | stop = true;
|
843 | } else {
|
844 | throw new Error(red(`\nIf 'NasConfig: Auto' is configured, only the configuration store function dependency is currently supported.`));
|
845 | }
|
846 | } else if (!_.isEmpty(vpcConfig) && _.isEmpty(nasConfig)) {
|
847 |
|
848 | 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/`));
|
849 | } else if (!_.isEmpty(vpcConfig) && !_.isEmpty(nasConfig)) {
|
850 |
|
851 | if (packageStage) {
|
852 | throw new Error(`When 'NasConfig' is specified, 'VpcConfig' is not supported.`);
|
853 | }
|
854 | if (definition.onlyOneNASExists(nasConfig)) {
|
855 | const yes = await promptForConfirmContinue(`We have detected that you already have a NAS configuration. Do you directly use this NAS storage function dependencies.`);
|
856 | if (yes) {
|
857 | await backupTemplateFile(tplPath);
|
858 |
|
859 | tplChanged = await processNasAutoConfiguration({ tpl, tplPath, runtime, codeUri, stage,
|
860 | serviceName: nasServiceName,
|
861 | functionName: nasFunctionName
|
862 | });
|
863 | } else {
|
864 | 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.`));
|
865 | }
|
866 | } else {
|
867 | const answer = await promptForMountPoints(nasConfig.MountPoints);
|
868 | const convertedNasConfig = replaceNasConfig(nasConfig, answer.mountDir);
|
869 | await backupTemplateFile(tplPath);
|
870 |
|
871 | tplChanged = await processNasAutoConfiguration({ tpl, tplPath, runtime, codeUri, stage,
|
872 | convertedNasConfig,
|
873 | serviceName: nasServiceName,
|
874 | functionName: nasFunctionName
|
875 | });
|
876 | }
|
877 |
|
878 | stop = true;
|
879 | } else if (_.isEmpty(vpcConfig) && _.isEmpty(nasConfig)) {
|
880 | const yes = await promptForConfirmContinue(`We recommend using the 'NasConfig: Auto' configuration to manage your function dependencies.`);
|
881 | if (yes) {
|
882 | await backupTemplateFile(tplPath);
|
883 |
|
884 | const updatedTpl = await updateNasAutoConfigure(tplPath, tpl, nasServiceName);
|
885 |
|
886 | tplChanged = await processNasAutoConfiguration({ tpl: updatedTpl, tplPath, runtime, codeUri, stage,
|
887 | serviceName: nasServiceName,
|
888 | functionName: nasFunctionName
|
889 | });
|
890 | } else {
|
891 |
|
892 | const { mountTarget, securityGroupId } = await processNasSelection();
|
893 |
|
894 | await backupTemplateFile(tplPath);
|
895 |
|
896 | const nasAndVpcConfig = generateNasAndVpcConfig(mountTarget, securityGroupId, nasServiceName);
|
897 | const updatedTpl = await updateNasAndVpc(tplPath, tpl, nasServiceName, nasAndVpcConfig);
|
898 |
|
899 | tplChanged = await processNasAutoConfiguration({ tpl: updatedTpl, tplPath, runtime, codeUri, stage,
|
900 | serviceName: nasServiceName,
|
901 | functionName: nasFunctionName
|
902 | });
|
903 | }
|
904 | stop = true;
|
905 | }
|
906 | }
|
907 | }
|
908 | return {
|
909 | stop,
|
910 | tplChanged
|
911 | };
|
912 | }
|
913 |
|
914 | function usingProjectTemplate(tplPath) {
|
915 | const baseDir = getBaseDir(tplPath);
|
916 | return path.dirname(tplPath) === path.resolve(baseDir);
|
917 | }
|
918 |
|
919 | function generateNasAndVpcConfig(mountTarget, securityGroupId, serviceName) {
|
920 | const nasConfig = {
|
921 | 'UserId': 10003,
|
922 | 'GroupId': 10003,
|
923 | 'MountPoints': [
|
924 | {
|
925 | 'ServerAddr': `${mountTarget.MountTargetDomain}:/${serviceName}`,
|
926 | 'MountDir': '/mnt/nas'
|
927 | }
|
928 | ]
|
929 | };
|
930 |
|
931 | const vpcConfig = {
|
932 | 'VpcId': mountTarget.VpcId,
|
933 | 'VSwitchIds': [mountTarget.VswId],
|
934 | 'SecurityGroupId': securityGroupId
|
935 | };
|
936 |
|
937 | return {
|
938 | 'VpcConfig': vpcConfig,
|
939 | 'NasConfig': nasConfig
|
940 | };
|
941 | }
|
942 |
|
943 | async function makeFunction(baseDir, {
|
944 | serviceName,
|
945 | functionName,
|
946 | description = '',
|
947 | handler,
|
948 | initializer = '',
|
949 | timeout = 3,
|
950 | initializationTimeout = 3,
|
951 | memorySize = 128,
|
952 | runtime = 'nodejs6',
|
953 | codeUri,
|
954 | environmentVariables = {},
|
955 | instanceConcurrency,
|
956 | nasConfig,
|
957 | vpcConfig
|
958 | }, onlyConfig, tplPath) {
|
959 | const fc = await getFcClient();
|
960 |
|
961 | var fn;
|
962 | try {
|
963 | fn = await fc.getFunction(serviceName, functionName);
|
964 | } catch (ex) {
|
965 | if (ex.code !== 'FunctionNotFound') {
|
966 | throw ex;
|
967 | }
|
968 | }
|
969 |
|
970 | if (!fn && onlyConfig) {
|
971 | 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.`);
|
972 | }
|
973 |
|
974 | let code;
|
975 |
|
976 | if (!onlyConfig) {
|
977 |
|
978 | if (codeUri && codeUri.startsWith('oss://')) {
|
979 | code = extractOssCodeUri(codeUri);
|
980 | } else {
|
981 | console.log(`\t\tWaiting for packaging function ${functionName} code...`);
|
982 | const { base64, count, compressedSize } = await zipCode(baseDir, codeUri, runtime, functionName);
|
983 |
|
984 | const rs = await nasAutoConfigurationIfNecessary({
|
985 | compressedSize, tplPath, runtime, nasConfig, vpcConfig,
|
986 | nasFunctionName: functionName,
|
987 | nasServiceName: serviceName,
|
988 | codeUri: path.resolve(baseDir, codeUri)
|
989 | });
|
990 |
|
991 | if (rs.stop) { return { tplChanged: rs.tplChanged }; }
|
992 |
|
993 | const convertedSize = bytes(compressedSize, {
|
994 | unitSeparator: ' '
|
995 | });
|
996 |
|
997 | if (!count || !compressedSize) {
|
998 | console.log(green(`\t\tThe function ${functionName} has been packaged.`));
|
999 | } else {
|
1000 | 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}`)));
|
1001 | }
|
1002 |
|
1003 | code = {
|
1004 | zipFile: base64
|
1005 | };
|
1006 | }
|
1007 | }
|
1008 |
|
1009 | const confEnv = await resolveLibPathsFromLdConf(baseDir, codeUri);
|
1010 |
|
1011 | Object.assign(environmentVariables, confEnv);
|
1012 |
|
1013 | const params = {
|
1014 | description,
|
1015 | handler,
|
1016 | initializer,
|
1017 | timeout,
|
1018 | initializationTimeout,
|
1019 | memorySize,
|
1020 | runtime,
|
1021 | code,
|
1022 | environmentVariables: addEnv(environmentVariables, nasConfig),
|
1023 | instanceConcurrency
|
1024 | };
|
1025 |
|
1026 | for (let i in params.environmentVariables) {
|
1027 | if (!isNaN(params.environmentVariables[i])) {
|
1028 | debug(`the value in environmentVariables:${params.environmentVariables[i]} cast String Done`);
|
1029 | params.environmentVariables[i] = params.environmentVariables[i] + '';
|
1030 | }
|
1031 | }
|
1032 |
|
1033 | try {
|
1034 | if (!fn) {
|
1035 |
|
1036 | params['functionName'] = functionName;
|
1037 | await fc.createFunction(serviceName, params);
|
1038 | } else {
|
1039 |
|
1040 | await fc.updateFunction(serviceName, functionName, params);
|
1041 | }
|
1042 | } catch (ex) {
|
1043 |
|
1044 | if (ex.message.indexOf('timeout') !== -1) {
|
1045 | 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.`));
|
1046 | }
|
1047 | throw ex;
|
1048 | }
|
1049 | return {
|
1050 | tplChanged: false
|
1051 | };
|
1052 | }
|
1053 |
|
1054 | async function makeService({
|
1055 | serviceName,
|
1056 | role,
|
1057 | description,
|
1058 | internetAccess = true,
|
1059 | logConfig = {},
|
1060 | vpcConfig,
|
1061 | nasConfig
|
1062 | }) {
|
1063 | const fc = await getFcClient();
|
1064 |
|
1065 | var service;
|
1066 | await promiseRetry(async (retry, times) => {
|
1067 | try {
|
1068 | service = await fc.getService(serviceName);
|
1069 | } catch (ex) {
|
1070 |
|
1071 | if (ex.code === 'AccessDenied' || !ex.code || ex.code === 'ENOTFOUND') {
|
1072 |
|
1073 | if (ex.message.indexOf('FC service is not enabled for current user') !== -1) {
|
1074 |
|
1075 | 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`));
|
1076 |
|
1077 | } else {
|
1078 | 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`));
|
1079 | }
|
1080 |
|
1081 | throw ex;
|
1082 | } else if (ex.code !== 'ServiceNotFound') {
|
1083 | debug('error when getService, serviceName is %s, error is: \n%O', serviceName, ex);
|
1084 |
|
1085 | console.log(red(`\tretry ${times} times`));
|
1086 | retry(ex);
|
1087 | }
|
1088 | }
|
1089 | });
|
1090 |
|
1091 | const options = {
|
1092 | description,
|
1093 | role,
|
1094 | logConfig: {
|
1095 | project: logConfig.Project || '',
|
1096 | logstore: logConfig.Logstore || ''
|
1097 | }
|
1098 | };
|
1099 |
|
1100 | if (internetAccess !== null) {
|
1101 |
|
1102 | Object.assign(options, {
|
1103 | internetAccess
|
1104 | });
|
1105 | }
|
1106 |
|
1107 | const isNasAuto = definition.isNasAutoConfig(nasConfig);
|
1108 | const isVpcAuto = definition.isVpcAutoConfig(vpcConfig);
|
1109 |
|
1110 | if (!_.isEmpty(vpcConfig) || isNasAuto) {
|
1111 |
|
1112 | if (isVpcAuto || (_.isEmpty(vpcConfig) && isNasAuto)) {
|
1113 | console.log('\tusing \'VpcConfig: Auto\', Fun will try to generate related vpc resources automatically');
|
1114 | vpcConfig = await vpc.createDefaultVpcIfNotExist();
|
1115 | console.log(green('\tgenerated auto VpcConfig done: ', JSON.stringify(vpcConfig)));
|
1116 |
|
1117 | debug('generated vpcConfig: %j', vpcConfig);
|
1118 | }
|
1119 | }
|
1120 |
|
1121 | Object.assign(options, {
|
1122 | vpcConfig: vpcConfig || defaultVpcConfig
|
1123 | });
|
1124 |
|
1125 | if (isNasAuto) {
|
1126 | const vpcId = vpcConfig.vpcId || vpcConfig.VpcId;
|
1127 | const vswitchIds = vpcConfig.vswitchIds || vpcConfig.VSwitchIds;
|
1128 |
|
1129 | console.log('\tusing \'NasConfig: Auto\', Fun will try to generate related nas file system automatically');
|
1130 | nasConfig = await nas.generateAutoNasConfig(serviceName, vpcId, vswitchIds, nasConfig.UserId, nasConfig.GroupId);
|
1131 | console.log(green('\tgenerated auto NasConfig done: ', JSON.stringify(nasConfig)));
|
1132 | }
|
1133 |
|
1134 | Object.assign(options, {
|
1135 | nasConfig: nasConfig || defaultNasConfig
|
1136 | });
|
1137 |
|
1138 | await promiseRetry(async (retry, times) => {
|
1139 | try {
|
1140 | if (!service) {
|
1141 | debug('create service %s, options is %j', serviceName, options);
|
1142 | service = await fc.createService(serviceName, options);
|
1143 | } else {
|
1144 | debug('update service %s, options is %j', serviceName, options);
|
1145 | service = await fc.updateService(serviceName, options);
|
1146 | }
|
1147 | } catch (ex) {
|
1148 | if (ex.code === 'AccessDenied') {
|
1149 | throw ex;
|
1150 | }
|
1151 | debug('error when createService or updateService, serviceName is %s, options is %j, error is: \n%O', serviceName, options, ex);
|
1152 |
|
1153 | console.log(red(`\tretry ${times} times`));
|
1154 | retry(ex);
|
1155 | }
|
1156 | });
|
1157 |
|
1158 |
|
1159 | if (serviceName !== FUN_GENERATED_SERVICE
|
1160 | && !_.isEmpty(nasConfig)
|
1161 | && !_.isEmpty(nasConfig.MountPoints)) {
|
1162 |
|
1163 | await ensureNasDirExist({
|
1164 | role, vpcConfig, nasConfig
|
1165 | });
|
1166 | }
|
1167 |
|
1168 | return service;
|
1169 | }
|
1170 |
|
1171 | function mapMountPointDir(mountPoints, func) {
|
1172 | let resolvedMountPoints = _.map(mountPoints, (mountPoint) => {
|
1173 | const serverAddr = mountPoint.ServerAddr;
|
1174 |
|
1175 | const index = _.lastIndexOf(serverAddr, ':');
|
1176 | if (index >= 0) {
|
1177 | const mountPointDomain = serverAddr.substring(0, index);
|
1178 | const remoteDir = serverAddr.substring(index + 1);
|
1179 | const mountDir = mountPoint.MountDir;
|
1180 |
|
1181 | debug('remoteDir is: %s', remoteDir);
|
1182 |
|
1183 | return func(mountPointDomain, remoteDir, mountDir);
|
1184 | }
|
1185 | });
|
1186 |
|
1187 | resolvedMountPoints = _.compact(resolvedMountPoints);
|
1188 |
|
1189 | return resolvedMountPoints;
|
1190 | }
|
1191 |
|
1192 | const EXTREME_PATH_PREFIX = '/share';
|
1193 |
|
1194 | function checkMountPointDomainIsExtremeNas(mountPointDomain, remoteDir) {
|
1195 | const isExtremeNAS = mountPointDomain.indexOf('.extreme.nas.aliyuncs.com') !== -1;
|
1196 |
|
1197 | if (isExtremeNAS && (remoteDir !== EXTREME_PATH_PREFIX && !remoteDir.startsWith(EXTREME_PATH_PREFIX + '/'))) {
|
1198 | throw new Error('Extreme nas mount point must start with /share. Please refer to https://nas.console.aliyun.com/#/extreme for more help.');
|
1199 | }
|
1200 |
|
1201 | return isExtremeNAS;
|
1202 | }
|
1203 |
|
1204 | async function ensureNasDirExist({
|
1205 | role,
|
1206 | vpcConfig,
|
1207 | nasConfig
|
1208 | }) {
|
1209 | const mountPoints = nasConfig.MountPoints;
|
1210 | const modifiedNasConfig = _.cloneDeep(nasConfig);
|
1211 |
|
1212 | modifiedNasConfig.MountPoints = mapMountPointDir(mountPoints, (mountPointDomain, remoteDir, mountDir) => {
|
1213 |
|
1214 | if (checkMountPointDomainIsExtremeNas(mountPointDomain, remoteDir)) {
|
1215 |
|
1216 | return {
|
1217 | ServerAddr: `${mountPointDomain}:${EXTREME_PATH_PREFIX}`,
|
1218 | MountDir: `${mountDir}`
|
1219 | };
|
1220 | } else if (remoteDir !== '/') {
|
1221 | return {
|
1222 | ServerAddr: `${mountPointDomain}:/`,
|
1223 | MountDir: `${mountDir}`
|
1224 | };
|
1225 | } return null;
|
1226 | });
|
1227 |
|
1228 | const nasMountDirs = mapMountPointDir(mountPoints, (mountPointDomain, remoteDir, mountDir) => {
|
1229 | if (checkMountPointDomainIsExtremeNas(mountPointDomain, remoteDir)) {
|
1230 | if (remoteDir !== EXTREME_PATH_PREFIX) {
|
1231 | return { mountDir, remoteDir, isExtreme: true };
|
1232 | }
|
1233 | } else if (remoteDir !== '/') {
|
1234 | return { mountDir, remoteDir, isExtreme: false };
|
1235 | }
|
1236 | return null;
|
1237 | });
|
1238 |
|
1239 | debug('dirs need to check: %s', nasMountDirs);
|
1240 |
|
1241 | if (!_.isEmpty(nasMountDirs)) {
|
1242 | let nasRemoteDirs = [];
|
1243 | let nasDirsNeedToCheck = [];
|
1244 | for (let nasMountDir of nasMountDirs) {
|
1245 | nasRemoteDirs.push(nasMountDir.remoteDir);
|
1246 | if (nasMountDir.isExtreme) {
|
1247 |
|
1248 | nasDirsNeedToCheck.push(path.posix.join(nasMountDir.mountDir, nasMountDir.remoteDir.substring(EXTREME_PATH_PREFIX.length)));
|
1249 | } else {
|
1250 | nasDirsNeedToCheck.push(path.posix.join(nasMountDir.mountDir, nasMountDir.remoteDir));
|
1251 | }
|
1252 | }
|
1253 |
|
1254 | console.log(`\tChecking if nas directories ${nasRemoteDirs} exists, if not, it will be created automatically`);
|
1255 |
|
1256 | const utilFunctionName = await makeFcUtilsFunctionNasDirChecker(role, vpcConfig, modifiedNasConfig);
|
1257 | await sleep(1000);
|
1258 | await invokeFcUtilsFunction({
|
1259 | functionName: utilFunctionName,
|
1260 | event: JSON.stringify(nasDirsNeedToCheck)
|
1261 | });
|
1262 |
|
1263 | console.log(green('\tChecking nas directories done', JSON.stringify(nasRemoteDirs)));
|
1264 | }
|
1265 | }
|
1266 |
|
1267 | async function makeFcUtilsService(role, vpcConfig, nasConfig) {
|
1268 | return await makeService({
|
1269 | serviceName: FUN_GENERATED_SERVICE,
|
1270 | role,
|
1271 | description: 'generated by Funcraft',
|
1272 | vpcConfig,
|
1273 | nasConfig
|
1274 | });
|
1275 | }
|
1276 |
|
1277 | async function makeFcUtilsFunction({
|
1278 | serviceName,
|
1279 | functionName,
|
1280 | codes,
|
1281 | description = '',
|
1282 | handler,
|
1283 | timeout = 60,
|
1284 | memorySize = 128,
|
1285 | runtime = 'nodejs8'
|
1286 | }) {
|
1287 | const fc = await getFcClient();
|
1288 |
|
1289 | var fn;
|
1290 | try {
|
1291 | fn = await fc.getFunction(serviceName, functionName);
|
1292 | } catch (ex) {
|
1293 | if (ex.code !== 'FunctionNotFound') {
|
1294 | throw ex;
|
1295 | }
|
1296 | }
|
1297 |
|
1298 | const base64 = await zip.packFromJson(codes);
|
1299 |
|
1300 | let code = {
|
1301 | zipFile: base64
|
1302 | };
|
1303 |
|
1304 | const params = {
|
1305 | description,
|
1306 | handler,
|
1307 | initializer: '',
|
1308 | timeout,
|
1309 | memorySize,
|
1310 | runtime,
|
1311 | code
|
1312 | };
|
1313 |
|
1314 | if (!fn) {
|
1315 |
|
1316 | params['functionName'] = functionName;
|
1317 | fn = await fc.createFunction(serviceName, params);
|
1318 | } else {
|
1319 |
|
1320 | fn = await fc.updateFunction(serviceName, functionName, params);
|
1321 | }
|
1322 |
|
1323 | return fn;
|
1324 | }
|
1325 |
|
1326 | async function invokeFcUtilsFunction({
|
1327 | functionName,
|
1328 | event
|
1329 | }) {
|
1330 | const fc = await getFcClient();
|
1331 | const rs = await fc.invokeFunction(FUN_GENERATED_SERVICE, functionName, event, {
|
1332 | 'X-Fc-Log-Type': 'Tail'
|
1333 | });
|
1334 |
|
1335 | if (rs.data !== 'OK') {
|
1336 | const log = rs.headers['x-fc-log-result'];
|
1337 |
|
1338 | if (log) {
|
1339 | const decodedLog = Buffer.from(log, 'base64');
|
1340 | if ((decodedLog.toString().toLowerCase()).includes('permission denied')) {
|
1341 | 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 \
|
1342 | permission, more information please refer to https://github.com/alibaba/funcraft/blob/master/docs/usage/faq-zh.md')}`);
|
1343 | }
|
1344 | throw new Error(`fc utils function ${functionName} invoke error, error message is: ${decodedLog}`);
|
1345 | }
|
1346 | }
|
1347 | }
|
1348 |
|
1349 | async function getFcUtilsFunctionCode(filename) {
|
1350 | return await fs.readFile(path.join(__dirname, 'utils', filename));
|
1351 | }
|
1352 |
|
1353 | async function makeFcUtilsFunctionNasDirChecker(role, vpcConfig, nasConfig) {
|
1354 | await makeFcUtilsService(role, vpcConfig, nasConfig);
|
1355 |
|
1356 | const functionName = 'nas_dir_checker';
|
1357 |
|
1358 | const functionCode = await getFcUtilsFunctionCode('nas-dir-check.js');
|
1359 |
|
1360 | const codes = {
|
1361 | 'index.js': functionCode
|
1362 | };
|
1363 |
|
1364 | await makeFcUtilsFunction({
|
1365 | serviceName: FUN_GENERATED_SERVICE,
|
1366 | functionName: 'nas_dir_checker',
|
1367 | codes,
|
1368 | description: 'used for fun to ensure nas remote dir exist',
|
1369 | handler: 'index.handler'
|
1370 | });
|
1371 |
|
1372 | return functionName;
|
1373 | }
|
1374 |
|
1375 |
|
1376 | async function invokeFunction({
|
1377 | serviceName,
|
1378 | functionName,
|
1379 | event,
|
1380 | invocationType
|
1381 | }) {
|
1382 |
|
1383 | var rs;
|
1384 | const fc = await getFcClient();
|
1385 |
|
1386 | if (invocationType === 'Sync') {
|
1387 |
|
1388 | rs = await fc.invokeFunction(serviceName, functionName, event, {
|
1389 | 'X-Fc-Log-Type': 'Tail',
|
1390 | 'X-Fc-Invocation-Type': invocationType
|
1391 | });
|
1392 |
|
1393 | const log = rs.headers['x-fc-log-result'];
|
1394 |
|
1395 | if (log) {
|
1396 |
|
1397 | console.log(yellow('========= FC invoke Logs begin ========='));
|
1398 | const decodedLog = Buffer.from(log, 'base64');
|
1399 | console.log(decodedLog.toString());
|
1400 | console.log(yellow('========= FC invoke Logs end ========='));
|
1401 |
|
1402 | console.log(green('\nFC Invoke Result:'));
|
1403 | console.log(rs.data);
|
1404 | }
|
1405 | } else {
|
1406 |
|
1407 | rs = await fc.invokeFunction(serviceName, functionName, event, {
|
1408 | 'X-Fc-Invocation-Type': invocationType
|
1409 | });
|
1410 |
|
1411 | console.log(green('✔ ') + `${serviceName}/${functionName} async invoke success.`);
|
1412 | }
|
1413 |
|
1414 | return rs;
|
1415 | }
|
1416 |
|
1417 | module.exports = {
|
1418 | zipCode, makeService, makeFunction, invokeFunction,
|
1419 | detectLibrary, generateFunIngore, parseMountDirPrefix,
|
1420 | FUN_GENERATED_SERVICE, invokeFcUtilsFunction, getFcUtilsFunctionCode,
|
1421 | processNasMappingsAndEnvs, processNasAutoConfiguration, nasAutoConfigurationIfNecessary,
|
1422 | makeFcUtilsFunctionNasDirChecker
|
1423 | }; |
\ | No newline at end of file |