diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..11c8ac8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +on: + push: + branches: [master] + +jobs: + release: + name: Release + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Install Node.js and npm + uses: actions/setup-node@v1 + with: + node-version: 14.x + registry-url: https://registry.npmjs.org + + - name: Retrieve dependencies from cache + id: cacheNpm + uses: actions/cache@v2 + with: + path: | + ~/.npm + node_modules + key: npm-v14-${{ runner.os }}-refs/heads/master-${{ hashFiles('package.json') }} + restore-keys: npm-v14-${{ runner.os }}-refs/heads/master- + + - name: Install dependencies + if: steps.cacheNpm.outputs.cache-hit != 'true' + run: | + npm update --no-save + npm update --save-dev --no-save + - name: Releasing + run: | + npm run release + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GIT_AUTHOR_NAME: slsplus + GIT_AUTHOR_EMAIL: slsplus.sz@gmail.com + GIT_COMMITTER_NAME: slsplus + GIT_COMMITTER_EMAIL: slsplus.sz@gmail.com diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c966e94 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,47 @@ +name: Test + +on: + pull_request: + branches: [master] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # Ensure connection with 'master' branch + fetch-depth: 2 + + - name: Install Node.js and npm + uses: actions/setup-node@v1 + with: + node-version: 14.x + registry-url: https://registry.npmjs.org + + - name: Retrieve dependencies from cache + id: cacheNpm + uses: actions/cache@v2 + with: + path: | + ~/.npm + node_modules + key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} + restore-keys: | + npm-v14-${{ runner.os }}-${{ github.ref }}- + npm-v14-${{ runner.os }}-refs/heads/master- + + - name: Install dependencies + if: steps.cacheNpm.outputs.cache-hit != 'true' + run: | + npm update --no-save + npm update --save-dev --no-save + - name: Running tests + run: npm run test + env: + SERVERLESS_PLATFORM_VENDOR: tencent + GLOBAL_ACCELERATOR_NA: true + TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} + TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..3840792 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,45 @@ +name: Validate + +on: + pull_request: + branches: [master] + +jobs: + lintAndFormatting: + name: Lint & Formatting + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # Ensure connection with 'master' branch + fetch-depth: 2 + + - name: Install Node.js and npm + uses: actions/setup-node@v1 + with: + node-version: 14.x + registry-url: https://registry.npmjs.org + + - name: Retrieve dependencies from cache + id: cacheNpm + uses: actions/cache@v2 + with: + path: | + ~/.npm + node_modules + key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} + restore-keys: | + npm-v14-${{ runner.os }}-${{ github.ref }}- + npm-v14-${{ runner.os }}-refs/heads/master- + + - name: Install dependencies + if: steps.cacheNpm.outputs.cache-hit != 'true' + run: | + npm update --no-save + npm update --save-dev --no-save + + - name: Validate Formatting + run: npm run prettier:fix + - name: Validate Lint rules + run: npm run lint:fix diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b9f070c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: node_js - -node_js: - - 8 - - 10 - -install: - - npm install - -# should change to serverless registry publish -jobs: - include: - # Define the release stage that runs semantic-release - - stage: release - node_js: 10.18 - # Advanced: optionally overwrite your default `script` step to skip the tests - # script: skip - deploy: - provider: script - skip_cleanup: true - script: - - npm run release diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..96dfed8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,132 @@ +# [0.3.0](https://github.com/serverless-components/tencent-express/compare/v0.2.3...v0.3.0) (2021-01-26) + + +### Features + +* support apigw base64 encode ([78ca266](https://github.com/serverless-components/tencent-express/commit/78ca26613eec65558852ff1b6a130a841b0178f3)) + +## [0.2.3](https://github.com/serverless-components/tencent-express/compare/v0.2.2...v0.2.3) (2020-12-28) + + +### Bug Fixes + +* add sls initialize before creating server ([7f18f2c](https://github.com/serverless-components/tencent-express/commit/7f18f2c5e8e86cc0a9252e5bd036742251964e4d)) + +## [0.2.2](https://github.com/serverless-components/tencent-express/compare/v0.2.1...v0.2.2) (2020-12-15) + + +### Bug Fixes + +* update remove flow ([f766ee6](https://github.com/serverless-components/tencent-express/commit/f766ee69cf39cf410df0203fc71644297ef70693)) + +## [0.2.1](https://github.com/serverless-components/tencent-express/compare/v0.2.0...v0.2.1) (2020-12-15) + + +### Bug Fixes + +* update to deployment to serial flow ([a76fe58](https://github.com/serverless-components/tencent-express/commit/a76fe586846269132f3a9b9141302050b02a13c3)) + +# [0.2.0](https://github.com/serverless-components/tencent-express/compare/v0.1.5...v0.2.0) (2020-10-12) + + +### Bug Fixes + +* support all parameters for apigw ([ec3ffc7](https://github.com/serverless-components/tencent-express/commit/ec3ffc7fb6a6959dbdb734b8a2137f5ac777d05c)) + + +### Features + +* support custom entry file and all scf config ([80e2890](https://github.com/serverless-components/tencent-express/commit/80e28903422c5710a56a8267f64ffe47398ddcf8)) + +## [0.1.5](https://github.com/serverless-components/tencent-express/compare/v0.1.4...v0.1.5) (2020-09-03) + + +### Bug Fixes + +* update deploy flow for multi region ([33ae212](https://github.com/serverless-components/tencent-express/commit/33ae21248a7c191e3ceea2371fd2e69a6b1ebf67)) + +## [0.1.4](https://github.com/serverless-components/tencent-express/compare/v0.1.3...v0.1.4) (2020-09-02) + + +### Bug Fixes + +* update tencnet-component-toolkit for api mark ([7e25e8a](https://github.com/serverless-components/tencent-express/commit/7e25e8a86f539b2db449caf969cbbd58c5af4d1b)) + +## [0.1.3](https://github.com/serverless-components/tencent-express/compare/v0.1.2...v0.1.3) (2020-09-01) + + +### Bug Fixes + +* support cfs ([9d1a1e1](https://github.com/serverless-components/tencent-express/commit/9d1a1e1793d866f5fbe5bdce61090db8381bb103)) +* update deps for error message ([f0a8f89](https://github.com/serverless-components/tencent-express/commit/f0a8f892f469efcda67070ff940421aa83a100c0)) + +## [0.1.2](https://github.com/serverless-components/tencent-express/compare/v0.1.1...v0.1.2) (2020-08-26) + + +### Bug Fixes + +* add apigw/scf deployed state saving manually ([0fcc0b8](https://github.com/serverless-components/tencent-express/commit/0fcc0b84bdeb7119086cdbb050ae2c8ab734ff32)) +* add regionList state ([f66d8d8](https://github.com/serverless-components/tencent-express/commit/f66d8d8ee6564e343442bb59b9996bf23fd475b9)) +* add release config ([26c7e12](https://github.com/serverless-components/tencent-express/commit/26c7e1201caf79830843e860237f3ebf6efdf1e7)) +* apigw custom domain update bug ([1a53027](https://github.com/serverless-components/tencent-express/commit/1a53027effde871dc9f021f510fd7a13cc86794a)) +* apigw isDisabled ([f624171](https://github.com/serverless-components/tencent-express/commit/f624171b927f0efd4605566c6168d67a3f9b8302)) +* bind unexist role bug ([6cd8990](https://github.com/serverless-components/tencent-express/commit/6cd89900111c7afd1e073d95f81f99364debfb96)) +* cache http server ([fc5c824](https://github.com/serverless-components/tencent-express/commit/fc5c82453e2469d66837e1b525d317e811b3115a)) +* change entry to sls.js ([61bd482](https://github.com/serverless-components/tencent-express/commit/61bd4820dfcacefa1338580fa8d3b0f2609ffe4c)) +* cns ([65aa8b8](https://github.com/serverless-components/tencent-express/commit/65aa8b8b42880c9709ff583429899275978b007e)) +* enableCORS for apigw ([a0f59ef](https://github.com/serverless-components/tencent-express/commit/a0f59ef01d5dfe500758adfe2c4f05d7ffc2f22b)) +* handle usageplan & auth undefined ([2a257d5](https://github.com/serverless-components/tencent-express/commit/2a257d564e0968c8db1e0a4030412749cb19e847)) +* make default runtime to 10.15 ([a295bd4](https://github.com/serverless-components/tencent-express/commit/a295bd49a9b48f470a3416026531e840dd2177ac)) +* metics data bug ([6bdf89b](https://github.com/serverless-components/tencent-express/commit/6bdf89bbe3b3665012acee5ca66e08fdff9f05db)) +* metrics qps limit ([dddb9f6](https://github.com/serverless-components/tencent-express/commit/dddb9f607c96d86a3500eddeae248d600e2c9813)) +* monitor timeout bug ([de33311](https://github.com/serverless-components/tencent-express/commit/de333111a692b5bc8d14e9274b81b5b2e5e57e84)) +* optimize outputs for one region ([b3c6964](https://github.com/serverless-components/tencent-express/commit/b3c69646528edfdb8ff0d6455a4dc2b16e13cb81)) +* optimize traffic config outputs ([8545ead](https://github.com/serverless-components/tencent-express/commit/8545ead58183454536ca43c412d42d9be9c05580)) +* package version ([d682b66](https://github.com/serverless-components/tencent-express/commit/d682b66ab7858bb85c29548f90514056f8c34749)) +* prettier config ([e17bd6f](https://github.com/serverless-components/tencent-express/commit/e17bd6fe939666f7d8c5c89182ed24125409e068)) +* read bucket and object from srcOriginal ([1b2cf9d](https://github.com/serverless-components/tencent-express/commit/1b2cf9d7c890a94e7deaa830814b9758ac42cdf4)) +* release v0.0.1 for v2 ([52b2a37](https://github.com/serverless-components/tencent-express/commit/52b2a37a9f57550d48af868b6d53ac3a9c020fd5)) +* remove credential bug ([bca2d1f](https://github.com/serverless-components/tencent-express/commit/bca2d1f9c6ab79ee7b29844fe8bdf2085ddac94c)) +* role check error ([1a0b4bf](https://github.com/serverless-components/tencent-express/commit/1a0b4bf3ec1fed5492d72f07378ca7b5d26bbfe8)) +* support apigw endpoint timeout config ([01dd4c9](https://github.com/serverless-components/tencent-express/commit/01dd4c931235ec5547ce5f34e0997beb4e094fd1)) +* support eip config ([62c597f](https://github.com/serverless-components/tencent-express/commit/62c597fdef0a8e296b1943dba13baa60725afd53)) +* template url output ([88df485](https://github.com/serverless-components/tencent-express/commit/88df48503bd1f106e451ce102ec5bd16d6a1442a)) +* throw error when no temp secrets ([2a9167e](https://github.com/serverless-components/tencent-express/commit/2a9167e98a9648fb3ed7264956b10ea2808ce490)) +* traffic zero display bug ([0285406](https://github.com/serverless-components/tencent-express/commit/0285406c812284bd8b5847f0c201c9b69761c9d6)) +* uniform throw error ([c09b6f5](https://github.com/serverless-components/tencent-express/commit/c09b6f5b0071a2fb6a3251b0f8f31bc0dd614d3b)) +* update deploy code ([90a4339](https://github.com/serverless-components/tencent-express/commit/90a433968502f9d1499534cf852c6d4c3db12ba6)) +* update deps ([2e551db](https://github.com/serverless-components/tencent-express/commit/2e551db26dcfa02adf5aa2c63d6c08fea771ed00)) +* update deps ([c14e8f1](https://github.com/serverless-components/tencent-express/commit/c14e8f1492fa8ca7e3bedbc8018571556aa0c6ff)) +* update error message ([c4c7243](https://github.com/serverless-components/tencent-express/commit/c4c724358fe5be700e596929b8bddcfa94faa6fd)) +* update get credential error message ([ed47b3d](https://github.com/serverless-components/tencent-express/commit/ed47b3d759944e41b0c8fa7641594450d88704de)) +* update toolkit verison ([2e99bc7](https://github.com/serverless-components/tencent-express/commit/2e99bc73c438318d125d50d959bf3f9de4a6e85e)) +* update usageplan & auth logic ([afa8807](https://github.com/serverless-components/tencent-express/commit/afa8807792aa0e69c191391b1fb49b059add3c35)) +* upgrade deps ([6a1f7a8](https://github.com/serverless-components/tencent-express/commit/6a1f7a86d4838d02cef19de37a2f6e9714ee640b)) +* upgrade deps ([d5c013b](https://github.com/serverless-components/tencent-express/commit/d5c013b6a04487a29b9848442d31cc71c6230c5f)) +* upgrade tencent-component-toolkit ([4ba2edf](https://github.com/serverless-components/tencent-express/commit/4ba2edf8570e511b9fd5a8286ae81044b531c094)) +* upgrade tencent-component-toolkit ([af8c234](https://github.com/serverless-components/tencent-express/commit/af8c234622058253a21ebe0231796dd7a47902f8)) +* upgrade tencent-component-toolkit ([bdd5062](https://github.com/serverless-components/tencent-express/commit/bdd5062f6501fbe91e167ce3e7a91da2e8eb4acd)) +* upgrade tencent-component-toolkit ([00f4a09](https://github.com/serverless-components/tencent-express/commit/00f4a0993d04d4a7be3cf8ea15381a6d8ff04c98)) +* upgrade tencent-component-toolkit for deleting compatibility ([fa215bf](https://github.com/serverless-components/tencent-express/commit/fa215bfb66264c360c24a3aca68f4b63cb4c60fd)) +* wrong region parameter bug ([#31](https://github.com/serverless-components/tencent-express/issues/31)) ([32e519c](https://github.com/serverless-components/tencent-express/commit/32e519ca00170ed75f0bea19b80c0255288e8d37)) +* 增加cns ([b452285](https://github.com/serverless-components/tencent-express/commit/b45228501a381dd6ede9381ca8d05cd5f403d585)) +* 增加cns ([a2b87d9](https://github.com/serverless-components/tencent-express/commit/a2b87d92908935dc4299dfb0d5b01029c5fc8649)) + + +### Features + +* add binary types support ([60603d6](https://github.com/serverless-components/tencent-express/commit/60603d61bbeb1cbdcf44d5bb6b9cd5a6f2b08fd6)) +* add integration test ([7c28271](https://github.com/serverless-components/tencent-express/commit/7c2827162f94b817f5e8f3c00e279263bff120e7)) +* add layers config support ([d971be9](https://github.com/serverless-components/tencent-express/commit/d971be985daf3bc4127c6648fd0e474c535ad1eb)) +* add metrics api ([#27](https://github.com/serverless-components/tencent-express/issues/27)) ([0d40154](https://github.com/serverless-components/tencent-express/commit/0d40154347b5db735fcac66a8a9b5ccf0e6ca035)) +* add state store, and remove method ([8815e40](https://github.com/serverless-components/tencent-express/commit/8815e4045cf7896e8843c017abb16b24720caa79)) +* optimize code zip flow ([f2d60ce](https://github.com/serverless-components/tencent-express/commit/f2d60ce03e96257f4aacda464ab83a4dc144fecb)) +* optimize deploy log ([80a2bc0](https://github.com/serverless-components/tencent-express/commit/80a2bc0a23a08051ede3c5d54657833499e0d2fc)) +* optimize metics and support disable apigw creating ([e722c80](https://github.com/serverless-components/tencent-express/commit/e722c80d3d1fce8315e08a86808c131402a7a81b)) +* support api gw metrics ([cc625ad](https://github.com/serverless-components/tencent-express/commit/cc625ad3e8a410b161b4744813685aa42a700898)) +* support role config ([f1d6ed6](https://github.com/serverless-components/tencent-express/commit/f1d6ed664b7354376f5791bd162113cdd44b7b5b)) +* support scf publish version and traffic setup ([8807d0e](https://github.com/serverless-components/tencent-express/commit/8807d0ef0fd8c560b3c7aaa8fa37165473a5f863)) +* update config and support usageplan+auth ([b067a14](https://github.com/serverless-components/tencent-express/commit/b067a149acec78d97f701aea893e95ad3833f7c3)) +* update event and context attach method ([31d0c1d](https://github.com/serverless-components/tencent-express/commit/31d0c1de42354103a800e0515f9a069971b357df)) +* use metrics api from toolkit library([#32](https://github.com/serverless-components/tencent-express/issues/32)) ([4b4742d](https://github.com/serverless-components/tencent-express/commit/4b4742d7e53a04fbbd7845073a0ebd2031a1dcd6)) +* using temporary secret for credentials ([cf61300](https://github.com/serverless-components/tencent-express/commit/cf61300f7d94f3149648a0d3378b8876a440aca1)) diff --git a/README.md b/README.md index 651a98e..9fb1b9d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +⚠️⚠️⚠️ 所有框架组件项目迁移到 [tencent-framework-components](https://github.com/serverless-components/tencent-framework-components). + [![Serverless Components](https://img.serverlesscloud.cn/2020210/1581352135771-express.png)](http://serverless.com)
@@ -17,8 +19,6 @@
- - 快速开始: 1. [**安装**](#1-安装) @@ -49,35 +49,31 @@ $ npm install -g serverless 通过如下命令和模板链接,快速创建一个 Express 应用: ```bash -$ serverless create --template-url https://github.com/serverless-components/tencent-express/tree/v2/serverless-express -$ cd serverless-express -``` - -执行如下命令,安装 Express 应用的对应依赖 - -``` -$ npm install +$ serverless init express-starter --name example +$ cd example ``` ### 3. 部署 -在 `serverless.yml` 文件下的目录中运行 `serverless deploy` 进行 Express 项目的部署。第一次部署可能耗时相对较久,但后续的二次部署会在几秒钟之内完成。部署完毕后,你可以在命令行的输出中查看到你 Express 应用的 URL 地址,点击地址即可访问你的 Express 项目。 +在 `serverless.yml` 文件所在的项目根目录,运行以下指令进行部署: -**注意:** +```bash +$ serverless deploy +``` -如您的账号未[登陆](https://cloud.tencent.com/login)或[注册](https://cloud.tencent.com/register)腾讯云,您可以直接通过`微信`扫描命令行中的二维码进行授权登陆和注册。 +部署时需要进行身份验证,如您的账号未 [登陆](https://cloud.tencent.com/login) 或 [注册](https://cloud.tencent.com/register) 腾讯云,您可以直接通过 `微信` 扫描命令行中的二维码进行授权登陆和注册。 -如果出现了 `internal server error` 的报错,请检查是否在创建模板后没有运行 `npm install`。 +> 注意: 如果希望查看更多部署过程的信息,可以通过`serverless deploy --debug` 命令查看部署过程中的实时日志信息。 -如果希望查看更多部署过程的信息,可以通过`sls deploy --debug` 命令查看部署过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 +部署完成后,控制台会打印相关的输出信息,您可以通过 `${output:${stage}:${app}:apigw.url}` 的形式在其他 `serverless` 组件中引用该组件的 API 网关访问链接(或通过类似的形式引用该组建其他输出结果),具体的,可以查看完成的输出文档: -
+- [点击此处查看输出文档](https://github.com/serverless-components/tencent-express/tree/master/docs/output.md) ### 4. 配置 Express 组件支持 0 配置部署,也就是可以直接通过配置文件中的默认值进行部署。但你依然可以修改更多可选配置来进一步开发该 Express 项目。 -以下是 Express 组件的 `serverless.yml`完整配置说明: +以下是 Express 组件的 `serverless.yml`配置示例: ```yml # serverless.yml @@ -89,12 +85,13 @@ app: appDemo # (optional) serverless dashboard app. default is the same as the n stage: dev # (optional) serverless dashboard stage. default is dev. inputs: - src: ./ # (optional) path to the source folder. default is a hello world app. + src: + src: ./ # (optional) path to the source folder. default is a hello world app. + exclude: + - .env functionName: expressDemo region: ap-guangzhou runtime: Nodejs10.15 - exclude: - - .env apigatewayConf: protocols: - http @@ -102,7 +99,7 @@ inputs: environment: release ``` -点此查看[全量配置及配置说明](https://github.com/serverless-components/tencent-express/blob/v2/docs/configure.md) +点此查看[全量配置及配置说明](https://github.com/serverless-components/tencent-express/tree/master/docs/configure.md) 当你根据该配置文件更新配置字段后,再次运行 `serverless deploy` 或者 `serverless` 就可以更新配置到云端。 @@ -132,7 +129,7 @@ $ serverless info $ serverless remove ``` -和部署类似,支持通过 `sls remove --debug` 命令查看移除过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 +和部署类似,支持通过 `serverless remove --debug` 命令查看移除过程中的实时日志信息。 ## 架构说明 @@ -164,6 +161,52 @@ TENCENT_SECRET_ID=123 TENCENT_SECRET_KEY=123 ``` +## 静态资源服务 + +如果想要支持返回静态资源,比如图片之类的,需要在入口文件 `sls.js` 中指定相关 `MIME` 类型的文件为二进制,这样云函数在返回请求结果给 API 网关是,会对指定类型进行 `Base64` 编码,最终返回给客户端才能正常显示。如下: + +```js +const express = require('express') +const app = express() + +// Routes +// ... + +app.binaryTypes = ['*/*'] + +module.exports = app +``` + +`['*/*']` 代表所有文件类型将进行 `Base64` 编码,如果需要定制化,可以配置为 `['image/png']`,意思是指定 `png` 格式的图片类型。 + +更多文件类型的 `MIME` 类型,可参考 [mime-db](https://github.com/jshttp/mime-db/blob/master/db.json)。 + +### slsInitialize 应用初始化 + +有些时候,Express 服务在启动前,需要进行一个初始化操作,比如数据库建连,就可以通过在 Express 实例对象上添加 `slsInitialize` 函数来实现,如下: + +```js +const express = require('express') +const mysql = require('mysql2/promise') + +const app = new express() + +// ... + +app.slsInitialize = async () => { + app.db = await mysql.createConnection({ + host: 'localhost', + user: 'root', + database: 'test' + }) +} + +// don't forget to export! +module.exports = app +``` + +这样应用部署到云函数后,在函数服务逻辑执行前,会先执行 `slsInitialize()` 函数,来初始化数据库连接。 + ## License MIT License diff --git a/__tests__/index.test.js b/__tests__/index.test.js new file mode 100644 index 0000000..2e67e22 --- /dev/null +++ b/__tests__/index.test.js @@ -0,0 +1,62 @@ +const { join } = require('path'); +require('dotenv').config({ path: join(__dirname, '.env.test') }); + +const { generateId, getServerlessSdk } = require('./lib/utils') +const execSync = require('child_process').execSync +const path = require('path') +const axios = require('axios') + +const instanceYaml = { + org: 'orgDemo', + app: 'appDemo', + component: 'express@dev', + name: `express-integration-tests-${generateId()}`, + stage: 'dev', + inputs: { + region: 'ap-guangzhou', + runtime: 'Nodejs10.15', + apigatewayConf: { environment: 'test' } + } +} + +const credentials = { + tencent: { + SecretId: process.env.TENCENT_SECRET_ID, + SecretKey: process.env.TENCENT_SECRET_KEY, + } +} + +const sdk = getServerlessSdk(instanceYaml.org) + +it('should successfully deploy express app', async () => { + const instance = await sdk.deploy(instanceYaml, credentials) + + expect(instance).toBeDefined() + expect(instance.instanceName).toEqual(instanceYaml.name) + expect(instance.outputs.templateUrl).toBeDefined() + expect(instance.outputs.region).toEqual(instanceYaml.inputs.region) + expect(instance.outputs.apigw).toBeDefined() + expect(instance.outputs.apigw.environment).toEqual(instanceYaml.inputs.apigatewayConf.environment) + expect(instance.outputs.scf).toBeDefined() + expect(instance.outputs.scf.runtime).toEqual(instanceYaml.inputs.runtime) +}) + +it('should successfully update source code', async () => { + // change source to own source './src' and need to install packages before deploy + const srcPath = path.join(__dirname, '..', 'example') + execSync('npm install', { cwd: srcPath }) + instanceYaml.inputs.src = srcPath + + const instance = await sdk.deploy(instanceYaml, credentials) + const response = await axios.get(instance.outputs.apigw.url) + + expect(response.data.includes('Serverless Framework')).toBeTruthy() + expect(instance.outputs.templateUrl).not.toBeDefined() +}) + +it('should successfully remove express app', async () => { + await sdk.remove(instanceYaml, credentials) + result = await sdk.getInstance(instanceYaml.org, instanceYaml.stage, instanceYaml.app, instanceYaml.name) + + expect(result.instance.instanceStatus).toEqual('inactive') +}) diff --git a/__tests__/lib/utils.js b/__tests__/lib/utils.js new file mode 100644 index 0000000..d047afa --- /dev/null +++ b/__tests__/lib/utils.js @@ -0,0 +1,24 @@ +const { ServerlessSDK } = require('@serverless/platform-client-china') + +/* + * Generate random id + */ +const generateId = () => + Math.random() + .toString(36) + .substring(6) + +/* + * Initializes and returns an instance of the serverless sdk + * @param ${string} orgName - the serverless org name. + */ +const getServerlessSdk = (orgName) => { + const sdk = new ServerlessSDK({ + context: { + orgName + } + }) + return sdk +} + +module.exports = { generateId, getServerlessSdk } diff --git a/docs/configure.md b/docs/configure.md index 28f4c0e..392f030 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -17,6 +17,7 @@ inputs: serviceName: mytest # api网关服务名称 runtime: Nodejs10.15 # 运行环境 serviceId: service-np1uloxw # api网关服务ID + entryFile: sls.js # 自定义 server 的入口文件名,默认为 sls.js,如果不想修改文件名为 sls.js 可以自定义 src: ./src # 第一种为string时,会打包src对应目录下的代码上传到默认cos上。 # src: # 第二种,部署src下的文件代码,并打包成zip上传到bucket上 # src: ./src # 本地需要打包的文件目录 @@ -30,9 +31,9 @@ inputs: layers: - name: layerName # layer名称 version: 1 # 版本 - traffic: 0.9 # 配置默认流量中 $LATEST 版本比重:0 - 1 functionConf: # 函数配置相关 timeout: 10 # 超时时间,单位秒 + eip: false # 是否固定出口IP memorySize: 128 # 内存大小,单位MB environment: # 环境变量 variables: # 环境变量数组 @@ -46,6 +47,8 @@ inputs: customDomains: # 自定义域名绑定 - domain: abc.com # 待绑定的自定义的域名 certificateId: abcdefg # 待绑定自定义域名的证书唯一 ID + # 如要设置自定义路径映射,请设置为 false + isDefaultMapping: false # 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 pathMappingSet: - path: / @@ -58,6 +61,7 @@ inputs: - https environment: test serviceTimeout: 15 + isBase64Encoded: false usagePlan: # 用户使用计划 usagePlanId: 1111 usagePlanName: slscmp @@ -73,20 +77,20 @@ inputs: 主要的参数 -| 参数名称 | 是否必选 | 默认值 | 描述 | -| ------------------------------------ | :------: | :-------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| runtime | 否 | Nodejs10.15 | 执行环境, 目前支持: Nodejs6.10, Nodejs8.9, Nodejs10.15, Nodejs12.16.16 | -| region | 否 | ap-guangzhou | 项目部署所在区域,默认广州区 | -| functionName | 否 | | 云函数名称 | -| serviceName | 否 | | API 网关服务名称, 默认创建一个新的服务名称 | -| serviceId | 否 | | API 网关服务 ID,如果存在将使用这个 API 网关服务 | -| src | 否 | `process.cwd()` | 默认为当前目录, 如果是对象, 配置参数参考 [执行目录](#执行目录) | -| layers | 否 | | 云函数绑定的 layer, 配置参数参考 [层配置](#层配置) | -| traffic | 否 | 1 | 配置默认流量中 `$LATEST` 版本比重,取值范围:0 ~ 1,比如 80%,可配置成 0.8。注意如果皮遏制灰度流量,需要配置对应的 API 网关触发器的 endpoints 的 `function.functionQualifier` 参数为 `$DEFAULT` (默认流量) | -| [functionConf](#函数配置) | 否 | | 函数配置 | -| [apigatewayConf](#API-网关配置) | 否 | | API 网关配置 | -| [cloudDNSConf](#DNS-配置) | 否 | | DNS 配置 | -| [Region special config](#指定区配置) | 否 | | 指定区配置 | +| 参数名称 | 必选 | 默认值 | 描述 | +| ------------------------------------ | :--: | :-------------: | :------------------------------------------------------------------ | +| runtime | 否 | `Nodejs10.15` | 执行环境, 目前支持: Nodejs6.10, Nodejs8.9, Nodejs10.15, Nodejs12.16 | +| region | 否 | `ap-guangzhou` | 项目部署所在区域,默认广州区 | +| functionName | 否 | | 云函数名称 | +| serviceName | 否 | | API 网关服务名称, 默认创建一个新的服务名称 | +| serviceId | 否 | | API 网关服务 ID,如果存在将使用这个 API 网关服务 | +| entryFile | 否 | `sls.js` | 自定义 server 的入口文件名 | +| src | 否 | `process.cwd()` | 默认为当前目录, 如果是对象, 配置参数参考 [执行目录](#执行目录) | +| layers | 否 | | 云函数绑定的 layer, 配置参数参考 [层配置](#层配置) | +| [functionConf](#函数配置) | 否 | | 函数配置 | +| [apigatewayConf](#API-网关配置) | 否 | | API 网关配置 | +| [cloudDNSConf](#DNS-配置) | 否 | | DNS 配置 | +| [Region special config](#指定区配置) | 否 | | 指定区配置 | ## 执行目录 @@ -110,7 +114,7 @@ inputs: | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | | ---------- | :------: | -------- | :----: | :---------------------------------------------- | -| ttl | 否 | Number | 600 | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | +| ttl | 否 | Number | `600` | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | | recordLine | 否 | String[] | | 记录的线路名称 | ### 指定区配置 @@ -125,12 +129,13 @@ inputs: 参考: https://cloud.tencent.com/document/product/583/18586 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| ----------- | :------: | :----: | :----: | :------------------------------------------------------------------------------ | -| timeout | 否 | Number | 3 | 函数最长执行时间,单位为秒,可选值范围 1-900 秒,默认为 3 秒 | -| memorySize | 否 | Number | 128 | 函数运行时内存大小,默认为 128M,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | -| environment | 否 | Object | | 函数的环境变量, 参考 [环境变量](#环境变量) | -| vpcConfig | 否 | Object | | 函数的 VPC 配置, 参考 [VPC 配置](#VPC-配置) | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| ----------- | :------: | :-----: | :-----: | :------------------------------------------------------------------------------ | +| timeout | 否 | Number | `3` | 函数最长执行时间,单位为秒,可选值范围 1-900 秒,默认为 3 秒 | +| memorySize | 否 | Number | `128` | 函数运行时内存大小,默认为 128M,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | +| environment | 否 | Object | | 函数的环境变量, 参考 [环境变量](#环境变量) | +| vpcConfig | 否 | Object | | 函数的 VPC 配置, 参考 [VPC 配置](#VPC-配置) | +| eip | 否 | Boolean | `false` | 是否固定出口 IP | ##### 环境变量 @@ -147,16 +152,17 @@ inputs: ### API 网关配置 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| -------------- | :------: | :------- | :------- | :--------------------------------------------------------------------------------- | -| protocols | 否 | String[] | ['http'] | 前端请求的类型,如 http,https,http 与 https | -| environment | 否 | String | release | 发布环境. 目前支持三种发布环境: test(测试), prepub(预发布) 与 release(发布). | -| usagePlan | 否 | | | 使用计划配置, 参考 [使用计划](#使用计划) | -| auth | 否 | | | API 密钥配置, 参考 [API 密钥](#API-密钥配置) | -| customDomain | 否 | Object[] | | 自定义 API 域名配置, 参考 [自定义域名](#自定义域名) | -| enableCORS | 否 | Boolean | `false` | 开启跨域。默认值为否。 | -| serviceTimeout | 否 | Number | `15` | Api 超时时间,单位: 秒 | -| isDisabled | 否 | Boolean | `false` | 关闭自动创建 API 网关功能。默认值为否,即默认自动创建 API 网关。 | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| --------------- | :------: | :------- | :--------- | :--------------------------------------------------------------------------------- | +| protocols | 否 | String[] | `['http']` | 前端请求的类型,如 http,https,http 与 https | +| environment | 否 | String | `release` | 发布环境. 目前支持三种发布环境: test(测试), prepub(预发布) 与 release(发布). | +| usagePlan | 否 | | | 使用计划配置, 参考 [使用计划](#使用计划) | +| auth | 否 | | | API 密钥配置, 参考 [API 密钥](#API-密钥配置) | +| customDomain | 否 | Object[] | | 自定义 API 域名配置, 参考 [自定义域名](#自定义域名) | +| enableCORS | 否 | Boolean | `false` | 开启跨域。默认值为否。 | +| serviceTimeout | 否 | Number | `15` | Api 超时时间,单位: 秒 | +| isDisabled | 否 | Boolean | `false` | 关闭自动创建 API 网关功能。默认值为否,即默认自动创建 API 网关。 | +| isBase64Encoded | 否 | Boolean | `false` | 是否开启 Base64 编码,如果需要文件上传,请配置为 `true` | ##### 使用计划 @@ -167,7 +173,7 @@ inputs: | usagePlanId | 否 | String | 用户自定义使用计划 ID | | usagePlanName | 否 | String | 用户自定义的使用计划名称 | | usagePlanDesc | 否 | String | 用户自定义的使用计划描述 | -| maxRequestNum | 否 | Int | 请求配额总数,如果为空,将使用-1 作为默认值,表示不开启 | +| maxRequestNum | 否 | Number | 请求配额总数,如果为空,将使用-1 作为默认值,表示不开启 | ##### API 密钥配置 @@ -182,13 +188,13 @@ inputs: Refer to: https://cloud.tencent.com/document/product/628/14906 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| ---------------- | :------: | :------: | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| domain | 是 | String | | 待绑定的自定义的域名。 | -| certificateId | 否 | String | | 待绑定自定义域名的证书唯一 ID,如果设置了 type 为 https,则为必选 | -| isDefaultMapping | 否 | String | `'TRUE'` | 是否使用默认路径映射,默认为 TRUE。为 FALSE 时,表示自定义路径映射,此时 pathMappingSet 必填。 | -| pathMappingSet | 否 | Object[] | `[]` | 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 | -| protocol | 否 | String[] | | 绑定自定义域名的协议类型,默认与服务的前端协议一致。 | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| ---------------- | :------: | :------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| domain | 是 | String | | 待绑定的自定义的域名。 | +| certificateId | 否 | String | | 待绑定自定义域名的证书唯一 ID,如果设置了 type 为 `https`,则为必选 | +| isDefaultMapping | 否 | String | `true` | 是否使用默认路径映射。为 `false` 时,表示自定义路径映射,此时 pathMappingSet 必填。 | +| pathMappingSet | 否 | Object[] | `[]` | 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 | +| protocol | 否 | String[] | | 绑定自定义域名的协议类型,默认与服务的前端协议一致。 | - 自定义路径映射 diff --git a/docs/output.md b/docs/output.md new file mode 100644 index 0000000..f9fdd99 --- /dev/null +++ b/docs/output.md @@ -0,0 +1,51 @@ +# 部署 `output` 参数介绍 + +> 组件输出可以在别的组件中通过 `${output:${stage}:${app}:.}` 获取 +> +> 例如,如果该组件名称是 `test_name`, ·且只部署于一个地域,则可以通过 `${output:${stage}:${app}:test_name.apigw.url}` 在别的组件中获取该组件的 API 网关的 `url`。 + +| 名称 | 类型 | 描述 | +| :---------- | :------------------------------------------------------------------------------: | :------------------------------- | +| templateUrl | string | 未提供代码时的模板代码 url | +| region | string | 地域信息(只有一个地域时才提供) | +| scf | [`FunctionOutput | Record`](#云函数输出-`FunctionOutput`) | 云函数输出信息 | +| apigw | [`ApigwOutput | Record`](#API-网关输出-`ApigwOutput`) | API 网关输出信息 | + +## 云函数输出 `FunctionOutput` + +| 名称 | 类型 | 描述 | +| :------------------- | :------------: | :--------------------- | +| functionName | string | 云函数名称 | +| runtime | string | 云运行环境 | +| namespace | string | 云函数名称空间 | +| lastVersion | string | 云函数版本 | +| traffic | `number (0~1)` | 将多少流量导向该云函数 | +| configTrafficVersion | string | | + +## API 网关输出 `ApigwOutput` + +| 名称 | 类型 | 描述 | +| :------------ | :------------------------------------------------------------------: | :------------------------- | +| serviceId | string | API 网关 ID | +| subDomain | string | API 网关子域名 | +| enviroment | `"release" | "prepub" | "test"` | API 网关 | +| url | string | API 网关对外的完整 URL | +| traffic | number (0~1) | 将多少流量导向该云函数 | +| customDomains | [CustomDomain[]](#API-网关自定义域名输出-`ApigwOutput.CustomDomain`) | API 网关自定义域名输出列表 | + +## API 网关自定义域名输出 `ApigwOutput.CustomDomain` + +| 名称 | 类型 | 描述 | +| :--------------- | :----------------------------------------------------------------: | :------------------------- | +| domain | string | 自定义域名 | +| certificateId | string | 域名证书 ID | +| isDefaultMapping | boolean | 该自定义域名是否为默认域名 | +| pathMappingSet | [PathMapping[]](#-API-网关域名映射规则-`CustomDomain.PathMapping`) | 该域名的路径映射规则列表 | +| protocols | `"http" | "https"` | 启用的协议 | + +## API 网关域名映射规则 `CustomDomain.PathMapping` + +| 名称 | 类型 | 描述 | +| :--------- | :----: | :--------------- | +| path | string | 路径 | +| enviroment | string | 路径映射到的环境 | diff --git a/docs/upload.md b/docs/upload.md new file mode 100644 index 0000000..2e5e68d --- /dev/null +++ b/docs/upload.md @@ -0,0 +1,31 @@ +## 文件上传说明 + +项目中如果涉及到文件上传,需要依赖 API 网关提供的 [Base64 编码能力](https://cloud.tencent.com/document/product/628/51799),使用时只需要 `serverless.yml` 中配置 `isBase64Encoded` 为 `true`,如下: + +```yaml +app: appDemo +stage: dev +component: express +name: expressDemo + +inputs: + # 省略... + apigatewayConf: + isBase64Encoded: true + # 省略... + # 省略... +``` + +当前 API 网关支持上传最大文件大小为 `2M`,如果文件过大,请修改为前端直传对象存储方案。 + +## Base64 示例 + +此 Github 项目的 `example` 目录下存在模板文件: + +- [sls.upload.js](../example/sls.upload.js) + +开发者可根据个人项目需要参考修改,使用时需要复制文件名为 `sls.js`。 + +文件中实现了文件上传接口 `POST /upload`,如果要支持文件上传,需要安装 `multer` 包。 + +同时需要在 `serverless.yml` 的 `apigatewayConf` 中配置 `isBase64Encoded` 为 `true`。 diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..225e894 --- /dev/null +++ b/example/README.md @@ -0,0 +1,172 @@ +[![Serverless Components](https://img.serverlesscloud.cn/2020210/1581352135771-express.png)](http://serverless.com) + +
+ +**腾讯云 Express 组件** ⎯⎯⎯ 通过使用 [Tencent Serverless Framework](https://github.com/serverless/components/tree/cloud),基于云上 Serverless 服务(如网关、云函数等),实现“0”配置,便捷开发,极速部署你的 Express 应用,Express 组件支持丰富的配置扩展,提供了目前最易用、低成本并且弹性伸缩的 Express 项目开发/托管能力。 +
+ +特性介绍: + +- [x] **按需付费** - 按照请求的使用量进行收费,没有请求时无需付费 +- [x] **"0"配置** - 只需要关心项目代码,之后部署即可,Serverless Framework 会搞定所有配置。 +- [x] **极速部署** - 仅需几秒,部署你的整个 Express 应用。 +- [x] **实时日志** - 通过实时日志的输出查看业务状态,便于直接在云端开发应用。 +- [x] **云端调试** - 针对 Node.js 框架支持一键云端调试能力,屏蔽本地环境的差异。 +- [x] **便捷协作** - 通过云端的状态信息和部署日志,方便的进行多人协作开发。 +- [x] **自定义域名** - 支持配置自定义域名及 HTTPS 访问 + +
+ + + +快速开始: + +1. [**安装**](#1-安装) +2. [**创建**](#2-创建) +3. [**部署**](#3-部署) +4. [**配置**](#4-配置) +5. [**开发调试**](#5-开发调试) +6. [**查看状态**](#6-查看状态) +7. [**移除**](#7-移除) + +更多资源: + +- [**架构说明**](#架构说明) +- [**账号配置**](#账号配置) + +  + +### 1. 安装 + +通过 npm 安装最新版本的 Serverless Framework + +```bash +$ npm install -g serverless +``` + +### 2. 创建 + +通过如下命令和模板链接,快速创建一个 Express 应用: + +```bash +$ serverless create --template-url https://github.com/serverless-components/tencent-express/tree/master/example +$ cd example +``` + +执行如下命令,安装 Express 应用的对应依赖 + +``` +$ npm install +``` + +### 3. 部署 + +在 `serverless.yml` 文件下的目录中运行 `serverless deploy` 进行 Express 项目的部署。第一次部署可能耗时相对较久,但后续的二次部署会在几秒钟之内完成。部署完毕后,你可以在命令行的输出中查看到你 Express 应用的 URL 地址,点击地址即可访问你的 Express 项目。 + +**注意:** + +如您的账号未[登陆](https://cloud.tencent.com/login)或[注册](https://cloud.tencent.com/register)腾讯云,您可以直接通过`微信`扫描命令行中的二维码进行授权登陆和注册。 + +如果出现了 `internal server error` 的报错,请检查是否在创建模板后没有运行 `npm install`。 + +如果希望查看更多部署过程的信息,可以通过`sls deploy --debug` 命令查看部署过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 + +
+ +### 4. 配置 + +Express 组件支持 0 配置部署,也就是可以直接通过配置文件中的默认值进行部署。但你依然可以修改更多可选配置来进一步开发该 Express 项目。 + +以下是 Express 组件的 `serverless.yml`完整配置说明: + +```yml +# serverless.yml + +component: express # (required) name of the component. In that case, it's express. +name: expressDemo # (required) name of your express component instance. +org: orgDemo # (optional) serverless dashboard org. default is the first org you created during signup. +app: appDemo # (optional) serverless dashboard app. default is the same as the name property. +stage: dev # (optional) serverless dashboard stage. default is dev. + +inputs: + src: + src: ./ # (optional) path to the source folder. default is a hello world app. + exclude: + - .env + functionName: expressDemo + region: ap-guangzhou + runtime: Nodejs10.15 + apigatewayConf: + protocols: + - http + - https + environment: release +``` + +点此查看[全量配置及配置说明](https://github.com/serverless-components/tencent-express/tree/master/docs/configure.md) + +当你根据该配置文件更新配置字段后,再次运行 `serverless deploy` 或者 `serverless` 就可以更新配置到云端。 + +### 5. 开发调试 + +部署了 Express.js 应用后,可以通过开发调试能力对该项目进行二次开发,从而开发一个生产应用。在本地修改和更新代码后,不需要每次都运行 `serverless deploy` 命令来反复部署。你可以直接通过 `serverless dev` 命令对本地代码的改动进行检测和自动上传。 + +可以通过在 `serverless.yml`文件所在的目录下运行 `serverless dev` 命令开启开发调试能力。 + +`serverless dev` 同时支持实时输出云端日志,每次部署完毕后,对项目进行访问,即可在命令行中实时输出调用日志,便于查看业务情况和排障。 + +除了实时日志输出之外,针对 Node.js 应用,当前也支持云端调试能力。在开启 `serverless dev` 命令之后,将会自动监听远端端口,并将函数的超时时间临时配置为 900s。此时你可以通过访问 chrome://inspect/#devices 查找远端的调试路径,并直接对云端代码进行断点等调试。在调试模式结束后,需要再次部署从而将代码更新并将超时时间设置为原来的值。详情参考[开发模式和云端调试](https://cloud.tencent.com/document/product/1154/43220)。 + +### 6. 查看状态 + +在`serverless.yml`文件所在的目录下,通过如下命令查看部署状态: + +``` +$ serverless info +``` + +### 7. 移除 + +在`serverless.yml`文件所在的目录下,通过以下命令移除部署的 Express 服务。移除后该组件会对应删除云上部署时所创建的所有相关资源。 + +``` +$ serverless remove +``` + +和部署类似,支持通过 `sls remove --debug` 命令查看移除过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 + +## 架构说明 + +Express 组件将在腾讯云账户中使用到如下 Serverless 服务: + +- [x] **API 网关** - API 网关将会接收外部请求并且转发到 SCF 云函数中。 +- [x] **SCF 云函数** - 云函数将承载 Express.js 应用。 +- [x] **CAM 访问控制** - 该组件会创建默认 CAM 角色用于授权访问关联资源。 +- [x] **COS 对象存储** - 为确保上传速度和质量,云函数压缩并上传代码时,会默认将代码包存储在特定命名的 COS 桶中。 +- [x] **SSL 证书服务** - 如果你在 yaml 文件中配置了 `apigatewayConf.customDomains` 字段,需要做自定义域名绑定并开启 HTTPS 时,也会用到证书管理服务和域名服务。Serverless Framework 会根据已经备案的域名自动申请并配置 SSL 证书。 + +## 账号配置 + +当前默认支持 CLI 扫描二维码登录,如您希望配置持久的环境变量/秘钥信息,也可以本地创建 `.env` 文件 + +```console +$ touch .env # 腾讯云的配置信息 +``` + +在 `.env` 文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存 + +如果没有腾讯云账号,可以在此[注册新账号](https://cloud.tencent.com/register)。 + +如果已有腾讯云账号,可以在[API 密钥管理](https://console.cloud.tencent.com/cam/capi)中获取 `SecretId` 和`SecretKey`. + +``` +# .env +TENCENT_SECRET_ID=123 +TENCENT_SECRET_KEY=123 +``` + +## License + +MIT License + +Copyright (c) 2020 Tencent Cloud, Inc. diff --git a/example/serverless.yml b/example/serverless.yml index 9eae45b..fe13841 100644 --- a/example/serverless.yml +++ b/example/serverless.yml @@ -1,18 +1,15 @@ -org: orgDemo # (optional) serverless dashboard org. default is the first org you created during signup. -app: appDemo # (optional) serverless dashboard app. default is the same as the name property. -stage: dev # (optional) serverless dashboard stage. default is dev. -component: express # (required) name of the component. In that case, it's express. -name: expressDemo # (required) name of your express component instance. +org: orgDemo +app: appDemo +stage: dev +component: express +name: expressDemo inputs: src: - src: ./ # (optional) path to the source folder. default is a hello world app. + src: ./ exclude: - .env - region: ap-guangzhou - runtime: Nodejs10.15 apigatewayConf: protocols: - http - https - environment: release diff --git a/example/sls.js b/example/sls.js index ce89135..b0d9090 100644 --- a/example/sls.js +++ b/example/sls.js @@ -1,6 +1,7 @@ const express = require('express') const path = require('path') const app = express() +const isServerless = process.env.SERVERLESS // Routes app.get(`/`, (req, res) => { @@ -39,6 +40,10 @@ app.use(function(err, req, res, next) { res.status(500).send('Internal Serverless Error') }) -app.listen(8080) - -module.exports = app +if (isServerless) { + module.exports = app +} else { + app.listen(3000, () => { + console.log(`Server start on http://localhost:3000`) + }) +} diff --git a/example/sls.upload.js b/example/sls.upload.js new file mode 100644 index 0000000..14b8552 --- /dev/null +++ b/example/sls.upload.js @@ -0,0 +1,59 @@ +const multer = require('multer') +const express = require('express') +const path = require('path') + +const app = express() +const isServerless = process.env.SERVERLESS +const upload = multer({ dest: isServerless ? '/tmp/upload' : './upload' }) + +// Routes +app.post('/upload', upload.single('file'), (req, res) => { + res.send({ + success: true, + data: req.file + }) +}) + +app.get(`/`, (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')) +}) + +app.get('/user', (req, res) => { + res.send([ + { + title: 'serverless framework', + link: 'https://serverless.com' + } + ]) +}) + +app.get('/user/:id', (req, res) => { + const id = req.params.id + res.send({ + id: id, + title: 'serverless framework', + link: 'https://serverless.com' + }) +}) + +app.get('/404', (req, res) => { + res.status(404).send('Not found') +}) + +app.get('/500', (req, res) => { + res.status(500).send('Server Error') +}) + +// Error handler +app.use(function(err, req, res, next) { + console.error(err) + res.status(500).send('Internal Serverless Error') +}) + +if (isServerless) { + module.exports = app +} else { + app.listen(3000, () => { + console.log(`Server start on http://localhost:3000`) + }) +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..a70dd57 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,14 @@ +const { join } = require('path') +require('dotenv').config({ path: join(__dirname, '.env.test') }) + +const config = { + verbose: true, + silent: false, + testTimeout: 600000, + testEnvironment: 'node', + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$', + testPathIgnorePatterns: ['/node_modules/', '/__tests__/lib/'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] +} + +module.exports = config diff --git a/package.json b/package.json index b06f08f..4b0d707 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "@serverless/express", - "version": "0.0.17", "main": "src/serverless.js", "publishConfig": { "access": "public" }, "scripts": { - "test": "npm run lint && npm run prettier", + "test": "jest", "commitlint": "commitlint -f HEAD@{15}", "lint": "eslint --ext .js,.ts,.tsx .", "lint:fix": "eslint --fix --ext .js,.ts,.tsx .", @@ -18,9 +17,9 @@ }, "husky": { "hooks": { - "pre-commit": "lint-staged", + "pre-commit": "ygsec && lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", - "pre-push": "npm run lint:fix && npm run prettier:fix" + "pre-push": "ygsec && npm run lint:fix && npm run prettier:fix" } }, "lint-staged": { @@ -44,15 +43,41 @@ "@semantic-release/git": "^9.0.0", "@semantic-release/npm": "^7.0.4", "@semantic-release/release-notes-generator": "^9.0.1", + "@serverless/platform-client-china": "^1.0.19", + "@ygkit/secure": "0.0.3", + "axios": "^0.19.2", "babel-eslint": "^10.1.0", "dotenv": "^8.2.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-prettier": "^3.1.2", - "husky": "^4.2.3", + "husky": "^4.2.5", + "jest": "^25.0.1", "lint-staged": "^10.0.8", "prettier": "^1.19.1", "semantic-release": "^17.0.4" - } + }, + "description": "Easily deploy serverless Express.js applications to Tencent Cloud with the Serverless Framework", + "directories": { + "doc": "docs", + "example": "example", + "test": "tests" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/serverless-components/tencent-express.git" + }, + "keywords": [ + "serverless-express", + "serverless", + "express", + "serverless-framework", + "serverless-components", + "tencent-cloud" + ], + "bugs": { + "url": "https://github.com/serverless-components/tencent-express/issues" + }, + "homepage": "https://github.com/serverless-components/tencent-express#readme" } diff --git a/release.config.js b/release.config.js index 53f3398..98b3864 100644 --- a/release.config.js +++ b/release.config.js @@ -1,7 +1,6 @@ module.exports = { verifyConditions: [ '@semantic-release/changelog', - '@semantic-release/npm', '@semantic-release/git', '@semantic-release/github' ], @@ -33,14 +32,6 @@ module.exports = { changelogFile: 'CHANGELOG.md' } ], - [ - '@semantic-release/npm', - { - pkgRoot: '.', - npmPublish: false, - tarballDir: false - } - ], [ '@semantic-release/git', { diff --git a/serverless.component.yml b/serverless.component.yml index c9a84d4..34269df 100644 --- a/serverless.component.yml +++ b/serverless.component.yml @@ -1,10 +1,11 @@ name: express -version: 0.0.17 +version: 0.3.0 author: Tencent Cloud, Inc. org: Tencent Cloud, Inc. -description: Deploys a serverless Express.js application onto Tencent SCF and Tencent APIGateway. +description: Deploy a serverless Express.js application on Tencent SCF and API Gateway. keywords: tencent, serverless, express -repo: https://github.com/serverless-components/tencent-express/tree/v2 -readme: https://github.com/serverless-components/tencent-express/tree/v2/README.md +repo: https://github.com/serverless-components/tencent-express +readme: https://github.com/serverless-components/tencent-express/tree/master/README.md license: MIT main: ./src +webDeployable: true diff --git a/src/_shims/handler.js b/src/_shims/handler.js index f132ec2..5863add 100644 --- a/src/_shims/handler.js +++ b/src/_shims/handler.js @@ -4,15 +4,15 @@ const path = require('path') const { createServer, proxy } = require('tencent-serverless-http') let server +let app exports.handler = async (event, context) => { - const userSls = path.join(__dirname, '..', 'sls.js') - let app + const userSls = path.join(__dirname, '..', process.env.SLS_ENTRY_FILE) if (fs.existsSync(userSls)) { - // load the user provided app + // eslint-disable-next-line + console.log(`Using user custom entry file ${process.env.SLS_ENTRY_FILE}`) app = require(userSls) } else { - // load the built-in default app app = require('./sls.js') } @@ -20,6 +20,11 @@ exports.handler = async (event, context) => { app.request.__SLS_EVENT__ = event app.request.__SLS_CONTEXT__ = context + // provide sls intialize hooks + if (app.slsInitialize && typeof app.slsInitialize === 'function') { + await app.slsInitialize() + } + // cache server, not create repeatly if (!server) { server = createServer(app, null, app.binaryTypes || []) @@ -27,11 +32,6 @@ exports.handler = async (event, context) => { context.callbackWaitsForEmptyEventLoop = app.callbackWaitsForEmptyEventLoop === true - // provide sls intialize hooks - if (app.slsInitialize && typeof app.slsInitialize === 'function') { - await app.slsInitialize() - } - const result = await proxy(server, event, context, 'PROMISE') return result.promise } diff --git a/src/config.js b/src/config.js index 51f414a..35901dd 100644 --- a/src/config.js +++ b/src/config.js @@ -3,13 +3,13 @@ const CONFIGS = { 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/express-demo.zip', compName: 'express', compFullname: 'Express.js', + defaultEntryFile: 'sls.js', handler: 'sl_handler.handler', runtime: 'Nodejs10.15', - exclude: ['.git/**', '.gitignore', '.DS_Store'], timeout: 3, memorySize: 128, namespace: 'default', - description: 'This is a function created by serverless component' + description: 'Created by Serverless Component' } module.exports = CONFIGS diff --git a/src/package.json b/src/package.json index f99537f..8722896 100644 --- a/src/package.json +++ b/src/package.json @@ -1,18 +1,7 @@ { - "name": "@serverless/express", - "main": "./serverless.js", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint . --fix --cache" - }, - "author": "Tencent Cloud, Inc.", - "license": "MIT", "dependencies": { "download": "^8.0.0", - "tencent-component-toolkit": "^1.12.7", + "tencent-component-toolkit": "^1.20.10", "type": "^2.0.0" } } diff --git a/src/serverless.js b/src/serverless.js index 5b69ee9..f76e037 100644 --- a/src/serverless.js +++ b/src/serverless.js @@ -1,7 +1,7 @@ const { Component } = require('@serverless/core') -const { MultiApigw, Scf, Apigw, Cns, Cam, Metrics } = require('tencent-component-toolkit') +const { Scf, Apigw, Cns, Cam, Metrics } = require('tencent-component-toolkit') const { TypeError } = require('tencent-component-toolkit/src/utils/error') -const { uploadCodeToCos, getDefaultProtocol, deleteRecord, prepareInputs } = require('./utils') +const { uploadCodeToCos, getDefaultProtocol, prepareInputs, deepClone } = require('./utils') const CONFIGS = require('./config') class ServerlessComponent extends Component { @@ -39,134 +39,141 @@ class ServerlessComponent extends Component { } } - const uploadCodeHandler = [] const outputs = {} const appId = this.getAppId() - for (let eveRegionIndex = 0; eveRegionIndex < regionList.length; eveRegionIndex++) { - const curRegion = regionList[eveRegionIndex] - const funcDeployer = async () => { - const code = await uploadCodeToCos(this, appId, credentials, inputs, curRegion) - const scf = new Scf(credentials, curRegion) - const tempInputs = { - ...inputs, - code - } - const scfOutput = await scf.deploy(tempInputs) - outputs[curRegion] = { - functionName: scfOutput.FunctionName, - runtime: scfOutput.Runtime, - namespace: scfOutput.Namespace - } + 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] - } + 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 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 + // 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 + if (outputs[curRegion].traffic !== 1 && scfOutput.ConfigTrafficVersion) { + outputs[curRegion].configTrafficVersion = scfOutput.ConfigTrafficVersion + this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion } - uploadCodeHandler.push(funcDeployer()) + + 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) } - await Promise.all(uploadCodeHandler) 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 apigw = new MultiApigw(credentials, regionList) - const oldState = this.state[regionList[0]] || {} - inputs.oldState = { - apiList: oldState.apiList || [], - customDomains: oldState.customDomains || [] + + const getServiceId = (instance, region) => { + const regionState = instance.state[region] + return inputs.serviceId || (regionState && regionState.serviceId) } - const apigwOutputs = await apigw.deploy(inputs) - const outputs = {} - Object.keys(apigwOutputs).forEach((curRegion) => { - const curOutput = apigwOutputs[curRegion] - outputs[curRegion] = { - serviceId: curOutput.serviceId, - subDomain: curOutput.subDomain, - environment: curOutput.environment, - url: `${getDefaultProtocol(inputs.protocols)}://${curOutput.subDomain}/${ - curOutput.environment - }/` - } - if (curOutput.customDomains) { - outputs[curRegion].customDomains = curOutput.customDomains - } - this.state[curRegion] = { - created: curOutput.created, - ...(this.state[curRegion] ? this.state[curRegion] : {}), - ...outputs[curRegion], - apiList: curOutput.apiList - } - }) - this.save() - return outputs - } - async deployCns(credentials, inputs, regionList, apigwOutputs) { - const cns = new Cns(credentials) - const cnsRegion = {} + const deployTasks = [] + const outputs = {} regionList.forEach((curRegion) => { - const curApigwOutput = apigwOutputs[curRegion] - cnsRegion[curRegion] = curApigwOutput.subDomain - }) + const apigwDeployer = async () => { + const apigw = new Apigw(credentials, curRegion) - const state = [] - const outputs = {} - const tempJson = {} - for (let i = 0; i < inputs.length; i++) { - const curCns = inputs[i] - for (let j = 0; j < curCns.records.length; j++) { - curCns.records[j].value = - cnsRegion[curCns.records[j].value.replace('temp_value_about_', '')] - } - const tencentCnsOutputs = await cns.deploy(curCns) - outputs[curCns.domain] = tencentCnsOutputs.DNS - ? tencentCnsOutputs.DNS - : 'The domain name has already been added.' - tencentCnsOutputs.domain = curCns.domain - state.push(tencentCnsOutputs) - } + 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}` + } - // 删除serverless创建的但是不在本次列表中 - try { - for (let i = 0; i < state.length; i++) { - tempJson[state[i].domain] = state[i].records - } - const recordHistory = this.state.cns || [] - for (let i = 0; i < recordHistory.length; i++) { - const delList = deleteRecord(tempJson[recordHistory[i].domain], recordHistory[i].records) - if (delList && delList.length > 0) { - await cns.remove({ deleteList: delList }) + 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 } } - } catch (e) {} + deployTasks.push(apigwDeployer()) + }) + + await Promise.all(deployTasks) - this.state['cns'] = state this.save() return outputs } @@ -177,7 +184,7 @@ class ServerlessComponent extends Component { const credentials = this.getCredentials() // 对Inputs内容进行标准化 - const { regionList, functionConf, apigatewayConf, cnsConf } = await prepareInputs( + const { regionList, functionConf, apigatewayConf } = await prepareInputs( this, credentials, inputs @@ -189,29 +196,33 @@ class ServerlessComponent extends Component { outputs.templateUrl = CONFIGS.templateUrl } - const deployTasks = [this.deployFunction(credentials, functionConf, regionList, outputs)] + let apigwOutputs + const functionOutputs = await this.deployFunction( + credentials, + functionConf, + regionList, + outputs + ) // support apigatewayConf.isDisabled if (apigatewayConf.isDisabled !== true) { - deployTasks.push(this.deployApigateway(credentials, apigatewayConf, regionList, outputs)) + apigwOutputs = await this.deployApigateway(credentials, apigatewayConf, regionList, outputs) } else { this.state.apigwDisabled = true } - const [functionOutputs, apigwOutputs = {}] = await Promise.all(deployTasks) // optimize outputs for one region if (regionList.length === 1) { const [oneRegion] = regionList outputs.region = oneRegion - outputs['apigw'] = apigwOutputs[oneRegion] outputs['scf'] = functionOutputs[oneRegion] + if (apigwOutputs) { + outputs['apigw'] = apigwOutputs[oneRegion] + } } else { - outputs['apigw'] = apigwOutputs outputs['scf'] = functionOutputs - } - - // cns depends on apigw, so if disabled apigw, just ignore it. - if (cnsConf.length > 0 && apigatewayConf.isDisabled !== true) { - outputs['cns'] = await this.deployCns(credentials, cnsConf, regionList, apigwOutputs) + if (apigwOutputs) { + outputs['apigw'] = apigwOutputs + } } this.state.region = regionList[0] @@ -236,10 +247,6 @@ class ServerlessComponent extends Component { const scf = new Scf(credentials, curRegion) const apigw = new Apigw(credentials, curRegion) const handler = async () => { - await scf.remove({ - functionName: curState.functionName, - namespace: curState.namespace - }) // if disable apigw, no need to remove if (state.apigwDisabled !== true) { await apigw.remove({ @@ -250,6 +257,10 @@ class ServerlessComponent extends Component { customDomains: curState.customDomains }) } + await scf.remove({ + functionName: curState.functionName, + namespace: curState.namespace + }) } removeHandlers.push(handler()) } @@ -290,6 +301,12 @@ class ServerlessComponent extends Component { 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( diff --git a/src/utils.js b/src/utils.js index 5403265..b8b6b6d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,5 @@ const path = require('path') -const { Domain, Cos } = require('tencent-component-toolkit') +const { Cos } = require('tencent-component-toolkit') const ensureObject = require('type/object/ensure') const ensureIterable = require('type/iterable/ensure') const ensureString = require('type/string/ensure') @@ -15,10 +15,45 @@ const generateId = () => .toString(36) .substring(6) +const deepClone = (obj) => { + return JSON.parse(JSON.stringify(obj)) +} + 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 +} + +const capitalString = (str) => { + if (str.length < 2) { + return str.toUpperCase() + } + + return `${str[0].toUpperCase()}${str.slice(1)}` +} + +const getDefaultProtocol = (protocols) => { + return String(protocols).includes('https') ? 'https' : 'http' +} + +const getDefaultFunctionName = () => { + return `${CONFIGS.compName}_component_${generateId()}` +} + +const getDefaultServiceName = () => { + return 'serverless' +} + +const getDefaultServiceDescription = () => { + return 'Created by Serverless Component' +} + const validateTraffic = (num) => { if (getType(num) !== 'Number') { throw new TypeError( @@ -107,12 +142,18 @@ const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => object: objectName, method: 'PUT' }) - const slsSDKEntries = instance.getSDKEntries('_shims/handler.handler') + // if shims and sls sdk entries had been injected to zipPath, no need to injected again console.log(`Uploading code to bucket ${bucketName}`) - await instance.uploadSourceZipToCOS(zipPath, uploadUrl, slsSDKEntries, { - _shims: path.join(__dirname, '_shims') - }) + 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`) } } @@ -127,72 +168,13 @@ const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => } } -const mergeJson = (sourceJson, targetJson) => { - for (const eveKey in sourceJson) { - if (targetJson.hasOwnProperty(eveKey)) { - if (['protocols', 'endpoints', 'customDomain'].indexOf(eveKey) != -1) { - for (let i = 0; i < sourceJson[eveKey].length; i++) { - const sourceEvents = JSON.stringify(sourceJson[eveKey][i]) - const targetEvents = JSON.stringify(targetJson[eveKey]) - if (targetEvents.indexOf(sourceEvents) == -1) { - targetJson[eveKey].push(sourceJson[eveKey][i]) - } - } - } else { - if (typeof sourceJson[eveKey] != 'string') { - mergeJson(sourceJson[eveKey], targetJson[eveKey]) - } else { - targetJson[eveKey] = sourceJson[eveKey] - } - } - } else { - targetJson[eveKey] = sourceJson[eveKey] - } - } - return targetJson -} - -const capitalString = (str) => { - if (str.length < 2) { - return str.toUpperCase() - } - - return `${str[0].toUpperCase()}${str.slice(1)}` -} - -const getDefaultProtocol = (protocols) => { - if (protocols.map((i) => i.toLowerCase()).includes('https')) { - return 'https' - } - return 'http' -} - -const deleteRecord = (newRecords, historyRcords) => { - const deleteList = [] - for (let i = 0; i < historyRcords.length; i++) { - let temp = false - for (let j = 0; j < newRecords.length; j++) { - if ( - newRecords[j].domain == historyRcords[i].domain && - newRecords[j].subDomain == historyRcords[i].subDomain && - newRecords[j].recordType == historyRcords[i].recordType && - newRecords[j].value == historyRcords[i].value && - newRecords[j].recordLine == historyRcords[i].recordLine - ) { - temp = true - break - } - } - if (!temp) { - deleteList.push(historyRcords[i]) - } - } - return deleteList -} - const prepareInputs = async (instance, credentials, inputs = {}) => { // 对function inputs进行标准化 - const tempFunctionConf = inputs.functionConf ? inputs.functionConf : {} + const tempFunctionConf = inputs.functionConf + ? inputs.functionConf + : inputs.functionConfig + ? inputs.functionConfig + : {} const fromClientRemark = `tencent-${CONFIGS.compName}` const regionList = inputs.region ? typeof inputs.region == 'string' @@ -203,10 +185,7 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { // chenck state function name const stateFunctionName = instance.state[regionList[0]] && instance.state[regionList[0]].functionName - // check state service id - const stateServiceId = instance.state[regionList[0]] && instance.state[regionList[0]].serviceId - - const functionConf = { + const functionConf = Object.assign(tempFunctionConf, { code: { src: inputs.src, bucket: inputs.srcOriginal && inputs.srcOriginal.bucket, @@ -215,7 +194,7 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { name: ensureString(inputs.functionName, { isOptional: true }) || stateFunctionName || - `${CONFIGS.compName}_component_${generateId()}`, + getDefaultFunctionName(), region: regionList, role: ensureString(tempFunctionConf.role ? tempFunctionConf.role : inputs.role, { default: '' @@ -240,10 +219,18 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { 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 - } + 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) { @@ -251,82 +238,78 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { } functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion - functionConf.tags = ensureObject(tempFunctionConf.tags ? tempFunctionConf.tags : inputs.tag, { - default: null - }) - - functionConf.include = ensureIterable( - tempFunctionConf.include ? tempFunctionConf.include : inputs.include, - { default: [], ensureItem: ensureString } - ) - functionConf.exclude = ensureIterable( - tempFunctionConf.exclude ? tempFunctionConf.exclude : inputs.exclude, - { default: [], ensureItem: ensureString } - ) - functionConf.exclude.push('.git/**', '.gitignore', '.serverless', '.DS_Store') - if (inputs.functionConf) { - functionConf.timeout = inputs.functionConf.timeout - ? inputs.functionConf.timeout - : CONFIGS.timeout - functionConf.memorySize = inputs.functionConf.memorySize - ? inputs.functionConf.memorySize - : CONFIGS.memorySize - if (inputs.functionConf.environment) { - functionConf.environment = inputs.functionConf.environment - } - if (inputs.functionConf.vpcConfig) { - functionConf.vpcConfig = inputs.functionConf.vpcConfig + 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 + } + // 对apigw inputs进行标准化 - const apigatewayConf = inputs.apigatewayConf ? inputs.apigatewayConf : {} - apigatewayConf.isDisabled = inputs.apigatewayConf === true - apigatewayConf.fromClientRemark = fromClientRemark - apigatewayConf.serviceName = inputs.serviceName - apigatewayConf.description = `Serverless Framework Tencent-${capitalString( - CONFIGS.compName - )} Component` - apigatewayConf.serviceId = inputs.serviceId || stateServiceId - apigatewayConf.region = functionConf.region - apigatewayConf.protocols = apigatewayConf.protocols || ['http'] - apigatewayConf.environment = apigatewayConf.environment ? apigatewayConf.environment : 'release' - apigatewayConf.endpoints = [ - { - path: '/', - enableCORS: apigatewayConf.enableCORS, - serviceTimeout: apigatewayConf.serviceTimeout, - method: 'ANY', - function: { - isIntegratedResponse: apigatewayConf.isIntegratedResponse === false ? false : true, - functionName: functionConf.name, - functionNamespace: functionConf.namespace, - functionQualifier: functionConf.needSetTraffic ? '$DEFAULT' : '$LATEST' + 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 || [] + }) + 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 (apigatewayConf.usagePlan) { + ] + } + if (tempApigwConf.usagePlan) { apigatewayConf.endpoints[0].usagePlan = { - usagePlanId: apigatewayConf.usagePlan.usagePlanId, - usagePlanName: apigatewayConf.usagePlan.usagePlanName, - usagePlanDesc: apigatewayConf.usagePlan.usagePlanDesc, - maxRequestNum: apigatewayConf.usagePlan.maxRequestNum + usagePlanId: tempApigwConf.usagePlan.usagePlanId, + usagePlanName: tempApigwConf.usagePlan.usagePlanName, + usagePlanDesc: tempApigwConf.usagePlan.usagePlanDesc, + maxRequestNum: tempApigwConf.usagePlan.maxRequestNum } } - if (apigatewayConf.auth) { + if (tempApigwConf.auth) { apigatewayConf.endpoints[0].auth = { - secretName: apigatewayConf.auth.secretName, - secretIds: apigatewayConf.auth.secretIds + secretName: tempApigwConf.auth.secretName, + secretIds: tempApigwConf.auth.secretIds } } - // 对cns inputs进行标准化 - const tempCnsConf = {} - const tempCnsBaseConf = inputs.cloudDNSConf ? inputs.cloudDNSConf : {} - - // 分地域处理functionConf/apigatewayConf/cnsConf - for (let i = 0; i < functionConf.region.length; i++) { - const curRegion = functionConf.region[i] + regionList.forEach((curRegion) => { const curRegionConf = inputs[curRegion] if (curRegionConf && curRegionConf.functionConf) { functionConf[curRegion] = curRegionConf.functionConf @@ -334,62 +317,21 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { if (curRegionConf && curRegionConf.apigatewayConf) { apigatewayConf[curRegion] = curRegionConf.apigatewayConf } - - const tempRegionCnsConf = mergeJson( - tempCnsBaseConf, - curRegionConf && curRegionConf.cloudDNSConf ? curRegionConf.cloudDNSConf : {} - ) - - tempCnsConf[functionConf.region[i]] = { - recordType: 'CNAME', - recordLine: tempRegionCnsConf.recordLine ? tempRegionCnsConf.recordLine : undefined, - ttl: tempRegionCnsConf.ttl, - mx: tempRegionCnsConf.mx, - status: tempRegionCnsConf.status ? tempRegionCnsConf.status : 'enable' - } - } - - const cnsConf = [] - // 对cns inputs进行检查和赋值 - if (apigatewayConf.customDomain && apigatewayConf.customDomain.length > 0) { - const domain = new Domain(credentials) - for (let domianNum = 0; domianNum < apigatewayConf.customDomain.length; domianNum++) { - const domainData = await domain.check(apigatewayConf.customDomain[domianNum].domain) - const tempInputs = { - domain: domainData.domain, - records: [] - } - for (let eveRecordNum = 0; eveRecordNum < functionConf.region.length; eveRecordNum++) { - if (tempCnsConf[functionConf.region[eveRecordNum]].recordLine) { - tempInputs.records.push({ - subDomain: domainData.subDomain || '@', - recordType: 'CNAME', - recordLine: tempCnsConf[functionConf.region[eveRecordNum]].recordLine, - value: `temp_value_about_${functionConf.region[eveRecordNum]}`, - ttl: tempCnsConf[functionConf.region[eveRecordNum]].ttl, - mx: tempCnsConf[functionConf.region[eveRecordNum]].mx, - status: tempCnsConf[functionConf.region[eveRecordNum]].status || 'enable' - }) - } - } - cnsConf.push(tempInputs) - } - } + }) return { regionList, functionConf, - apigatewayConf, - cnsConf + apigatewayConf } } module.exports = { + deepClone, generateId, uploadCodeToCos, mergeJson, capitalString, getDefaultProtocol, - deleteRecord, prepareInputs }