diff --git a/CHANGELOG.md b/CHANGELOG.md index 678dbae..b8d0efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.2.1](https://github.com/serverless-components/tencent-nuxtjs/compare/v0.2.0...v0.2.1) (2021-01-26) + + +### Bug Fixes + +* change cos access by policy ([7884f8d](https://github.com/serverless-components/tencent-nuxtjs/commit/7884f8de205951956861bfcda6e1a892e1b18018)) + # [0.2.0](https://github.com/serverless-components/tencent-nuxtjs/compare/v0.1.8...v0.2.0) (2021-01-25) diff --git a/README.md b/README.md index 53f64d3..9512e34 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +⚠️⚠️⚠️ 所有框架组件项目迁移到 [tencent-framework-components](https://github.com/serverless-components/tencent-framework-components). + [![Serverless Nuxtjs Tencent Cloud](https://img.serverlesscloud.cn/2020310/1583829094342-nuxt.js%20_%E9%95%BF.png)](http://serverless.com) # 腾讯云 Nuxt.js Serverless Component @@ -86,7 +88,7 @@ inputs: environment: release ``` -- 点此查看[更多配置及说明](https://github.com/yugasun/tencent-nuxtjs/tree/master/docs/configure.md) +- 点此查看[更多配置及说明](https://github.com/serverless-components/tencent-nuxtjs/tree/master/docs/configure.md) ### 4. 开发调试 @@ -223,6 +225,10 @@ server.get('/no-report', (req, res, next) => { 那么用户在访问 `GET /no-report` 路由时,就不会上报自定义监控指标。 +## 文件上传 + +[文件上传教程](https://github.com/serverless-components/tencent-nuxtjs/tree/master/docs/upload.md) + ## License MIT License diff --git a/docs/configure.md b/docs/configure.md index bc2d0eb..d7689a2 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -74,8 +74,6 @@ inputs: staticConf: cosConf: bucket: static-bucket - acl: - permissions: public-read sources: - src: .nuxt/dist/client targetDir: / @@ -235,17 +233,10 @@ Refer to: https://cloud.tencent.com/document/product/628/14906 ##### COS 配置 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| -------- | :------: | :------: | :-----------------------------------------------------------------------------: | :------------------------------- | -| bucket | 是 | string | | COS 存储同名称,没有将自动创建 | -| acl | 否 | Object | | 存储桶权限配置,参考 [acl](#acl) | -| sources | 否 | Object[] | `[{src: '.nuxt/dist/client', targetDir: '/'}, {src: 'static', targetDir: '/'}]` | 需要托管到 COS 的静态资源目录 | - -###### acl - -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| ----------- | :------: | :----: | :-----------: | :----------- | -| permissions | 是 | string | `public-read` | 公共权限配置 | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| -------- | :------: | :------: | :-----------------------------------------------------------------------------: | :----------------------------- | +| bucket | 是 | string | | COS 存储同名称,没有将自动创建 | +| sources | 否 | Object[] | `[{src: '.nuxt/dist/client', targetDir: '/'}, {src: 'static', targetDir: '/'}]` | 需要托管到 COS 的静态资源目录 | ##### CDN 配置 diff --git a/docs/upload.md b/docs/upload.md new file mode 100644 index 0000000..114543c --- /dev/null +++ b/docs/upload.md @@ -0,0 +1,32 @@ +## 文件上传说明 + +项目中如果涉及到文件上传,需要依赖 API 网关提供的 [Base64 编码能力](https://cloud.tencent.com/document/product/628/51799),使用时只需要 `serverless.yml` 中配置 `isBase64Encoded` 为 `true`,如下: + +```yaml +app: appDemo +stage: dev +component: nuxtjs +name: nuxtjsDemo + +inputs: + # 省略... + apigatewayConf: + isBase64Encoded: true + # 省略... + # 省略... +``` + +当前 API 网关支持上传最大文件大小为 `2M`,如果文件过大,请修改为前端直传对象存储方案。 + +## Base64 示例 + +此 Github 项目的 `example` 目录下存在两个模板文件: + +- [sls.express.js](../example/sls.express.js) +- [sls.koa.js](../example/sls.koa.js) + +开发者可根据个人项目需要参考修改,使用时需要复制对应文件名为 `sls.js`。 + +两个模板文件中均实现了文件上传接口 `POST /upload`。使用 Koa 的项目,如果要支持文件上传,需要安装 `@koajs/multer` 和 `multer` 包。使用 Express 的项目,如果要支持文件上传,需要安装 `multer` 包。 + +同时需要在 `serverless.yml` 的 `apigatewayConf` 中配置 `isBase64Encoded` 为 `true`。 diff --git a/example/serverless.yml b/example/serverless.yml index 512f698..79d03bb 100644 --- a/example/serverless.yml +++ b/example/serverless.yml @@ -1,4 +1,3 @@ -org: orgDemo app: appDemo stage: dev component: nuxtjs diff --git a/example/sls.express.js b/example/sls.express.js new file mode 100644 index 0000000..01f027d --- /dev/null +++ b/example/sls.express.js @@ -0,0 +1,41 @@ +const multer = require('multer') +const express = require('express') +const { loadNuxt } = require('nuxt') + +const isServerless = process.env.SERVERLESS + +async function createServer() { + const upload = multer({ dest: isServerless ? '/tmp/upload' : './upload' }) + + const server = express() + const nuxt = await loadNuxt('start') + + server.post('/upload', upload.single('file'), (req, res) => { + res.send({ + success: true, + data: req.file + }) + }) + + server.all('*', (req, res, next) => { + return nuxt.render(req, res, next) + }) + + // define binary type for response + // if includes, will return base64 encoded, very useful for images + server.binaryTypes = ['*/*'] + + return server +} + +module.exports = createServer + +if (isServerless) { + module.exports = createServer +} else { + createServer().then((server) => { + server.listen(3000, () => { + console.log(`Server start on http://localhost:3000`) + }) + }) +} diff --git a/example/sls.koa.js b/example/sls.koa.js new file mode 100644 index 0000000..5e037b7 --- /dev/null +++ b/example/sls.koa.js @@ -0,0 +1,47 @@ +const Koa = require('koa') +const Router = require('@koa/router') +const multer = require('@koa/multer') + +const { loadNuxt } = require('nuxt') + +const isServerless = process.env.SERVERLESS + +async function createServer() { + const server = new Koa() + const router = new Router() + const upload = multer({ dest: isServerless ? '/tmp/upload' : './upload' }) + const nuxt = await loadNuxt('start') + + router.post('/upload', upload.single('file'), (ctx) => { + ctx.body = { + success: true, + data: ctx.file + } + }) + + server.use(router.routes()).use(router.allowedMethods()) + + server.use((ctx) => { + ctx.status = 200 + ctx.respond = false + ctx.req.ctx = ctx + + nuxt.render(ctx.req, ctx.res) + }) + + // define binary type for response + // if includes, will return base64 encoded, very useful for images + server.binaryTypes = ['*/*'] + + return server +} + +if (process.env.SERVERLESS) { + module.exports = createServer +} else { + createServer().then((server) => { + server.listen(3000, () => { + console.log(`Server start on http://localhost:3000`) + }) + }) +} diff --git a/serverless.component.yml b/serverless.component.yml index 504421a..94b1da2 100644 --- a/serverless.component.yml +++ b/serverless.component.yml @@ -1,5 +1,6 @@ name: nuxtjs -version: 0.2.0 +version: 1.0.14 +# version: dev author: 'Tencent Cloud, Inc.' org: 'Tencent Cloud, Inc.' description: Deploy a serverless Nuxt.js application onto Tencent SCF and API Gateway. diff --git a/src/_shims/handler.js b/src/_shims/handler.js index f51ffad..f460f39 100644 --- a/src/_shims/handler.js +++ b/src/_shims/handler.js @@ -1,4 +1,8 @@ -require('tencent-component-monitor') +try { + require('tencent-component-monitor') +} catch (e) { + console.log(e) +} const fs = require('fs') const path = require('path') const { createServer, proxy } = require('tencent-serverless-http') @@ -7,33 +11,39 @@ let app let server module.exports.handler = async (event, context) => { - const userSls = path.join(__dirname, '..', process.env.SLS_ENTRY_FILE) - if (fs.existsSync(userSls)) { - // eslint-disable-next-line - console.log(`Using user custom entry file ${process.env.SLS_ENTRY_FILE}`) - app = await require(userSls)(true) - } else { - app = await require('./sls')(false) + if (!app) { + const userSls = path.join(__dirname, '..', process.env.SLS_ENTRY_FILE) + if (fs.existsSync(userSls)) { + // eslint-disable-next-line + console.log(`Using user custom entry file ${process.env.SLS_ENTRY_FILE}`) + app = await require(userSls)(true) + } else { + app = await require('./sls')(false) + } + + // provide sls intialize hooks + if (app.slsInitialize && typeof app.slsInitialize === 'function') { + await app.slsInitialize() + } } // attach event and context to request - app.request.__SLS_EVENT__ = event - app.request.__SLS_CONTEXT__ = context - - if (!server) { - server = createServer( - app.callback && typeof app.callback === 'function' ? app.callback() : app, - null, - app.binaryTypes || [] - ) + try { + app.request.__SLS_EVENT__ = event + app.request.__SLS_CONTEXT__ = context + } catch (e) { + // no op } - context.callbackWaitsForEmptyEventLoop = app.callbackWaitsForEmptyEventLoop === true + // do not cache server, so we can pass latest event to server + server = createServer( + app.callback && typeof app.callback === 'function' ? app.callback() : app, + null, + app.binaryTypes || [] + ) - if (app.slsInitialize && typeof app.slsInitialize === 'function') { - await app.slsInitialize() - } + context.callbackWaitsForEmptyEventLoop = app.callbackWaitsForEmptyEventLoop === true - const result = await proxy(server, event, context, 'PROMISE') - return result.promise + const { promise } = await proxy(server, event, context, 'PROMISE') + return promise } diff --git a/src/_shims/sls.js b/src/_shims/sls.js index 45afad9..880e4c1 100644 --- a/src/_shims/sls.js +++ b/src/_shims/sls.js @@ -21,6 +21,7 @@ async function createServer() { // if includes, will return base64 encoded, very useful for images server.binaryTypes = ['*/*'] + // 返回 server return server } diff --git a/src/config.js b/src/config.js index 387c385..addb013 100644 --- a/src/config.js +++ b/src/config.js @@ -1,22 +1,139 @@ +Object.defineProperty(exports, '__esModule', { value: true }) +exports.getConfig = void 0 +const fs = require('fs') +const path = require('path') +const YAML = require('js-yaml') +const TEMPLATE_BASE_URL = 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com' +const frameworks = { + express: { + injectSlsSdk: true, + runtime: 'Nodejs10.15', + defaultEntryFile: 'sls.js', + defaultStatics: [{ src: 'public', targetDir: '/' }] + }, + koa: { + injectSlsSdk: true, + runtime: 'Nodejs10.15', + defaultEntryFile: 'sls.js', + defaultStatics: [{ src: 'public', targetDir: '/' }] + }, + egg: { + injectSlsSdk: true, + runtime: 'Nodejs10.15', + defaultEntryFile: 'sls.js', + defaultStatics: [{ src: 'public', targetDir: '/' }], + defaultEnvs: [ + { + key: 'SERVERLESS', + value: '1' + }, + { + key: 'EGG_APP_CONFIG', + value: '{"rundir":"/tmp","logger":{"dir":"/tmp"}}' + } + ] + }, + nestjs: { + injectSlsSdk: true, + runtime: 'Nodejs10.15', + defaultEntryFile: 'sls.js', + defaultStatics: [{ src: 'public', targetDir: '/' }] + }, + nextjs: { + injectSlsSdk: true, + runtime: 'Nodejs10.15', + defaultEntryFile: 'sls.js', + defaultStatics: [ + { src: '.next/static', targetDir: '/_next/static' }, + { src: 'public', targetDir: '/' } + ] + }, + nuxtjs: { + injectSlsSdk: true, + runtime: 'Nodejs10.15', + defaultEntryFile: 'sls.js', + defaultStatics: [ + { src: '.nuxt/dist/client', targetDir: '/' }, + { src: 'static', targetDir: '/' } + ] + }, + laravel: { + injectSlsSdk: false, + runtime: 'Php7', + defaultEnvs: [ + { + key: 'SERVERLESS', + value: '1' + }, + { + key: 'VIEW_COMPILED_PATH', + value: '/tmp/storage/framework/views' + }, + { + key: 'SESSION_DRIVER', + value: 'array' + }, + { + key: 'LOG_CHANNEL', + value: 'stderr' + }, + { + key: 'APP_STORAGE', + value: '/tmp/storage' + } + ] + }, + thinkphp: { + injectSlsSdk: false, + runtime: 'Php7' + }, + flask: { + injectSlsSdk: false, + runtime: 'Python3.6' + }, + django: { + injectSlsSdk: false, + runtime: 'Python3.6' + } +} const CONFIGS = { - templateUrl: - 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/nuxtjs-demo.zip', - compName: 'nuxtjs', - compFullname: 'Nuxt.js', - defaultEntryFile: 'sls.js', + // support metrics frameworks + pythonFrameworks: ['flask', 'django'], + supportMetrics: ['express', 'next', 'nuxt'], region: 'ap-guangzhou', + description: 'Created by Serverless Component', handler: 'sl_handler.handler', - runtime: 'Nodejs10.15', - timeout: 3, + timeout: 10, memorySize: 128, namespace: 'default', - description: 'Created by Serverless Component', - defaultStatics: [ - { src: '.nuxt/dist/client', targetDir: '/' }, - { src: 'static', targetDir: '/' } + defaultEnvs: [ + { + key: 'SERVERLESS', + value: '1' + } ], - defaultCdnConf: { - autoRefresh: true, + cos: { + lifecycle: [ + { + status: 'Enabled', + id: 'deleteObject', + expiration: { days: '10' }, + abortIncompleteMultipartUpload: { daysAfterInitiation: '10' } + } + ] + }, + cdn: { + forceRedirect: { + switch: 'on', + redirectType: 'https', + redirectStatusCode: 301 + }, + https: { + switch: 'on', + http2: 'on' + } + }, + defaultCdnConfig: { forceRedirect: { switch: 'on', redirectType: 'https', @@ -26,7 +143,41 @@ const CONFIGS = { switch: 'on', http2: 'on' } + }, + acl: { + permissions: 'public-read', + grantRead: '', + grantWrite: '', + grantFullControl: '' + }, + getPolicy(region, bucket, appid) { + return { + Statement: [ + { + Principal: { qcs: ['qcs::cam::anyone:anyone'] }, + Effect: 'Allow', + Action: [ + 'name/cos:HeadBucket', + 'name/cos:ListMultipartUploads', + 'name/cos:ListParts', + 'name/cos:GetObject', + 'name/cos:HeadObject', + 'name/cos:OptionsObject' + ], + Resource: [`qcs::cos:${region}:uid/${appid}:${bucket}/*`] + } + ], + version: '2.0' + } } } - -module.exports = CONFIGS +const getConfig = () => { + const { name: framework } = YAML.load( + // framework.yml 会在组件部署流程中动态生成 + fs.readFileSync(path.join(__dirname, 'framework.yml'), 'utf-8') + ) + const templateUrl = `${TEMPLATE_BASE_URL}/${framework}-demo.zip` + const frameworkConfigs = frameworks[framework] + return Object.assign(Object.assign({ framework, templateUrl }, CONFIGS), frameworkConfigs) +} +exports.getConfig = getConfig diff --git a/src/formatter.js b/src/formatter.js new file mode 100644 index 0000000..bdca8c0 --- /dev/null +++ b/src/formatter.js @@ -0,0 +1,319 @@ +Object.defineProperty(exports, '__esModule', { value: true }) +exports.formatInputs = exports.formatStaticCdnInputs = exports.formatStaticCosInputs = void 0 +const AdmZip = require('adm-zip') +const error_1 = require('tencent-component-toolkit/lib/utils/error') +const config_1 = require('./config') +const CONFIGS = config_1.getConfig() +const utils_1 = require('./utils') +const formatStaticCosInputs = async (cosConf, appId, codeZipPath, region) => { + try { + const staticCosInputs = [] + const sources = cosConf.sources || CONFIGS.defaultStatics + const { bucket } = cosConf + // 删除用户填写时携带的 appid + const bucketName = utils_1.removeAppid(bucket, appId) + const staticPath = `/tmp/${utils_1.generateId()}` + const codeZip = new AdmZip(codeZipPath) + const entries = codeZip.getEntries() + for (let i = 0; i < sources.length; i++) { + const curSource = sources[i] + const entryName = `${curSource.src}` + let exist = false + entries.forEach((et) => { + if (et.entryName.indexOf(entryName) === 0) { + codeZip.extractEntryTo(et, staticPath, true, true) + exist = true + } + }) + if (exist) { + const cosInputs = { + force: true, + protocol: 'https', + bucket: `${bucketName}-${appId}`, + src: `${staticPath}/${entryName}`, + keyPrefix: curSource.targetDir || '/' + } + staticCosInputs.push(cosInputs) + } + } + return { + bucket: `${bucketName}-${appId}`, + staticCosInputs, + // 通过设置 policy 来支持公网访问 + policy: CONFIGS.getPolicy(region, `${bucketName}-${appId}`, appId) + } + } catch (e) { + throw new error_1.ApiTypeError( + `UTILS_${CONFIGS.framework.toUpperCase()}_prepareStaticCosInputs`, + e.message, + e.stack + ) + } +} +exports.formatStaticCosInputs = formatStaticCosInputs +const formatStaticCdnInputs = async (cdnConf, origin) => { + const cdnInputs = { + async: true, + area: cdnConf.area || 'mainland', + domain: cdnConf.domain, + serviceType: 'web', + origin: { + origins: [origin], + originType: 'cos', + originPullProtocol: 'https' + }, + onlyRefresh: cdnConf.onlyRefresh + } + if (cdnConf.https) { + // 通过提供默认的配置来简化用户配置 + cdnInputs.forceRedirect = cdnConf.forceRedirect || CONFIGS.defaultCdnConfig.forceRedirect + if (!cdnConf.https.certId) { + throw new error_1.ApiTypeError( + `PARAMETER_${CONFIGS.framework.toUpperCase()}_HTTPS`, + 'https.certId is required' + ) + } + cdnInputs.https = Object.assign(Object.assign({}, CONFIGS.defaultCdnConfig.https), { + http2: cdnConf.https.http2 || 'on', + certInfo: { + certId: cdnConf.https.certId + } + }) + } + if (cdnConf.autoRefresh !== false) { + cdnInputs.refreshCdn = { + flushType: cdnConf.refreshType || 'delete', + urls: [`http://${cdnInputs.domain}`, `https://${cdnInputs.domain}`] + } + } + return cdnInputs +} +exports.formatStaticCdnInputs = formatStaticCdnInputs +const formatInputs = (state, inputs = {}) => { + var _a, + _b, + _c, + _d, + _e, + _f, + _g, + _h, + _j, + _k, + _l, + _m, + _o, + _p, + _q, + _r, + _s, + _t, + _u, + _v, + _w, + _x, + _y, + _z, + _0, + _1, + _2, + _3, + _4, + _5, + _6, + _7, + _8, + _9, + _10, + _11, + _12, + _13 + // 标准化函数参数 + const tempFunctionConf = (_a = inputs.functionConf) !== null && _a !== void 0 ? _a : {} + const region = (_b = inputs.region) !== null && _b !== void 0 ? _b : 'ap-guangzhou' + // 获取状态中的函数名称 + const regionState = state[region] + const stateFunctionName = state.functionName || (regionState && regionState.funcitonName) + const functionConf = Object.assign(tempFunctionConf, { + code: { + src: inputs.src, + bucket: + (_c = inputs === null || inputs === void 0 ? void 0 : inputs.srcOriginal) === null || + _c === void 0 + ? void 0 + : _c.bucket, + object: + (_d = inputs === null || inputs === void 0 ? void 0 : inputs.srcOriginal) === null || + _d === void 0 + ? void 0 + : _d.object + }, + name: + (_g = + (_f = (_e = tempFunctionConf.name) !== null && _e !== void 0 ? _e : inputs.functionName) !== + null && _f !== void 0 + ? _f + : stateFunctionName) !== null && _g !== void 0 + ? _g + : utils_1.getDefaultFunctionName(), + region: region, + role: + (_j = (_h = tempFunctionConf.role) !== null && _h !== void 0 ? _h : inputs.role) !== null && + _j !== void 0 + ? _j + : '', + handler: + (_l = (_k = tempFunctionConf.handler) !== null && _k !== void 0 ? _k : inputs.handler) !== + null && _l !== void 0 + ? _l + : CONFIGS.handler, + runtime: + (_o = (_m = tempFunctionConf.runtime) !== null && _m !== void 0 ? _m : inputs.runtime) !== + null && _o !== void 0 + ? _o + : CONFIGS.runtime, + namespace: + (_q = (_p = tempFunctionConf.namespace) !== null && _p !== void 0 ? _p : inputs.namespace) !== + null && _q !== void 0 + ? _q + : CONFIGS.namespace, + description: + (_s = + (_r = tempFunctionConf.description) !== null && _r !== void 0 ? _r : inputs.description) !== + null && _s !== void 0 + ? _s + : CONFIGS.description, + layers: + (_u = (_t = tempFunctionConf.layers) !== null && _t !== void 0 ? _t : inputs.layers) !== + null && _u !== void 0 + ? _u + : [], + cfs: (_v = tempFunctionConf.cfs) !== null && _v !== void 0 ? _v : [], + publish: tempFunctionConf.publish || inputs.publish, + traffic: tempFunctionConf.traffic || inputs.traffic, + lastVersion: state.lastVersion, + timeout: (_w = tempFunctionConf.timeout) !== null && _w !== void 0 ? _w : CONFIGS.timeout, + memorySize: + (_x = tempFunctionConf.memorySize) !== null && _x !== void 0 ? _x : CONFIGS.memorySize, + tags: + (_z = (_y = tempFunctionConf.tags) !== null && _y !== void 0 ? _y : inputs.tags) !== null && + _z !== void 0 + ? _z + : null + }) + if (!((_0 = functionConf.environment) === null || _0 === void 0 ? void 0 : _0.variables)) { + functionConf.environment = { + variables: {} + } + } + // 添加框架需要添加的默认环境变量 + const { defaultEnvs } = CONFIGS + defaultEnvs.forEach((item) => { + functionConf.environment.variables[item.key] = item.value + }) + // 添加入口文件环境变量 + const entryFile = functionConf.entryFile || inputs.entryFile || CONFIGS.defaultEntryFile + if (entryFile) { + functionConf.environment.variables['SLS_ENTRY_FILE'] = entryFile + } + // django 项目需要 projectName 参数 + if (CONFIGS.framework === 'django') { + functionConf.projectName = + (_3 = + (_2 = + (_1 = tempFunctionConf.projectName) !== null && _1 !== void 0 + ? _1 + : tempFunctionConf.djangoProjectName) !== null && _2 !== void 0 + ? _2 + : inputs.djangoProjectName) !== null && _3 !== void 0 + ? _3 + : '' + } + // TODO: 验证流量配置,将废弃 + if (inputs.traffic !== undefined) { + utils_1.validateTraffic(inputs.traffic) + } + // TODO: 判断是否需要配置流量,将废弃 + functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion + // 初始化 VPC 配置,兼容旧的vpc配置 + const vpc = tempFunctionConf.vpcConfig || tempFunctionConf.vpc || inputs.vpcConfig || inputs.vpc + if (vpc) { + functionConf.vpcConfig = vpc + } + // 标准化网关配置参数 + const tempApigwConf = (_4 = inputs.apigatewayConf) !== null && _4 !== void 0 ? _4 : {} + const apigatewayConf = Object.assign(tempApigwConf, { + serviceId: + (_6 = (_5 = tempApigwConf.serviceId) !== null && _5 !== void 0 ? _5 : tempApigwConf.id) !== + null && _6 !== void 0 + ? _6 + : inputs.serviceId, + region: region, + isDisabled: tempApigwConf.isDisabled === true, + serviceName: + (_9 = + (_8 = + (_7 = tempApigwConf.serviceName) !== null && _7 !== void 0 ? _7 : tempApigwConf.name) !== + null && _8 !== void 0 + ? _8 + : inputs.serviceName) !== null && _9 !== void 0 + ? _9 + : utils_1.getDefaultServiceName(), + serviceDesc: + (_11 = + (_10 = tempApigwConf.serviceDesc) !== null && _10 !== void 0 + ? _10 + : tempApigwConf.description) !== null && _11 !== void 0 + ? _11 + : utils_1.getDefaultServiceDescription(), + protocols: tempApigwConf.protocols || ['http'], + environment: tempApigwConf.environment ? tempApigwConf.environment : 'release', + customDomains: tempApigwConf.customDomains || [] + }) + // 如果没配置,添加默认的 API 配置,通常 Web 框架组件是不要用户自定义的 + if (!apigatewayConf.endpoints) { + apigatewayConf.endpoints = [ + { + path: tempApigwConf.path || '/', + enableCORS: + (_12 = tempApigwConf.enableCORS) !== null && _12 !== void 0 ? _12 : tempApigwConf.cors, + serviceTimeout: + (_13 = tempApigwConf.serviceTimeout) !== null && _13 !== void 0 + ? _13 + : tempApigwConf.timeout, + method: tempApigwConf.method || 'ANY', + apiName: tempApigwConf.apiName || 'index', + isBase64Encoded: tempApigwConf.isBase64Encoded, + function: { + isIntegratedResponse: true, + functionName: functionConf.name, + functionNamespace: functionConf.namespace, + functionQualifier: + (tempApigwConf.function && tempApigwConf.function.functionQualifier) || + apigatewayConf.qualifier || + '$DEFAULT' + } + } + ] + } + if (tempApigwConf.usagePlan) { + apigatewayConf.endpoints[0].usagePlan = { + usagePlanId: tempApigwConf.usagePlan.usagePlanId, + usagePlanName: tempApigwConf.usagePlan.usagePlanName, + usagePlanDesc: tempApigwConf.usagePlan.usagePlanDesc, + maxRequestNum: tempApigwConf.usagePlan.maxRequestNum + } + } + if (tempApigwConf.auth) { + apigatewayConf.endpoints[0].auth = { + secretName: tempApigwConf.auth.secretName, + secretIds: tempApigwConf.auth.secretIds + } + } + return { + region, + functionConf, + apigatewayConf + } +} +exports.formatInputs = formatInputs diff --git a/src/framework.yml b/src/framework.yml new file mode 100644 index 0000000..2b5be26 --- /dev/null +++ b/src/framework.yml @@ -0,0 +1 @@ +name: nuxtjs diff --git a/src/handler.js b/src/handler.js new file mode 100644 index 0000000..522e77d --- /dev/null +++ b/src/handler.js @@ -0,0 +1,2 @@ +const { handler } = require('@serverless/core') +module.exports.handler = handler diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b1a3321 --- /dev/null +++ b/src/index.js @@ -0,0 +1,379 @@ +Object.defineProperty(exports, '__esModule', { value: true }) +exports.ServerlessComponent = void 0 +const core_1 = require('@serverless/core') +const tencent_component_toolkit_1 = require('tencent-component-toolkit') +const error_1 = require('tencent-component-toolkit/lib/utils/error') +const utils_1 = require('./utils') +const formatter_1 = require('./formatter') +const config_1 = require('./config') +const CONFIGS = config_1.getConfig() +class ServerlessComponent extends core_1.Component { + getCredentials() { + const { tmpSecrets } = this.credentials.tencent + if (!tmpSecrets || !tmpSecrets.TmpSecretId) { + throw new error_1.ApiTypeError( + 'CREDENTIAL', + 'Cannot get secretId/Key, your account could be sub-account and does not have the access to use SLS_QcsRole, please make sure the role exists first, then visit https://cloud.tencent.com/document/product/1154/43006, follow the instructions to bind the role to your account.' + ) + } + return { + SecretId: tmpSecrets.TmpSecretId, + SecretKey: tmpSecrets.TmpSecretKey, + Token: tmpSecrets.Token + } + } + getAppId() { + return this.credentials.tencent.tmpSecrets.appId + } + async uploadCodeToCos(appId, inputs, region) { + var _a, _b, _c, _d, _e, _f + const credentials = this.getCredentials() + const bucketName = + ((_a = inputs.code) === null || _a === void 0 ? void 0 : _a.bucket) || + `sls-cloudfunction-${region}-code` + const objectName = + ((_b = inputs.code) === null || _b === void 0 ? void 0 : _b.object) || + `${inputs.name}-${Math.floor(Date.now() / 1000)}.zip` + // if set bucket and object not pack code + if ( + !((_c = inputs.code) === null || _c === void 0 ? void 0 : _c.bucket) || + !((_d = inputs.code) === null || _d === void 0 ? void 0 : _d.object) + ) { + const zipPath = await utils_1.getCodeZipPath(inputs) + console.log(`Code zip path ${zipPath}`) + // save the zip path to state for lambda to use it + this.state.zipPath = zipPath + const cos = new tencent_component_toolkit_1.Cos(credentials, region) + if (!((_e = inputs.code) === null || _e === void 0 ? void 0 : _e.bucket)) { + // create default bucket + await cos.deploy({ + bucket: bucketName + '-' + appId, + force: true, + lifecycle: [ + { + status: 'Enabled', + id: 'deleteObject', + expiration: { days: '10' }, + abortIncompleteMultipartUpload: { daysAfterInitiation: '10' } + } + ] + }) + } + // upload code to cos + if (!((_f = inputs.code) === null || _f === void 0 ? void 0 : _f.object)) { + console.log(`Getting cos upload url for bucket ${bucketName}`) + const uploadUrl = await cos.getObjectUrl({ + bucket: bucketName + '-' + appId, + object: objectName, + method: 'PUT' + }) + // if shims and sls sdk entries had been injected to zipPath, no need to injected again + console.log(`Uploading code to bucket ${bucketName}`) + const { injectFiles, injectDirs } = utils_1.getInjection(this, inputs) + await this.uploadSourceZipToCOS(zipPath, uploadUrl, injectFiles, injectDirs) + console.log(`Upload ${objectName} to bucket ${bucketName} success`) + } + } + // save bucket state + this.state.bucket = bucketName + this.state.object = objectName + return { + bucket: bucketName, + object: objectName + } + } + async deployFunction(credentials, inputs = {}, region) { + var _a, _b, _c, _d + const appId = this.getAppId() + const code = await this.uploadCodeToCos(appId, inputs, region) + const scf = new tencent_component_toolkit_1.Scf(credentials, region) + const tempInputs = Object.assign(Object.assign({}, inputs), { code }) + const scfOutput = await scf.deploy(utils_1.deepClone(tempInputs)) + const outputs = { + functionName: scfOutput.FunctionName, + runtime: scfOutput.Runtime, + namespace: scfOutput.Namespace + } + this.state = Object.assign(Object.assign({}, this.state), outputs) + // default version is $LATEST + outputs.lastVersion = + (_b = + (_a = scfOutput.LastVersion) !== null && _a !== void 0 ? _a : this.state.lastVersion) !== + null && _b !== void 0 + ? _b + : '$LATEST' + // default traffic is 1.0, it can also be 0, so we should compare to undefined + outputs.traffic = + (_d = (_c = scfOutput.Traffic) !== null && _c !== void 0 ? _c : this.state.traffic) !== + null && _d !== void 0 + ? _d + : 1 + if (outputs.traffic !== 1 && scfOutput.ConfigTrafficVersion) { + outputs.configTrafficVersion = scfOutput.ConfigTrafficVersion + this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion + } + this.state.lastVersion = outputs.lastVersion + this.state.traffic = outputs.traffic + return outputs + } + async deployApigw(credentials, inputs, region) { + var _a, _b + const { state } = this + const serviceId = + (_a = inputs.serviceId) !== null && _a !== void 0 ? _a : state && state.serviceId + const apigw = new tencent_component_toolkit_1.Apigw(credentials, region) + const oldState = (_b = this.state) !== null && _b !== void 0 ? _b : {} + const apigwInputs = Object.assign(Object.assign({}, inputs), { + oldState: { + apiList: oldState.apiList || [], + customDomains: oldState.customDomains || [] + } + }) + // different region deployment has different service id + apigwInputs.serviceId = serviceId + const apigwOutput = await apigw.deploy(utils_1.deepClone(apigwInputs)) + const outputs = { + serviceId: apigwOutput.serviceId, + subDomain: apigwOutput.subDomain, + environment: apigwOutput.environment, + url: `${utils_1.getDefaultProtocol(inputs.protocols)}://${apigwOutput.subDomain}/${ + apigwOutput.environment + }${apigwInputs.endpoints[0].path}` + } + if (apigwOutput.customDomains) { + outputs.customDomains = apigwOutput.customDomains + } + this.state = Object.assign(Object.assign(Object.assign({}, this.state), outputs), { + apiList: apigwOutput.apiList, + created: true + }) + return outputs + } + // deploy static to cos, and setup cdn + async deployStatic(inputs, region) { + const credentials = this.getCredentials() + const { zipPath } = this.state + const appId = this.getAppId() + const deployStaticOutputs = { + cos: { + region: '', + cosOrigin: '' + } + } + if (zipPath) { + console.log(`Deploying static files`) + // 1. deploy to cos + const { staticCosInputs, bucket, policy } = await formatter_1.formatStaticCosInputs( + inputs.cosConf, + appId, + zipPath, + region + ) + const cos = new tencent_component_toolkit_1.Cos(credentials, region) + const cosOutput = { + region, + bucket, + cosOrigin: `${bucket}.cos.${region}.myqcloud.com`, + url: `https://${bucket}.cos.${region}.myqcloud.com` + } + // try to create bucket + await cos.createBucket({ + bucket, + force: true + }) + // set public access policy + await cos.setPolicy({ + bucket, + policy + }) + // 创建 COS 桶后等待1s,防止偶发出现桶不存在错误 + await utils_1.sleep(1000) + // flush bucket + if (inputs.cosConf.replace) { + await cos.flushBucketFiles(bucket) + try { + } catch (e) {} + } + for (let i = 0; i < staticCosInputs.length; i++) { + const curInputs = staticCosInputs[i] + console.log(`Starting upload directory ${curInputs.src} to cos bucket ${curInputs.bucket}`) + await cos.upload({ + bucket, + dir: curInputs.src, + keyPrefix: curInputs.keyPrefix + }) + console.log(`Upload directory ${curInputs.src} to cos bucket ${curInputs.bucket} success`) + } + deployStaticOutputs.cos = cosOutput + // 2. deploy cdn + if (inputs.cdnConf) { + const cdn = new tencent_component_toolkit_1.Cdn(credentials) + const cdnInputs = await formatter_1.formatStaticCdnInputs( + inputs.cdnConf, + cosOutput.cosOrigin + ) + console.log(`Starting deploy cdn ${cdnInputs.domain}`) + const cdnDeployRes = await cdn.deploy(cdnInputs) + const protocol = cdnInputs.https ? 'https' : 'http' + const cdnOutput = { + domain: cdnDeployRes.domain, + url: `${protocol}://${cdnDeployRes.domain}`, + cname: cdnDeployRes.cname + } + deployStaticOutputs.cdn = cdnOutput + console.log(`Deploy cdn ${cdnInputs.domain} success`) + } + console.log(`Deployed static files success`) + return deployStaticOutputs + } + return null + } + async deploy(inputs) { + var _a + console.log(`Deploying ${CONFIGS.framework} application`) + const credentials = this.getCredentials() + // 对Inputs内容进行标准化 + const { region, functionConf, apigatewayConf } = await formatter_1.formatInputs( + this.state, + inputs + ) + // 部署函数 + API网关 + const outputs = {} + if (!((_a = functionConf.code) === null || _a === void 0 ? void 0 : _a.src)) { + outputs.templateUrl = CONFIGS.templateUrl + } + let apigwOutputs + const functionOutputs = await this.deployFunction(credentials, functionConf, region) + // support apigatewayConf.isDisabled + if (apigatewayConf.isDisabled !== true) { + apigwOutputs = await this.deployApigw(credentials, apigatewayConf, region) + } else { + this.state.apigwDisabled = true + } + // optimize outputs for one region + outputs.region = region + outputs.scf = functionOutputs + if (apigwOutputs) { + outputs.apigw = apigwOutputs + } + // start deploy static cdn + if (inputs.staticConf) { + const { staticConf } = inputs + const res = await this.deployStatic(staticConf, region) + if (res) { + this.state.staticConf = res + outputs.staticConf = res + } + } + this.state.region = region + this.state.lambdaArn = functionConf.name + return outputs + } + async removeStatic() { + // remove static + const { region, staticConf } = this.state + if (staticConf) { + console.log(`Removing static files`) + const credentials = this.getCredentials() + // 1. remove cos + if (staticConf.cos) { + const { cos: cosState } = staticConf + if (cosState.bucket) { + const { bucket } = cosState + const cos = new tencent_component_toolkit_1.Cos(credentials, region) + await cos.remove({ bucket }) + } + } + // 2. remove cdn + if (staticConf.cdn) { + const cdn = new tencent_component_toolkit_1.Cdn(credentials) + try { + await cdn.remove(staticConf.cdn) + } catch (e) { + // no op + } + } + console.log(`Remove static config success`) + } + } + async remove() { + console.log(`Removing application`) + const { state } = this + const { region } = state + const { + namespace, + functionName, + created, + serviceId, + apigwDisabled, + customDomains, + apiList, + environment + } = state + const credentials = this.getCredentials() + // if disable apigw, no need to remove + if (apigwDisabled !== true && serviceId) { + const apigw = new tencent_component_toolkit_1.Apigw(credentials, region) + await apigw.remove({ + created, + environment, + serviceId, + apiList, + customDomains + }) + } + if (functionName) { + const scf = new tencent_component_toolkit_1.Scf(credentials, region) + await scf.remove({ + functionName, + namespace + }) + } + // remove static + await this.removeStatic() + this.state = {} + } + async metrics(inputs = {}) { + console.log(`Getting metrics data`) + if (!inputs.rangeStart || !inputs.rangeEnd) { + throw new error_1.ApiTypeError( + `PARAMETER_${CONFIGS.framework.toUpperCase()}_METRICS`, + 'rangeStart and rangeEnd are require inputs' + ) + } + const { state } = this + const { region } = state + if (!region) { + throw new error_1.ApiTypeError( + `PARAMETER_${CONFIGS.framework.toUpperCase()}_METRICS`, + 'No region property in state' + ) + } + const { functionName, namespace } = state + if (functionName) { + const options = { + funcName: functionName, + namespace: namespace, + region, + timezone: inputs.tz + } + if (state.serviceId) { + options.apigwServiceId = state.serviceId + options.apigwEnvironment = state.environment || 'release' + } + const credentials = this.getCredentials() + const mertics = new tencent_component_toolkit_1.Metrics(credentials, options) + const metricResults = await mertics.getDatas( + inputs.rangeStart, + inputs.rangeEnd, + tencent_component_toolkit_1.Metrics.Type.All + ) + return metricResults + } + throw new error_1.ApiTypeError( + `PARAMETER_${CONFIGS.framework.toUpperCase()}_METRICS`, + 'Function name not define' + ) + } +} +exports.ServerlessComponent = ServerlessComponent diff --git a/src/package.json b/src/package.json index 7b66920..26d0fce 100644 --- a/src/package.json +++ b/src/package.json @@ -2,7 +2,9 @@ "dependencies": { "adm-zip": "^0.4.16", "download": "^8.0.0", - "tencent-component-toolkit": "^1.20.10", + "fs-extra": "^9.1.0", + "js-yaml": "^4.0.0", + "tencent-component-toolkit": "2.23.3", "type": "^2.1.0" } } diff --git a/src/serverless.js b/src/serverless.js index 9378242..8f3f132 100644 --- a/src/serverless.js +++ b/src/serverless.js @@ -1,432 +1,2 @@ -const { Component } = require('@serverless/core') -const { Scf, Apigw, Cns, Cam, Metrics, Cos, Cdn } = require('tencent-component-toolkit') -const { TypeError } = require('tencent-component-toolkit/src/utils/error') -const { - deepClone, - uploadCodeToCos, - getDefaultProtocol, - prepareInputs, - prepareStaticCosInputs, - prepareStaticCdnInputs -} = require('./utils') -const CONFIGS = require('./config') - -class ServerlessComopnent extends Component { - getCredentials() { - const { tmpSecrets } = this.credentials.tencent - - if (!tmpSecrets || !tmpSecrets.TmpSecretId) { - throw new TypeError( - 'CREDENTIAL', - 'Cannot get secretId/Key, your account could be sub-account and does not have the access to use SLS_QcsRole, please make sure the role exists first, then visit https://cloud.tencent.com/document/product/1154/43006, follow the instructions to bind the role to your account.' - ) - } - - return { - SecretId: tmpSecrets.TmpSecretId, - SecretKey: tmpSecrets.TmpSecretKey, - Token: tmpSecrets.Token - } - } - - getAppId() { - return this.credentials.tencent.tmpSecrets.appId - } - - async deployFunction(credentials, inputs, regionList) { - if (!inputs.role) { - try { - const camClient = new Cam(credentials) - const roleExist = await camClient.CheckSCFExcuteRole() - if (roleExist) { - inputs.role = 'QCS_SCFExcuteRole' - } - } catch (e) { - // no op - } - } - - const outputs = {} - const appId = this.getAppId() - - const funcDeployer = async (curRegion) => { - const code = await uploadCodeToCos(this, appId, credentials, inputs, curRegion) - const scf = new Scf(credentials, curRegion) - const tempInputs = { - ...inputs, - code - } - const scfOutput = await scf.deploy(deepClone(tempInputs)) - outputs[curRegion] = { - functionName: scfOutput.FunctionName, - runtime: scfOutput.Runtime, - namespace: scfOutput.Namespace - } - - this.state[curRegion] = { - ...(this.state[curRegion] ? this.state[curRegion] : {}), - ...outputs[curRegion] - } - - // default version is $LATEST - outputs[curRegion].lastVersion = scfOutput.LastVersion - ? scfOutput.LastVersion - : this.state.lastVersion || '$LATEST' - - // default traffic is 1.0, it can also be 0, so we should compare to undefined - outputs[curRegion].traffic = - scfOutput.Traffic !== undefined - ? scfOutput.Traffic - : this.state.traffic !== undefined - ? this.state.traffic - : 1 - - if (outputs[curRegion].traffic !== 1 && scfOutput.ConfigTrafficVersion) { - outputs[curRegion].configTrafficVersion = scfOutput.ConfigTrafficVersion - this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion - } - - this.state.lastVersion = outputs[curRegion].lastVersion - this.state.traffic = outputs[curRegion].traffic - } - - for (let i = 0; i < regionList.length; i++) { - const curRegion = regionList[i] - await funcDeployer(curRegion) - } - this.save() - return outputs - } - - // try to add dns record - async tryToAddDnsRecord(credentials, customDomains) { - try { - const cns = new Cns(credentials) - for (let i = 0; i < customDomains.length; i++) { - const item = customDomains[i] - if (item.domainPrefix) { - await cns.deploy({ - domain: item.subDomain.replace(`${item.domainPrefix}.`, ''), - records: [ - { - subDomain: item.domainPrefix, - recordType: 'CNAME', - recordLine: '默认', - value: item.cname, - ttl: 600, - mx: 10, - status: 'enable' - } - ] - }) - } - } - } catch (e) { - console.log('METHOD_tryToAddDnsRecord', e.message) - } - } - - async deployApigateway(credentials, inputs, regionList) { - if (inputs.isDisabled) { - return {} - } - - const getServiceId = (instance, region) => { - const regionState = instance.state[region] - return inputs.serviceId || (regionState && regionState.serviceId) - } - - const deployTasks = [] - const outputs = {} - regionList.forEach((curRegion) => { - const apigwDeployer = async () => { - const apigw = new Apigw(credentials, curRegion) - - const oldState = this.state[curRegion] || {} - const apigwInputs = { - ...inputs, - oldState: { - apiList: oldState.apiList || [], - customDomains: oldState.customDomains || [] - } - } - // different region deployment has different service id - apigwInputs.serviceId = getServiceId(this, curRegion) - const apigwOutput = await apigw.deploy(deepClone(apigwInputs)) - outputs[curRegion] = { - serviceId: apigwOutput.serviceId, - subDomain: apigwOutput.subDomain, - environment: apigwOutput.environment, - url: `${getDefaultProtocol(inputs.protocols)}://${apigwOutput.subDomain}/${ - apigwOutput.environment - }${apigwInputs.endpoints[0].path}` - } - - if (apigwOutput.customDomains) { - // TODO: need confirm add cns authentication - if (inputs.autoAddDnsRecord === true) { - // await this.tryToAddDnsRecord(credentials, apigwOutput.customDomains) - } - outputs[curRegion].customDomains = apigwOutput.customDomains - } - this.state[curRegion] = { - created: true, - ...(this.state[curRegion] ? this.state[curRegion] : {}), - ...outputs[curRegion], - apiList: apigwOutput.apiList - } - } - deployTasks.push(apigwDeployer()) - }) - - await Promise.all(deployTasks) - - this.save() - return outputs - } - - // deploy static to cos, and setup cdn - async deployStatic(credentials, inputs, region) { - const { zipPath } = this.state - const appId = this.getAppId() - const deployStaticOutpus = {} - - if (zipPath) { - console.log(`Deploy static for ${CONFIGS.compFullname} application`) - // 1. deploy to cos - const { staticCosInputs, bucket } = await prepareStaticCosInputs(this, inputs, appId, zipPath) - - const cos = new Cos(credentials, region) - const cosOutput = { - region - } - // flush bucket - if (inputs.cosConf.replace) { - await cos.flushBucketFiles(bucket) - } - for (let i = 0; i < staticCosInputs.length; i++) { - const curInputs = staticCosInputs[i] - console.log(`Starting deploy directory ${curInputs.src} to cos bucket ${curInputs.bucket}`) - const deployRes = await cos.deploy(curInputs) - cosOutput.cosOrigin = `${curInputs.bucket}.cos.${region}.myqcloud.com` - cosOutput.bucket = deployRes.bucket - cosOutput.url = `https://${curInputs.bucket}.cos.${region}.myqcloud.com` - console.log(`Deploy directory ${curInputs.src} to cos bucket ${curInputs.bucket} success`) - } - deployStaticOutpus.cos = cosOutput - - // 2. deploy cdn - if (inputs.cdnConf) { - const cdn = new Cdn(credentials) - const cdnInputs = await prepareStaticCdnInputs(this, inputs, cosOutput.cosOrigin) - console.log(`Starting deploy cdn ${cdnInputs.domain}`) - const cdnDeployRes = await cdn.deploy(cdnInputs) - const protocol = cdnInputs.https ? 'https' : 'http' - const cdnOutput = { - domain: cdnDeployRes.domain, - url: `${protocol}://${cdnDeployRes.domain}`, - cname: cdnDeployRes.cname - } - deployStaticOutpus.cdn = cdnOutput - - console.log(`Deploy cdn ${cdnInputs.domain} success`) - } - - console.log(`Deployed static for ${CONFIGS.compFullname} application successfully`) - - return deployStaticOutpus - } - - return null - } - - async deploy(inputs) { - console.log(`Deploying ${CONFIGS.compFullname} App...`) - - const credentials = this.getCredentials() - - // 对Inputs内容进行标准化 - const { regionList, functionConf, apigatewayConf } = await prepareInputs( - this, - credentials, - inputs - ) - - // 部署函数 + API网关 - const outputs = {} - if (!functionConf.code.src) { - outputs.templateUrl = CONFIGS.templateUrl - } - - let apigwOutputs - const functionOutputs = await this.deployFunction( - credentials, - functionConf, - regionList, - outputs - ) - // support apigatewayConf.isDisabled - if (apigatewayConf.isDisabled !== true) { - apigwOutputs = await this.deployApigateway(credentials, apigatewayConf, regionList, outputs) - } else { - this.state.apigwDisabled = true - } - - // optimize outputs for one region - if (regionList.length === 1) { - const [oneRegion] = regionList - outputs.region = oneRegion - outputs['scf'] = functionOutputs[oneRegion] - if (apigwOutputs) { - outputs['apigw'] = apigwOutputs[oneRegion] - } - } else { - outputs['scf'] = functionOutputs - if (apigwOutputs) { - outputs['apigw'] = apigwOutputs - } - } - - // start deploy static cdn - if (inputs.staticConf) { - const staticOutputs = {} - const staticTasks = [] - regionList.forEach((region) => { - const staticDeployer = async () => { - const res = await this.deployStatic(credentials, inputs.staticConf, region) - staticOutputs[region] = res - } - staticTasks.push(staticDeployer()) - }) - - await Promise.all(staticTasks) - - if (regionList.length === 1) { - const [oneRegion] = regionList - this.state.staticConf = staticOutputs[oneRegion] - outputs.staticConf = staticOutputs[oneRegion] - } else { - this.state.staticConf = staticOutputs - outputs.staticConf = staticOutputs - } - } - - this.state.region = regionList[0] - this.state.regionList = regionList - - this.state.lambdaArn = functionConf.name - - return outputs - } - - async removeStatic() { - // remove static - const { region, staticConf } = this.state - if (staticConf) { - console.log(`Removing static config`) - const credentials = this.getCredentials() - // 1. remove cos - if (staticConf.cos) { - const cos = new Cos(credentials, region) - await cos.remove(staticConf.cos) - } - // 2. remove cdn - if (staticConf.cdn) { - const cdn = new Cdn(credentials) - try { - await cdn.remove(staticConf.cdn) - } catch (e) { - // no op - } - } - console.log(`Remove static config success`) - } - } - - async remove() { - console.log(`Removing ${CONFIGS.compFullname} App...`) - - const { state } = this - const { regionList = [] } = state - - const credentials = this.getCredentials() - - const removeHandlers = [] - for (let i = 0; i < regionList.length; i++) { - const curRegion = regionList[i] - const curState = state[curRegion] - const scf = new Scf(credentials, curRegion) - const apigw = new Apigw(credentials, curRegion) - const handler = async () => { - // if disable apigw, no need to remove - if (state.apigwDisabled !== true) { - await apigw.remove({ - created: curState.created, - environment: curState.environment, - serviceId: curState.serviceId, - apiList: curState.apiList, - customDomains: curState.customDomains - }) - } - await scf.remove({ - functionName: curState.functionName, - namespace: curState.namespace - }) - } - removeHandlers.push(handler()) - } - - await Promise.all(removeHandlers) - - // remove static - await this.removeStatic() - - this.state = {} - } - - async metrics(inputs = {}) { - console.log(`Get ${CONFIGS.compFullname} Metrics Datas...`) - if (!inputs.rangeStart || !inputs.rangeEnd) { - throw new TypeError( - `PARAMETER_${CONFIGS.compName.toUpperCase()}_METRICS`, - 'rangeStart and rangeEnd are require inputs' - ) - } - const { region } = this.state - if (!region) { - throw new TypeError( - `PARAMETER_${CONFIGS.compName.toUpperCase()}_METRICS`, - 'No region property in state' - ) - } - const { functionName, namespace, functionVersion } = this.state[region] || {} - if (functionName) { - const options = { - funcName: functionName, - namespace: namespace, - version: functionVersion, - region, - timezone: inputs.tz - } - const curState = this.state[region] - if (curState.serviceId) { - options.apigwServiceId = curState.serviceId - options.apigwEnvironment = curState.environment || 'release' - } - const credentials = this.getCredentials() - const mertics = new Metrics(credentials, options) - const metricResults = await mertics.getDatas( - inputs.rangeStart, - inputs.rangeEnd, - Metrics.Type.All - ) - return metricResults - } - throw new TypeError( - `PARAMETER_${CONFIGS.compName.toUpperCase()}_METRICS`, - 'Function name not define' - ) - } -} - -module.exports = ServerlessComopnent +const index_1 = require('./index') +module.exports = index_1.ServerlessComponent diff --git a/src/utils.js b/src/utils.js index 5448137..72c9044 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,57 +1,56 @@ -const path = require('path') -const { Cos } = require('tencent-component-toolkit') -const { TypeError } = require('tencent-component-toolkit/src/utils/error') -const ensureObject = require('type/object/ensure') -const ensureIterable = require('type/iterable/ensure') -const ensureString = require('type/string/ensure') +Object.defineProperty(exports, '__esModule', { value: true }) +exports.getInjection = exports.getCodeZipPath = exports.validateTraffic = exports.removeAppid = exports.getDefaultServiceDescription = exports.getDefaultServiceName = exports.getDefaultFunctionName = exports.getDefaultProtocol = exports.capitalString = exports.getType = exports.deepClone = exports.generateId = exports.sleep = void 0 +const error_1 = require('tencent-component-toolkit/lib/utils/error') const download = require('download') +const fse = require('fs-extra') +const path = require('path') const AdmZip = require('adm-zip') -const CONFIGS = require('./config') - +const config_1 = require('./config') +const CONFIGS = config_1.getConfig() +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true) + }, ms) + }) +} +exports.sleep = sleep const generateId = () => Math.random() .toString(36) .substring(6) - +exports.generateId = generateId const deepClone = (obj) => { return JSON.parse(JSON.stringify(obj)) } - +exports.deepClone = deepClone const getType = (obj) => { return Object.prototype.toString.call(obj).slice(8, -1) } - -const mergeJson = (sourceJson, targetJson) => { - Object.entries(sourceJson).forEach(([key, val]) => { - targetJson[key] = deepClone(val) - }) - return targetJson -} - +exports.getType = getType const capitalString = (str) => { if (str.length < 2) { return str.toUpperCase() } - return `${str[0].toUpperCase()}${str.slice(1)}` } - +exports.capitalString = capitalString const getDefaultProtocol = (protocols) => { return String(protocols).includes('https') ? 'https' : 'http' } - +exports.getDefaultProtocol = getDefaultProtocol const getDefaultFunctionName = () => { - return `${CONFIGS.compName}_component_${generateId()}` + return `${CONFIGS.framework}_${exports.generateId()}` } - +exports.getDefaultFunctionName = getDefaultFunctionName const getDefaultServiceName = () => { return 'serverless' } - +exports.getDefaultServiceName = getDefaultServiceName const getDefaultServiceDescription = () => { return 'Created by Serverless Component' } - +exports.getDefaultServiceDescription = getDefaultServiceDescription const removeAppid = (str, appid) => { const suffix = `-${appid}` if (!str || str.indexOf(suffix) === -1) { @@ -59,426 +58,107 @@ const removeAppid = (str, appid) => { } return str.slice(0, -suffix.length) } - +exports.removeAppid = removeAppid const validateTraffic = (num) => { - if (getType(num) !== 'Number') { - throw new TypeError( - `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, + if (exports.getType(num) !== 'Number') { + throw new error_1.ApiTypeError( + `PARAMETER_${CONFIGS.framework.toUpperCase()}_TRAFFIC`, 'traffic must be a number' ) } if (num < 0 || num > 1) { - throw new TypeError( - `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, + throw new error_1.ApiTypeError( + `PARAMETER_${CONFIGS.framework.toUpperCase()}_TRAFFIC`, 'traffic must be a number between 0 and 1' ) } return true } - -const getCodeZipPath = async (instance, inputs) => { - console.log(`Packaging ${CONFIGS.compFullname} application...`) - +exports.validateTraffic = validateTraffic +const generatePublicDir = (zipPath) => { + const zip = new AdmZip(zipPath) + const entries = zip.getEntries() + const [entry] = entries.filter((e) => e.entryName === 'app/public/' && e.name === '') + if (!entry) { + const extraPublicPath = path.join(__dirname, '_fixtures/public') + zip.addLocalFolder(extraPublicPath, 'app/public') + zip.writeZip() + } +} +const getCodeZipPath = async (inputs) => { + var _a + const { framework } = CONFIGS + console.log(`Packaging ${framework} application`) // unzip source zip file let zipPath - if (!inputs.code.src) { + if (!((_a = inputs.code) === null || _a === void 0 ? void 0 : _a.src)) { // add default template - const downloadPath = `/tmp/${generateId()}` + const downloadPath = `/tmp/${exports.generateId()}` const filename = 'template' - - console.log(`Installing Default ${CONFIGS.compFullname} App...`) + console.log(`Installing Default ${framework} App`) try { await download(CONFIGS.templateUrl, downloadPath, { filename: `${filename}.zip` }) } catch (e) { - throw new TypeError(`DOWNLOAD_TEMPLATE`, 'Download default template failed.') + throw new error_1.ApiTypeError(`DOWNLOAD_TEMPLATE`, 'Download default template failed.') } zipPath = `${downloadPath}/${filename}.zip` } else { zipPath = inputs.code.src } - - return zipPath -} - -/** - * transform export to module.exports in nuxt.config.js - * @param {string} zipPath code zip path - */ -const transformNuxtConfig = (zipPath) => { - const zip = new AdmZip(zipPath) - const entries = zip.getEntries() - const [entry] = entries.filter((e) => e.name === 'nuxt.config.js') - if (entry) { - entry.setData( - Buffer.from( - entry - .getData() - .toString('utf-8') - .replace('export default', 'module.exports =') - ) - ) - } else { - console.log('transformNuxtConfig: nuxt.config.js not exist.') - } - zip.writeZip() -} - -/** - * Upload code to COS - * @param {Component} instance serverless component instance - * @param {string} appId app id - * @param {object} credentials credentials - * @param {object} inputs component inputs parameters - * @param {string} region region - */ -const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => { - const bucketName = inputs.code.bucket || `sls-cloudfunction-${region}-code` - const objectName = inputs.code.object || `${inputs.name}-${Math.floor(Date.now() / 1000)}.zip` - // if set bucket and object not pack code - if (!inputs.code.bucket || !inputs.code.object) { - const zipPath = await getCodeZipPath(instance, inputs) - console.log(`Code zip path ${zipPath}`) - - // transform nuxt.config.js file - // export default -> module.exports = - transformNuxtConfig(zipPath) - - // save the zip path to state for lambda to use it - instance.state.zipPath = zipPath - - const cos = new Cos(credentials, region) - - if (!inputs.code.bucket) { - // create default bucket - await cos.deploy({ - bucket: bucketName + '-' + appId, - force: true, - lifecycle: [ - { - status: 'Enabled', - id: 'deleteObject', - filter: '', - expiration: { days: '10' }, - abortIncompleteMultipartUpload: { daysAfterInitiation: '10' } - } - ] - }) - } - - // upload code to cos - if (!inputs.code.object) { - console.log(`Getting cos upload url for bucket ${bucketName}`) - const uploadUrl = await cos.getObjectUrl({ - bucket: bucketName + '-' + appId, - object: objectName, - method: 'PUT' - }) - - // if shims and sls sdk entries had been injected to zipPath, no need to injected again - console.log(`Uploading code to bucket ${bucketName}`) - if (instance.codeInjected === true) { - await instance.uploadSourceZipToCOS(zipPath, uploadUrl, {}, {}) - } else { - const slsSDKEntries = instance.getSDKEntries('_shims/handler.handler') - await instance.uploadSourceZipToCOS(zipPath, uploadUrl, slsSDKEntries, { - _shims: path.join(__dirname, '_shims') - }) - instance.codeInjected = true - } - console.log(`Upload ${objectName} to bucket ${bucketName} success`) - } - } - - // save bucket state - instance.state.bucket = bucketName - instance.state.object = objectName - - return { - bucket: bucketName, - object: objectName - } -} - -const prepareStaticCosInputs = async (instance, inputs, appId, codeZipPath) => { - try { - const staticCosInputs = [] - const { cosConf } = inputs - const sources = cosConf.sources || CONFIGS.defaultStatics - const { bucket } = cosConf - // remove user append appid - const bucketName = removeAppid(bucket, appId) - const staticPath = `/tmp/${generateId()}` - const codeZip = new AdmZip(codeZipPath) - const entries = codeZip.getEntries() - - // traverse sources, generate static directory and deploy to cos - for (let i = 0; i < sources.length; i++) { - const curSource = sources[i] - const entryName = `${curSource.src}` - let exist = false - entries.forEach((et) => { - if (et.entryName.indexOf(entryName) === 0) { - codeZip.extractEntryTo(et, staticPath, true, true) - exist = true - } - }) - if (exist) { - const cosInputs = { - force: true, - protocol: cosConf.protocol, - bucket: `${bucketName}-${appId}`, - src: `${staticPath}/${entryName}`, - keyPrefix: curSource.targetDir || '/', - acl: { - permissions: 'public-read', - grantRead: '', - grantWrite: '', - grantFullControl: '' - } - } - - if (cosConf.acl) { - cosInputs.acl = { - permissions: cosConf.acl.permissions || 'public-read', - grantRead: cosConf.acl.grantRead || '', - grantWrite: cosConf.acl.grantWrite || '', - grantFullControl: cosConf.acl.grantFullControl || '' - } - } - - staticCosInputs.push(cosInputs) - } - } - return { - bucket: `${bucketName}-${appId}`, - staticCosInputs - } - } catch (e) { - throw new TypeError( - `UTILS_${CONFIGS.compName.toUpperCase()}_prepareStaticCosInputs`, - e.message, - e.stack - ) - } -} - -const prepareStaticCdnInputs = async (instance, inputs, origin) => { - try { - const { cdnConf } = inputs - const cdnInputs = { - async: true, - area: cdnConf.area || 'mainland', - domain: cdnConf.domain, - serviceType: 'web', - origin: { - origins: [origin], - originType: 'cos', - originPullProtocol: 'https' - }, - autoRefresh: true, - ...cdnConf - } - if (cdnConf.https) { - // using these default configs, for making user's config more simple - cdnInputs.forceRedirect = cdnConf.https.forceRedirect || CONFIGS.defaultCdnConf.forceRedirect - if (!cdnConf.https.certId) { - throw new TypeError( - `PARAMETER_${CONFIGS.compName.toUpperCase()}_HTTPS`, - 'https.certId is required' - ) - } - cdnInputs.https = { - ...CONFIGS.defaultCdnConf.https, - ...{ - http2: cdnConf.https.http2 || 'on', - certInfo: { - certId: cdnConf.https.certId - } - } - } - } - if (cdnInputs.autoRefresh) { - cdnInputs.refreshCdn = { - flushType: cdnConf.refreshType || 'delete', - urls: [`http://${cdnInputs.domain}`, `https://${cdnInputs.domain}`] - } - } - - return cdnInputs - } catch (e) { - throw new TypeError( - `UTILS_${CONFIGS.compName.toUpperCase()}_prepareStaticCdnInputs`, - e.message, - e.stack - ) + // 自动注入 public 目录 + if (framework === 'egg') { + generatePublicDir(zipPath) } + return zipPath } - -const prepareInputs = async (instance, credentials, inputs = {}) => { - const tempFunctionConf = inputs.functionConf - ? inputs.functionConf - : inputs.functionConfig - ? inputs.functionConfig - : {} - const fromClientRemark = `tencent-${CONFIGS.compName}` - const regionList = inputs.region - ? typeof inputs.region == 'string' - ? [inputs.region] - : inputs.region - : ['ap-guangzhou'] - - // chenck state function name - const stateFunctionName = - instance.state[regionList[0]] && instance.state[regionList[0]].functionName - const functionConf = Object.assign(tempFunctionConf, { - code: { - src: inputs.src, - bucket: inputs.srcOriginal && inputs.srcOriginal.bucket, - object: inputs.srcOriginal && inputs.srcOriginal.object - }, - name: - ensureString(inputs.functionName, { isOptional: true }) || - stateFunctionName || - getDefaultFunctionName(), - region: regionList, - role: ensureString(tempFunctionConf.role ? tempFunctionConf.role : inputs.role, { - default: '' - }), - handler: ensureString(tempFunctionConf.handler ? tempFunctionConf.handler : inputs.handler, { - default: CONFIGS.handler - }), - runtime: ensureString(tempFunctionConf.runtime ? tempFunctionConf.runtime : inputs.runtime, { - default: CONFIGS.runtime - }), - namespace: ensureString( - tempFunctionConf.namespace ? tempFunctionConf.namespace : inputs.namespace, - { default: CONFIGS.namespace } - ), - description: ensureString( - tempFunctionConf.description ? tempFunctionConf.description : inputs.description, - { - default: CONFIGS.description - } - ), - fromClientRemark, - layers: ensureIterable(tempFunctionConf.layers ? tempFunctionConf.layers : inputs.layers, { - default: [] - }), - cfs: ensureIterable(tempFunctionConf.cfs ? tempFunctionConf.cfs : inputs.cfs, { - default: [] - }), - publish: inputs.publish, - traffic: inputs.traffic, - lastVersion: instance.state.lastVersion, - timeout: tempFunctionConf.timeout ? tempFunctionConf.timeout : CONFIGS.timeout, - memorySize: tempFunctionConf.memorySize ? tempFunctionConf.memorySize : CONFIGS.memorySize, - tags: ensureObject(tempFunctionConf.tags ? tempFunctionConf.tags : inputs.tag, { - default: null - }) - }) - - // validate traffic - if (inputs.traffic !== undefined) { - validateTraffic(inputs.traffic) - } - functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion - - if (tempFunctionConf.environment) { - functionConf.environment = tempFunctionConf.environment - functionConf.environment.variables = functionConf.environment.variables || {} - functionConf.environment.variables.SERVERLESS = '1' - functionConf.environment.variables.SLS_ENTRY_FILE = inputs.entryFile || CONFIGS.defaultEntryFile - } else { - functionConf.environment = { - variables: { - SERVERLESS: '1', - SLS_ENTRY_FILE: inputs.entryFile || CONFIGS.defaultEntryFile - } - } - } - if (tempFunctionConf.vpcConfig) { - functionConf.vpcConfig = tempFunctionConf.vpcConfig - } - - const tempApigwConf = inputs.apigatewayConf - ? inputs.apigatewayConf - : inputs.apigwConfig - ? inputs.apigwConfig - : {} - const apigatewayConf = Object.assign(tempApigwConf, { - serviceId: inputs.serviceId || tempApigwConf.serviceId, - region: regionList, - isDisabled: tempApigwConf.isDisabled === true, - fromClientRemark: fromClientRemark, - serviceName: inputs.serviceName || tempApigwConf.serviceName || getDefaultServiceName(instance), - serviceDesc: tempApigwConf.serviceDesc || getDefaultServiceDescription(instance), - protocols: tempApigwConf.protocols || ['http'], - environment: tempApigwConf.environment ? tempApigwConf.environment : 'release', - customDomains: tempApigwConf.customDomains || [] +exports.getCodeZipPath = getCodeZipPath +const modifyDjangoEntryFile = (projectName, shimPath) => { + console.log(`Modifying django entry file for project ${projectName}`) + const compShimsPath = `/tmp/_shims` + const fixturePath = path.join(__dirname, '_fixtures/python') + fse.copySync(shimPath, compShimsPath) + fse.copySync(fixturePath, compShimsPath) + // replace {{django_project}} in _shims/index.py to djangoProjectName + const indexPath = path.join(compShimsPath, 'sl_handler.py') + const indexPyFile = fse.readFileSync(indexPath, 'utf8') + const replacedFile = indexPyFile.replace(eval('/{{django_project}}/g'), projectName) + fse.writeFileSync(indexPath, replacedFile) + return compShimsPath +} +const getDirFiles = (dirPath) => { + const targetPath = path.resolve(dirPath) + const files = fse.readdirSync(targetPath) + const temp = {} + files.forEach((file) => { + temp[file] = path.join(targetPath, file) }) - if (!apigatewayConf.endpoints) { - apigatewayConf.endpoints = [ - { - path: tempApigwConf.path || '/', - enableCORS: tempApigwConf.enableCORS, - serviceTimeout: tempApigwConf.serviceTimeout, - method: 'ANY', - apiName: tempApigwConf.apiName || 'index', - isBase64Encoded: tempApigwConf.isBase64Encoded, - isBase64Trigger: tempApigwConf.isBase64Trigger, - base64EncodedTriggerRules: tempApigwConf.base64EncodedTriggerRules, - function: { - isIntegratedResponse: true, - functionName: functionConf.name, - functionNamespace: functionConf.namespace, - functionQualifier: - (tempApigwConf.function && tempApigwConf.function.functionQualifier) || '$LATEST' - } - } - ] - } - if (tempApigwConf.usagePlan) { - apigatewayConf.endpoints[0].usagePlan = { - usagePlanId: tempApigwConf.usagePlan.usagePlanId, - usagePlanName: tempApigwConf.usagePlan.usagePlanName, - usagePlanDesc: tempApigwConf.usagePlan.usagePlanDesc, - maxRequestNum: tempApigwConf.usagePlan.maxRequestNum + return temp +} +const getInjection = (instance, inputs) => { + const { framework } = CONFIGS + let injectFiles = {} + let injectDirs = {} + const shimPath = path.join(__dirname, '_shims') + if (CONFIGS.injectSlsSdk) { + injectFiles = instance.getSDKEntries(`_shims/handler.handler`) + injectDirs = { + _shims: shimPath } - } - if (tempApigwConf.auth) { - apigatewayConf.endpoints[0].auth = { - secretName: tempApigwConf.auth.secretName, - secretIds: tempApigwConf.auth.secretIds - } - } - - regionList.forEach((curRegion) => { - const curRegionConf = inputs[curRegion] - if (curRegionConf && curRegionConf.functionConf) { - functionConf[curRegion] = curRegionConf.functionConf + } else if (framework === 'django') { + const djangoShimPath = modifyDjangoEntryFile(inputs.projectName, shimPath) + injectDirs = { + '': djangoShimPath } - if (curRegionConf && curRegionConf.apigatewayConf) { - apigatewayConf[curRegion] = curRegionConf.apigatewayConf + } else if (framework === 'flask') { + injectDirs = { + '': path.join(__dirname, '_fixtures/python') } - }) - - return { - regionList, - functionConf, - apigatewayConf + injectFiles = getDirFiles(shimPath) + } else { + injectFiles = getDirFiles(shimPath) } + return { injectFiles, injectDirs } } - -module.exports = { - deepClone, - generateId, - uploadCodeToCos, - mergeJson, - capitalString, - getDefaultProtocol, - prepareInputs, - prepareStaticCosInputs, - prepareStaticCdnInputs -} +exports.getInjection = getInjection