const fs = require('fs'); const path = require('path'); const os = require('os'); const util = require('util'); const inquirer = require('inquirer'); const exec = util.promisify(require('child_process').exec); import axios from 'axios'; import i18n from '../utils/i18n'; import { DownloadManager } from '../utils/download-manager'; import { GetManager } from '../config/get/get-manager'; import { AddManager } from '../config/add/add-manager'; import * as logger from '../utils/logger'; import { PackageType } from '../utils/package-type'; import { Hook } from './hook'; import { Parse } from '../utils/parse'; import { ConfigError } from '../error/config-error'; const S_COMPONENT_BASE_PATH = path.join(os.homedir(), `.s/components`); const BASE_API_URL = 'https://tool.serverlessfans.com/api'; const CHECK_VERSION_URL = '/package/object/version'; const TYPE_MAP = { [PackageType.component]: 'Component', [PackageType.plugin]: 'Plugin', [PackageType.application]: 'Application' }; export interface ComponentConfig { Component: string; Provider: string; Access?: string; Extends: any; Properties: { [key: string]: any }; Params: any; ProjectName: string; } export interface VersionCheckParams { name: string; type: PackageType; provider: string; } export interface GenerateComponentExeParams { list: Array; parse: Parse; parsedObj: any; method: string; } export async function synchronizeExecuteComponentList( list: any = [], index:any = 0, initData:any = {} ) { if (index >= 0 && index < list.length) { return await list[index]().then(async ({ name, data }: any) => { initData[name] = data; return await synchronizeExecuteComponentList( list, index + 1, initData ); }); } return initData; } export function generateSynchronizeComponentExeList( { list, parse, parsedObj, method }: GenerateComponentExeParams, equipment: ( parse: Parse, projectName: string, parsedObj: any ) => Promise ): Array { return list.map((projectName) => { return () => { return new Promise(async (resolve, reject) => { try { logger.info(i18n.__(`Start executing project {{projectName}}`, {projectName: projectName})); const projectConfig = await equipment(parse, projectName, parsedObj); const componentExecute = new ComponentExeCute(projectConfig, method); const Output = await componentExecute.init(); parsedObj[projectName].Output = Output; logger.info(i18n.__(`Project {{projectName}} successfully to execute \n\t`, {projectName: projectName})); resolve({ name: projectName, data: Output }); } catch (e) { if (String(e).indexOf('method does not exist') !== -1) { logger.error(i18n.__(`Project {{projectName}} doesn't have the method: {{method}}`, {projectName: projectName, method: method} )); resolve({}); } else { logger.error(e); logger.error(i18n.__(`Project {{projectName}} failed to execute`, {projectName: projectName})); // 批量部署的时候,单个出错,不会阻塞全部流程 resolve({ name: projectName, data: '' }); // reject(e); } } }); }; }); } export class ComponentExeCute { protected componentPath: string; protected credentials: any; protected isPackageProject = false; constructor( protected componentConfig: ComponentConfig, protected method: string ) { if (!fs.existsSync(S_COMPONENT_BASE_PATH)) { fs.mkdirSync(S_COMPONENT_BASE_PATH); } let { Component: name } = this.componentConfig; this.componentPath = path.join(S_COMPONENT_BASE_PATH, `/${name}`); this.isPackageProject = fs.existsSync( path.join(this.componentPath, '/package.json') ); } async init() { let { Component: name, Provider } = this.componentConfig; const providerOnlyPrefix = Provider.split('.')[0]; let credentials = await this.getCredentials(); if (!credentials) { credentials = await this.setCredentials(providerOnlyPrefix); } this.credentials = credentials; // 判断组件名类型 // 如果路径存在,获取路径 let version; if (await fs.existsSync(name)) { this.componentPath = name; } else { if (name.indexOf('@') !== -1) { const temp = name.split('@'); name = temp[0]; version = temp[1]; if (!(name && version)) { throw new Error('Could not get component name and version, please check you component content.'); } } else { version = await this.getRemoteComponentVersion({ name, provider: providerOnlyPrefix, type: PackageType.component }); } // 判断组件是否已存在 const tempPath = path.join(S_COMPONENT_BASE_PATH, `/${name}-${providerOnlyPrefix}@${version}`); if (!(await fs.existsSync(tempPath))) { logger.info(i18n.__(`No component {{name}}-{{provider}}@{{version}} is found, it will be downloaded soon, this may take a few minutes......`, {name: name, version: version, provider: providerOnlyPrefix})); await this.downLoadAndUnCompressComponentV2(PackageType.component, name, providerOnlyPrefix, version); } this.componentPath = tempPath; } // { if (!this.componentExist()) { // logger.info( // `No component "${name}" is found, it will be downloaded soon, this may take a few minutes......` // ); // await this.downLoadAndUnCompressComponent( // PackageType.component, // name, // providerOnlyPrefix // ); // } else if (this.isPackageProject) { // // const localVersion = this.getLocalComponentVersion(); // const remoteVersion = await this.getRemoteComponentVersion({ // name, // provider: providerOnlyPrefix, // type: PackageType.component // }); // // const isSameVersion = this.checkVersion(localVersion, remoteVersion); // if (!isSameVersion) { // await this.downLoadAndUnCompressComponent( // PackageType.component, // name, // providerOnlyPrefix // ); // } // } } return await this.startExecute(); } async getCredentials() { const { Provider, Access } = this.componentConfig; const configUserInput = { 'Provider': Provider, 'AliasName': Access }; const getManager = new GetManager(); await getManager.initAccessData(configUserInput); const providerMap: { [key: string]: any; } = await getManager.getUserSecretID(configUserInput); const accessData = Provider && Access ? providerMap : providerMap[`${Provider}.${Access || 'default'}`]; if (accessData) { return accessData; } if (!Access) { logger.warning('\n'); logger.warning(' You can configure the specified key in yaml. For example:'); logger.warning(`\n ${this.componentConfig.ProjectName}`); logger.warning(` Component: ${this.componentConfig.Component}`); logger.warning(` Provider: ${Provider}`); logger.warning(' Access: Fill in the specified key here'); logger.warning('\n'); let result = ''; const selectObject = []; Object.keys(providerMap).forEach(item=>{ const temp = { name: item.startsWith('project') ? `${item.replace('project.', 'project: ')}` : `${item.replace(Provider + '.', Provider + ': ')}`, value: item }; if (Provider) { if (item.startsWith(Provider) || item.startsWith('project')) { selectObject.push(temp); } } else { selectObject.push(temp); } }); // for (let item in providerMap) { // const temp = { // name: item.startsWith('project') ? `${item.replace('project.', 'project: ')}` : `${item.replace(Provider + '.', Provider + ': ')}`, // value: item // }; // if (Provider) { // if (item.startsWith(Provider) || item.startsWith('project')) { // selectObject.push(temp); // } // } else { // selectObject.push(temp); // } // } selectObject.push({ name: 'Create a new account', value: 'create' }); await inquirer .prompt([ { type: 'list', name: 'access', message: i18n.__('Please select an access:'), choices: selectObject } ]) .then((answers: any) => { result = answers.access; }); if (result === 'create') { return undefined; } return providerMap[result]; } // 没找到密钥信息 throw new ConfigError('Failed to get the specified key: {{access}}', { access: Access }); } async setCredentials(provider: any) { const addManager = new AddManager(); const result = addManager.inputLengthZero(provider); return result; } componentExist() { return fs.existsSync(this.componentPath); } async getRemoteComponentVersion({ name, provider, type }: VersionCheckParams) { const url = `${BASE_API_URL}${CHECK_VERSION_URL}`; let version = null; try { const result: any = await axios.get(url, { params: { name, provider, type: TYPE_MAP[type] } }); if (result.data && result.data.Response && result.data.Response.Version) { version = result.data.Response.Version; } else { throw new Error('Please Check the provider and component'); } } catch (e) { logger.error(e.message); } return version; } getLocalComponentVersion() { const { Component: name } = this.componentConfig; const pkgFile = path.join(S_COMPONENT_BASE_PATH, `/${name}/package.json`); if (!fs.existsSync(pkgFile)) { return null; } const componentPackageJsonObj = require(pkgFile); return componentPackageJsonObj.version; } // private checkVersion(localVersion: string | null, remoteVersion: string) { // return remoteVersion === localVersion; // } private async preLoadNodeModules() { if ( this.isPackageProject && !fs.existsSync(path.join(this.componentPath, 'node_modules')) ) { logger.info('npm install'); const { stdout, stderr } = await exec('npm install', { cwd: this.componentPath }); if (stderr) { logger.error(stderr); } else { logger.info(stdout); } } } async downLoadAndUnCompressComponent( type: PackageType, name: string, provider: string ) { const downloadManager = new DownloadManager(); const componentPath = path.join(S_COMPONENT_BASE_PATH, `/${name}`); if (!fs.existsSync(componentPath)) { fs.mkdirSync(componentPath); } await downloadManager.downloadTemplateFromAppCenter( type, name, componentPath, provider ); } async downLoadAndUnCompressComponentV2( type: PackageType, name: string, provider: string, version: string ) { const downloadManager = new DownloadManager(); const componentPath = path.join(S_COMPONENT_BASE_PATH, `/${name}-${provider}@${version}`); if (!fs.existsSync(componentPath)) { fs.mkdirSync(componentPath); } await downloadManager.downloadTemplateFromAppCenter( type, name, componentPath, provider ); } private loadExtends(): Hook | null { const { Extends = {} } = this.componentConfig; const method = this.method; let hookExecuteInstance = null; const hooks = Extends[method]; if (hooks) { hookExecuteInstance = new Hook(hooks); } return hookExecuteInstance; } async loadPreExtends(extend: Hook | null) { if (extend) { // logger.info('Start the pre-hook'); await extend.executePreHook(); } } async loadAfterExtend(extend: Hook | null) { if (extend) { // logger.info('Start the after-hook'); await extend.executeAfterHook(); } } async loadComponent() { await this.preLoadNodeModules(); // check and install node_module const componentModule = require(this.componentPath); return componentModule; } async invokeMethod(ComponentClass: any, method: string, data: any) { const promise = new Promise(async (resolve, reject) => { try { const componentInstance = new ComponentClass(); const result = await componentInstance[method](data); resolve(result); } catch (e) { reject(new Error(e.message)); // reject(new Error(`${method} method does not exist`)); } }); return promise; } async executeCommand(): Promise { const { Properties, Params, Provider, Access, Component, ProjectName } = this.componentConfig; const inputs = { Properties, Credentials: this.credentials, Project: { ProjectName, Component, Provider: Provider, AccessAlias: Access || '' }, Command: this.method, Args: Params || {}, State: {} }; const ComponentClass = await this.loadComponent(); const data:any = await this.invokeMethod( ComponentClass, this.method, inputs ); // // const outData = { // projectName: ProjectName, // ...data // }; // logger.success(JSON.stringify(outData, null, "\t")); return data; } async startExecute() { let outData = {}; const extend = this.loadExtends(); await this.loadPreExtends(extend); outData = await this.executeCommand(); await this.loadAfterExtend(extend); return outData; } }