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/.prettierignore b/.prettierignore index 18f2b36..ed450ca 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,5 @@ coverage dist node_modules +CHANGELOG.md +*.test.js \ No newline at end of file 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..677f120 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,57 @@ +## [0.0.10](https://github.com/serverless-components/tencent-flask/compare/v0.0.9...v0.0.10) (2021-02-02) + + +### Bug Fixes + +* multi cookie bug ([97043cb](https://github.com/serverless-components/tencent-flask/commit/97043cb2d0a1d66d448f216309aac92289cc2e7d)) + +## [0.0.9](https://github.com/serverless-components/tencent-flask/compare/v0.0.8...v0.0.9) (2020-12-15) + + +### Bug Fixes + +* update deploy and remove flow ([#14](https://github.com/serverless-components/tencent-flask/issues/14)) ([4b3c40c](https://github.com/serverless-components/tencent-flask/commit/4b3c40cb9e9f5f586a9d781cbae523112dfdebc0)) + +## [0.0.8](https://github.com/serverless-components/tencent-flask/compare/v0.0.7...v0.0.8) (2020-09-07) + + +### Bug Fixes + +* update deploy flow for multi region ([fe034d1](https://github.com/serverless-components/tencent-flask/commit/fe034d170e434f9b3ac31c1b495957e6f5bf3f3e)) +* update deps ([821f3e6](https://github.com/serverless-components/tencent-flask/commit/821f3e65312332335eb804caefdc8fd928b618aa)) + +## [0.0.7](https://github.com/serverless-components/tencent-flask/compare/v0.0.6...v0.0.7) (2020-09-02) + + +### Bug Fixes + +* update tencnet-component-toolkit for api mark ([86e5a49](https://github.com/serverless-components/tencent-flask/commit/86e5a498820c8f0312405593033fa9b0590f1478)) + +## [0.0.6](https://github.com/serverless-components/tencent-flask/compare/v0.0.5...v0.0.6) (2020-09-02) + + +### Bug Fixes + +* support cfs config ([27f4374](https://github.com/serverless-components/tencent-flask/commit/27f437462b664930fd0483119d414705b660071b)) + +## [0.0.5](https://github.com/serverless-components/tencent-flask/compare/v0.0.4...v0.0.5) (2020-08-26) + + +### Bug Fixes + +* deploy error ([5934744](https://github.com/serverless-components/tencent-flask/commit/59347449c62fec0784a06d373a64a9635786108a)) +* prettier config ([46a7011](https://github.com/serverless-components/tencent-flask/commit/46a701142c21b7fd5a069599a0c429a3942b1e38)) +* support eip config ([aaf8ca1](https://github.com/serverless-components/tencent-flask/commit/aaf8ca1dc166e37f9a635695f0dc2c52ff9f3243)) +* traffic zero display bug ([d80d421](https://github.com/serverless-components/tencent-flask/commit/d80d4218bbd51c51cd4a590efc6cffe4fce6f959)) +* update get credential error message ([3964862](https://github.com/serverless-components/tencent-flask/commit/396486273ead2dcacec85607418bc4f06db95ea2)) +* upgrade deps ([4dfde96](https://github.com/serverless-components/tencent-flask/commit/4dfde9610102d0cd7b081b42b329474df8513378)) +* upgrade deps ([b0366d7](https://github.com/serverless-components/tencent-flask/commit/b0366d77d78c754eecf0d06c1ec2d7aa6197ff58)) +* upgrade deps ([7a6aead](https://github.com/serverless-components/tencent-flask/commit/7a6aead877bfe58a0168c6ff9628ec250c292cee)) + + +### Features + +* init v2 ([245ce3a](https://github.com/serverless-components/tencent-flask/commit/245ce3a09e36e3224ead0381c97ab7d684d67903)) +* support scf publish version and traffic setup ([661bd44](https://github.com/serverless-components/tencent-flask/commit/661bd449a7c51801163f537e4ea12837542f119b)) +* support usage plan & auth ([f8084f5](https://github.com/serverless-components/tencent-flask/commit/f8084f5fa3d506ddc9f8e37fb8b53a0afd6183ad)) +* update config & docs ([cb3f8d4](https://github.com/serverless-components/tencent-flask/commit/cb3f8d4c939041cfcec09a62d371453e5c4ec9f5)) diff --git a/README.en.md b/README.en.md deleted file mode 100755 index 5e92dc1..0000000 --- a/README.en.md +++ /dev/null @@ -1,120 +0,0 @@ -[![Serverless Python Flask Tencent Cloud](https://img.serverlesscloud.cn/20191226/1577347052683-flask_%E9%95%BF.png)](http://serverless.com) - -# Tencent Flask Serverless Component - -[简体中文](./README.md) | English - -## Introduction - -Tencent [Flask](https://github.com/pallets/flask) Serverless Component, support Restful API deploy, not supportting Flask command. - -## Content - -1. [Prepare](#0-prepare) -1. [Install](#1-install) -1. [Create](#2-create) -1. [Configure](#3-configure) -1. [Deploy](#4-deploy) -1. [Remove](#5-Remove) - -### 0. Prepare - -Before using this component, you need create a flask project, then add `Flask` and `werkzeug` in `requirements.txt`. Like below: - -```txt -Flask==1.0.2 -werkzeug==0.16.0 -``` - -Then create your API service entry file `app.py`, below is a example: - -```python -from flask import Flask, jsonify -app = Flask(__name__) - -@app.route("/") -def index(): - return "Hello Flash" - -@app.route("/users") -def users(): - users = [{'name': 'test1'}, {'name': 'test2'}] - return jsonify(data=users) - -@app.route("/users/") -def user(id): - return jsonify(data={'name': 'test1'}) -``` - -### 1. Install - -Install the Serverless Framework globally: - -```shell -$ npm install -g serverless -``` - -### 2. Create - -Just create the following simple boilerplate: - -```shell -$ touch serverless.yml -$ touch .env # your Tencent api keys -``` - -Add the access keys of a [Tencent CAM Role](https://console.cloud.tencent.com/cam/capi) with `AdministratorAccess` in the `.env` file, using this format: - -``` -# .env -TENCENT_SECRET_ID=XXX -TENCENT_SECRET_KEY=XXX -``` - -- If you don't have a Tencent Cloud account, you could [sign up](https://intl.cloud.tencent.com/register) first. - -### 3. Configure - -```yml -# serverless.yml - -component: flask -name: flashDemo -org: orgDemo -app: appDemo -stage: dev - -inputs: - src: - hook: 'pip install -r requirements.txt -t ./' - dist: ./ - exclude: - - .env - region: ap-guangzhou - runtime: Python3.6 - apigatewayConf: - protocols: - - http - - https - environment: release -``` - -- [More Options](./docs/configure.md)) - -### 4. Deploy - -```shell -$ sls --debug -``` - -  - -### 5. Remove - -```shell -$ sls remove --debug -``` - -### More Components - -Checkout the [Serverless Components](https://github.com/serverless/components) repo for more information. diff --git a/README.md b/README.md index 288a87d..6fc4cf8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ +⚠️⚠️⚠️ 所有框架组件项目迁移到 [tencent-framework-components](https://github.com/serverless-components/tencent-framework-components). + [![Serverless Python Flask Tencent Cloud](https://img.serverlesscloud.cn/20191226/1577347052683-flask_%E9%95%BF.png)](http://serverless.com) # 腾讯云 Flask Serverless Component -简体中文 | [English](./README.en.md) - ## 简介 腾讯云 [Flask](https://github.com/pallets/flask) Serverless Component, 支持 Restful API 服务的部署,不支持 Flask Command. @@ -35,7 +35,7 @@ app = Flask(__name__) @app.route("/") def index(): - return "Hello Flash" + return "Hello Flask" @app.route("/users") def users(): @@ -51,7 +51,7 @@ def user(id): 通过 npm 全局安装 [serverless cli](https://github.com/serverless/serverless) -```shell +```bash $ npm install -g serverless ``` @@ -59,7 +59,7 @@ $ npm install -g serverless 本地创建 `serverless.yml` 文件,在其中进行如下配置 -```shell +```bash $ touch serverless.yml ``` @@ -87,7 +87,7 @@ inputs: environment: release ``` -- [更多配置](./docs/configure.md) +- [更多配置](https://github.com/serverless-components/tencent-flask/tree/master/docs/configure.md) ### 3. 部署 @@ -95,15 +95,15 @@ inputs: 通过 `sls` 命令进行部署,并可以添加 `--debug` 参数查看部署过程中的信息 -```shell -$ sls --debug +```bash +$ sls deploy --debug ``` ### 4. 移除 通过以下命令移除部署的 API 网关 -```shell +```bash $ sls remove --debug ``` @@ -111,7 +111,7 @@ $ sls remove --debug 当前默认支持 CLI 扫描二维码登录,如您希望配置持久的环境变量/秘钥信息,也可以本地创建 `.env` 文件 -```shell +```bash $ touch .env # 腾讯云的配置信息 ``` @@ -130,3 +130,9 @@ TENCENT_SECRET_KEY=123 ### 更多组件 可以在 [Serverless Components](https://github.com/serverless/components/blob/master/README.cn.md) repo 中查询更多组件的信息。 + +## License + +MIT License + +Copyright (c) 2020 Tencent Cloud, Inc. diff --git a/__tests__/index.test.js b/__tests__/index.test.js new file mode 100644 index 0000000..3c172e5 --- /dev/null +++ b/__tests__/index.test.js @@ -0,0 +1,50 @@ +const { generateId, getServerlessSdk } = require('./lib/utils') + +const instanceYaml = { + org: 'orgDemo', + app: 'appDemo', + component: 'flask@dev', + name: `flask-integration-tests-${generateId()}`, + stage: 'dev', + inputs: { + runtime: 'Python3.6', + region: 'ap-guangzhou', + 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 flask app', async () => { + const instance = await sdk.deploy(instanceYaml, credentials) + expect(instance).toBeDefined() + expect(instance.instanceName).toEqual(instanceYaml.name) + // get src from template by default + expect(instance.outputs.templateUrl).toBeDefined() + expect(instance.outputs).toBeDefined() + expect(instance.outputs.region).toEqual(instanceYaml.inputs.region) + expect(instance.outputs.scf).toBeDefined() + expect(instance.outputs.scf.runtime).toEqual(instanceYaml.inputs.runtime) + expect(instance.outputs.apigw).toBeDefined() + expect(instance.outputs.apigw.environment).toEqual(instanceYaml.inputs.apigatewayConf.environment) +}) + +it('should successfully remove flask app', async () => { + await sdk.remove(instanceYaml, credentials) + result = await sdk.getInstance( + instanceYaml.org, + instanceYaml.stage, + instanceYaml.app, + instanceYaml.name + ) + + // remove action won't delete the service cause the apigw have the api binded + 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 7359fb4..8d00c44 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -6,18 +6,18 @@ # serverless.yml component: flask # (必选) 组件名称,在该实例中为flask -name: flashDemo # (必选) 组件实例名称. +name: flaskDemo # 必选) 组件实例名称. org: orgDemo # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid,必须为字符串 app: appDemo # (可选) 用于记录组织信息. 默认与name相同,必须为字符串 stage: dev # (可选) 用于区分环境信息,默认值是 dev inputs: - src: # 打包src对应目录下的代码上传到默认cos上 - hook: 'pip install -r requirements.txt -t ./' # (可选) 安装python相关依赖,每次执行部署前都会构建代码并放到当前目录下 - dist: ./ # (可选) 要上传的源码的路径,默认为一个hello world app - exclude: # (可选) 被排除的文件或目录 - - .env - # src: ./src # 第一种为string时,会打包src对应目录下的代码上传到默认cos上。 + region: ap-guangzhou # 云函数所在区域 + functionName: flaskDemo # 云函数名称 + serviceName: mytest # api网关服务名称 + runtime: Nodejs10.15 # 运行环境 + serviceId: service-np1uloxw # api网关服务ID + src: ./src # 第一种为string时,会打包src对应目录下的代码上传到默认cos上。 # src: # 第二种,部署src下的文件代码,并打包成zip上传到bucket上 # src: ./src # 本地需要打包的文件目录 # bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid @@ -27,34 +27,32 @@ inputs: # src: # 第三种,在指定存储桶bucket中已经存在了object代码,直接部署 # bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid # object: cos.zip # bucket key 指定存储桶内的文件 - region: ap-guangzhou # 云函数所在区域 - functionName: expressDemo # 云函数名称 - serviceName: mytest # api网关服务名称 - runtime: Python3.6 # 运行环境 - serviceId: service-np1uloxw # api网关服务ID layers: - name: layerName # layer名称 version: 1 # 版本 functionConf: # 函数配置相关 timeout: 10 # 超时时间,单位秒 + eip: false # 是否固定出口IP memorySize: 128 # 内存大小,单位MB environment: # 环境变量 variables: # 环境变量数组 TEST: vale vpcConfig: # 私有网络配置 - subnetId: '' # 私有网络的Id - vpcId: '' # 子网ID + vpcId: '' # 私有网络的Id + subnetId: '' # 子网ID apigatewayConf: # api网关配置 isDisabled: false # 是否禁用自动创建 API 网关功能 enableCORS: true # 允许跨域 customDomains: # 自定义域名绑定 - domain: abc.com # 待绑定的自定义的域名 certificateId: abcdefg # 待绑定自定义域名的证书唯一 ID - # 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效 + # 如要设置自定义路径映射,请设置为 false + isDefaultMapping: false + # 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 pathMappingSet: - path: / environment: release - protocols: # 绑定自定义域名的协议类型,默认与服务的前端协议一致 + protocols: # 绑定自定义域名的协议类型,默认与服务的前端协议一致。 - http # 支持http协议 - https # 支持https协议 protocols: @@ -79,15 +77,13 @@ inputs: | 参数名称 | 是否必选 | 默认值 | 描述 | | ------------------------------------ | :------: | :-------------: | :------------------------------------------------------------- | -| runtime | 否 | Python3.6 | 执行环境, 目前支持: Python3.x | -| region | 否 | ap-guangzhou | 项目部署所在区域,默认广州区 | +| runtime | 否 | `Python3.6` | 执行环境, 目前支持: Python3.6, Python2.7 | +| region | 否 | `ap-guangzhou` | 项目部署所在区域,默认广州区 | | functionName | 否 | | 云函数名称 | | serviceName | 否 | | API 网关服务名称, 默认创建一个新的服务名称 | | serviceId | 否 | | API 网关服务 ID,如果存在将使用这个 API 网关服务 | | src | 否 | `process.cwd()` | 默认为当前目录, 如果是对象, 配置参数参考 [执行目录](#执行目录) | | layers | 否 | | 云函数绑定的 layer, 配置参数参考 [层配置](#层配置) | -| exclude | 否 | | 不包含的文件 | -| include | 否 | | 包含的文件, 如果是相对路径,是相对于 `serverless.yml`的路径 | | [functionConf](#函数配置) | 否 | | 函数配置 | | [apigatewayConf](#API-网关配置) | 否 | | API 网关配置 | | [cloudDNSConf](#DNS-配置) | 否 | | DNS 配置 | @@ -95,12 +91,12 @@ inputs: ## 执行目录 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| -------- | -------- | --------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| src | 否 | String | | 代码路径。与 object 不能同时存在。 | -| exclude | 否 | Array of String | | 不包含的文件或路径, 遵守 [glob 语法](https://github.com/isaacs/node-glob) | -| bucket | 否 | String | | bucket 名称。如果配置了 src,表示部署 src 的代码并压缩成 zip 后上传到 bucket-appid 对应的存储桶中;如果配置了 object,表示获取 bucket-appid 对应存储桶中 object 对应的代码进行部署。 | -| object | 否 | String | | 部署的代码在存储桶中的路径。 | | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| -------- | :------: | :-------------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| src | 否 | String | | 代码路径。与 object 不能同时存在。 | +| exclude | 否 | Array of String | | 不包含的文件或路径, 遵守 [glob 语法](https://github.com/isaacs/node-glob) | +| bucket | 否 | String | | bucket 名称。如果配置了 src,表示部署 src 的代码并压缩成 zip 后上传到 bucket-appid 对应的存储桶中;如果配置了 object,表示获取 bucket-appid 对应存储桶中 object 对应的代码进行部署。 | +| object | 否 | String | | 部署的代码在存储桶中的路径。 | ## 层配置 @@ -115,7 +111,7 @@ inputs: | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | | ---------- | :------: | -------- | :----: | :---------------------------------------------- | -| ttl | 否 | Number | 600 | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | +| ttl | 否 | Number | `600` | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | | recordLine | 否 | String[] | | 记录的线路名称 | ### 指定区配置 @@ -130,12 +126,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 | ##### 环境变量 @@ -152,16 +149,16 @@ 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 网关。 | ##### 使用计划 @@ -187,13 +184,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/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..2cef6cd --- /dev/null +++ b/example/.gitignore @@ -0,0 +1 @@ +requirements diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..b78aea5 --- /dev/null +++ b/example/README.md @@ -0,0 +1,9 @@ +# Flask example + +本地启动服务: + +```bash +$ ENV=local python app.py +``` + +可以发现 `app.py` 中通过判断环境变量 `ENV` 为 `local` 才启动服务,云函数运行时就不会启动服务。 diff --git a/example/app.py b/example/app.py index 82a0f6f..b932647 100644 --- a/example/app.py +++ b/example/app.py @@ -1,10 +1,11 @@ +import os from flask import Flask, jsonify app = Flask(__name__) @app.route("/") def index(): - return "Hello Flask" + return "Flask Restful API Created By Serverless Component" @app.route("/users") @@ -16,3 +17,7 @@ def users(): @app.route("/users/") def user(id): return jsonify(data={'name': 'test1'}) + +isLocal = os.getenv('ENV') == 'local' +if isLocal: + app.run(host='0.0.0.0',port=3000,debug=True) diff --git a/example/serverless.yml b/example/serverless.yml index 18d6dc2..0b423f4 100644 --- a/example/serverless.yml +++ b/example/serverless.yml @@ -1,16 +1,20 @@ -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: flask # (required) name of the component. In that case, it's flask. -name: flashDemo # (required) name of your flash component instance. +org: orgDemo +app: appDemo +stage: dev +component: flask +name: flaskDemo inputs: src: # TODO: 安装python项目依赖到项目当前目录 - hook: 'pip3 install -r requirements.txt -t ./' - dist: ./ # (optional) path to the source folder. default is a hello world app. + hook: 'pip3 install -r requirements.txt -t ./requirements' + dist: ./ + include: + - source: ./requirements + prefix: ../ # prefix, can make ./requirements files/dir to ./ exclude: - .env + - 'requirements/**' region: ap-guangzhou runtime: Python3.6 apigatewayConf: 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 cef867f..6ad2369 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "@serverless/flask", - "version": "0.0.1", "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,13 +43,16 @@ "@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", "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" 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 0c3f0f8..8a37f6c 100644 --- a/serverless.component.yml +++ b/serverless.component.yml @@ -1,10 +1,11 @@ name: flask -version: 0.0.1 -author: Tencent Cloud, Inc -org: Tencent Cloud, Inc -description: Deploys a serverless Flask application onto Tencent SCF and Tencent APIGateway. -keywords: tencent, serverless, flask -repo: https://github.com/serverless-components/tencent-flask -readme: https://github.com/serverless-components/tencent-flask/tree/v2/README.md +version: 0.0.10 +author: 'Tencent Cloud, Inc' +org: 'Tencent Cloud, Inc' +description: Deploy a serverless Flask application onto Tencent SCF and API Gateway. +keywords: 'tencent, serverless, flask' +repo: 'https://github.com/serverless-components/tencent-flask' +readme: 'https://github.com/serverless-components/tencent-flask/tree/master/README.md' license: MIT main: ./src +webDeployable: true diff --git a/src/_shims/severless_wsgi.py b/src/_shims/severless_wsgi.py index 2de91a3..71e0a94 100644 --- a/src/_shims/severless_wsgi.py +++ b/src/_shims/severless_wsgi.py @@ -55,11 +55,13 @@ def split_headers(headers): """ new_headers = {} - for key in headers.keys(): + for key in set(headers.keys()): values = headers.get_all(key) - if len(values) > 1: + if len(values) > 1 and key.lower() != 'set-cookie': for value, casing in zip(values, all_casings(key)): new_headers[casing] = value + elif key.lower() == 'set-cookie': + new_headers[key] = values elif len(values) == 1: new_headers[key] = values[0] diff --git a/src/config.js b/src/config.js index c8ef45c..8116d62 100644 --- a/src/config.js +++ b/src/config.js @@ -4,11 +4,10 @@ const CONFIGS = { compFullname: 'Flask', handler: 'sl_handler.handler', runtime: 'Python3.6', - exclude: ['.git/**', '.gitignore', '.DS_Store'], timeout: 3, memorySize: 128, namespace: 'default', - description: 'Function created by serverless component' + description: 'Created by Serverless Component' } module.exports = CONFIGS diff --git a/src/package.json b/src/package.json index 26358e9..850fa0c 100644 --- a/src/package.json +++ b/src/package.json @@ -1,18 +1,7 @@ { - "name": "@serverless/flask", - "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.11.4", - "type": "^2.0.0" + "tencent-component-toolkit": "^1.19.8", + "type": "^2.1.0" } } diff --git a/src/serverless.js b/src/serverless.js index 52f114b..bdd8375 100644 --- a/src/serverless.js +++ b/src/serverless.js @@ -1,7 +1,7 @@ const { Component } = require('@serverless/core') -const { MultiApigw, Scf, Apigw, Cns } = require('tencent-component-toolkit') +const { Scf, Apigw, Cns, Cam } = 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 { @@ -27,114 +27,153 @@ class ServerlessComponent extends Component { } async deployFunction(credentials, inputs, regionList) { - const uploadCodeHandler = [] + 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() - 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 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 } - 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 + }/` + } - // 删除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 } @@ -145,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 @@ -157,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] @@ -204,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({ @@ -218,6 +257,10 @@ class ServerlessComponent extends Component { customDomains: curState.customDomains }) } + await scf.remove({ + functionName: curState.functionName, + namespace: curState.namespace + }) } removeHandlers.push(handler()) } diff --git a/src/utils.js b/src/utils.js index d32b376..ef8aa9f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ const path = require('path') const fs = require('fs') -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') @@ -8,13 +8,6 @@ const download = require('download') const { TypeError } = require('tencent-component-toolkit/src/utils/error') const CONFIGS = require('./config') -/* - * Pauses execution for the provided miliseconds - * - * @param ${number} wait - number of miliseconds to wait - */ -const sleep = async (wait) => new Promise((resolve) => setTimeout(() => resolve(), wait)) - /* * Generates a random id */ @@ -23,18 +16,63 @@ const generateId = () => .toString(36) .substring(6) -const getDirFiles = async (dirPath) => { - const targetPath = path.resolve(dirPath) - const files = fs.readdirSync(targetPath) - const temp = {} - files.forEach((file) => { - temp[file] = path.join(targetPath, file) +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 temp + 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( + `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, + 'traffic must be a number' + ) + } + if (num < 0 || num > 1) { + throw new TypeError( + `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, + 'traffic must be a number between 0 and 1' + ) + } + return true } const getCodeZipPath = async (instance, inputs) => { - console.log(`Packaging ${CONFIGS.compNameFullname} application...`) + console.log(`Packaging ${CONFIGS.compFullname} application...`) // unzip source zip file let zipPath @@ -43,7 +81,7 @@ const getCodeZipPath = async (instance, inputs) => { const downloadPath = `/tmp/${generateId()}` const filename = 'template' - console.log(`Installing Default ${CONFIGS.compNameFullname} App...`) + console.log(`Installing Default ${CONFIGS.compFullname} App...`) try { await download(CONFIGS.templateUrl, downloadPath, { filename: `${filename}.zip` @@ -59,6 +97,16 @@ const getCodeZipPath = async (instance, inputs) => { return zipPath } +const getDirFiles = async (dirPath) => { + const targetPath = path.resolve(dirPath) + const files = fs.readdirSync(targetPath) + const temp = {} + files.forEach((file) => { + temp[file] = path.join(targetPath, file) + }) + return temp +} + /** * Upload code to COS * @param {Component} instance serverless component instance @@ -105,10 +153,16 @@ const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => object: objectName, method: 'PUT' }) - const shimFiles = await getDirFiles(path.join(__dirname, '_shims')) + // 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, shimFiles, {}) + if (instance.codeInjected === true) { + await instance.uploadSourceZipToCOS(zipPath, uploadUrl, {}, {}) + } else { + const shimFiles = await getDirFiles(path.join(__dirname, '_shims')) + await instance.uploadSourceZipToCOS(zipPath, uploadUrl, shimFiles, {}) + instance.codeInjected = true + } console.log(`Upload ${objectName} to bucket ${bucketName} success`) } } @@ -123,69 +177,6 @@ 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 : {} @@ -199,9 +190,6 @@ 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 = { code: { src: inputs.src, @@ -211,7 +199,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: '' @@ -235,82 +223,77 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { 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, + eip: tempFunctionConf.eip === true, + l5Enable: tempFunctionConf.l5Enable === true, + timeout: tempFunctionConf.timeout ? tempFunctionConf.timeout : CONFIGS.timeout, + memorySize: tempFunctionConf.memorySize ? tempFunctionConf.memorySize : CONFIGS.memorySize, + tags: ensureObject(tempFunctionConf.tags ? tempFunctionConf.tags : inputs.tag, { + default: null }) } - 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 - } + // validate traffic + if (inputs.traffic !== undefined) { + validateTraffic(inputs.traffic) + } + functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion + + if (tempFunctionConf.environment) { + functionConf.environment = inputs.functionConf.environment + } + if (tempFunctionConf.vpcConfig) { + functionConf.vpcConfig = inputs.functionConf.vpcConfig } // 对apigw inputs进行标准化 - const apigatewayConf = inputs.apigatewayConf ? inputs.apigatewayConf : {} - 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 + const tempApigwConf = inputs.apigatewayConf ? inputs.apigatewayConf : {} + const apigatewayConf = { + serviceId: inputs.serviceId, + region: regionList, + isDisabled: tempApigwConf.isDisabled === true, + fromClientRemark: fromClientRemark, + serviceName: inputs.serviceName || getDefaultServiceName(instance), + description: getDefaultServiceDescription(instance), + protocols: tempApigwConf.protocols || ['http'], + environment: tempApigwConf.environment ? tempApigwConf.environment : 'release', + endpoints: [ + { + path: '/', + enableCORS: tempApigwConf.enableCORS, + serviceTimeout: tempApigwConf.serviceTimeout, + method: 'ANY', + function: { + isIntegratedResponse: true, + functionName: functionConf.name, + functionNamespace: functionConf.namespace + } } - } - ] - if (apigatewayConf.usagePlan) { + ], + customDomains: tempApigwConf.customDomains || [] + } + 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 @@ -318,63 +301,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, - sleep, uploadCodeToCos, mergeJson, capitalString, getDefaultProtocol, - deleteRecord, prepareInputs }