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 d78fad9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: node_js - -node_js: - - 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 - on: - branch: master - script: - - npm run release diff --git a/CHANGELOG.md b/CHANGELOG.md index 01aa130..677f120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +## [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) 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/integration.test.js b/__tests__/index.test.js similarity index 78% rename from tests/integration.test.js rename to __tests__/index.test.js index 4d35f2b..3c172e5 100644 --- a/tests/integration.test.js +++ b/__tests__/index.test.js @@ -1,9 +1,5 @@ -const { generateId, getServerlessSdk } = require('./utils') +const { generateId, getServerlessSdk } = require('./lib/utils') -// set enough timeout for deployment to finish -jest.setTimeout(300000) - -// the yaml file we're testing against const instanceYaml = { org: 'orgDemo', app: 'appDemo', @@ -17,16 +13,17 @@ const instanceYaml = { } } -// get credentials from process.env but need to init empty credentials object const credentials = { - tencent: {} + tencent: { + SecretId: process.env.TENCENT_SECRET_ID, + SecretKey: process.env.TENCENT_SECRET_KEY, + } } -// get serverless construct sdk const sdk = getServerlessSdk(instanceYaml.org) it('should successfully deploy flask app', async () => { - const instance = await sdk.deploy(instanceYaml, { tencent: {} }) + const instance = await sdk.deploy(instanceYaml, credentials) expect(instance).toBeDefined() expect(instance.instanceName).toEqual(instanceYaml.name) // get src from template by default diff --git a/tests/utils.js b/__tests__/lib/utils.js similarity index 100% rename from tests/utils.js rename to __tests__/lib/utils.js 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 cd62520..b932647 100644 --- a/example/app.py +++ b/example/app.py @@ -1,3 +1,4 @@ +import os from flask import Flask, jsonify app = Flask(__name__) @@ -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/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 76bc5fc..6ad2369 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,11 @@ { "name": "@serverless/flask", - "version": "0.0.5", "main": "src/serverless.js", "publishConfig": { "access": "public" }, "scripts": { - "int-test": "jest ./tests/integration.test.js --testEnvironment node", - "test": "npm run lint && npm run prettier && npm run int-test", + "test": "jest", "commitlint": "commitlint -f HEAD@{15}", "lint": "eslint --ext .js,.ts,.tsx .", "lint:fix": "eslint --fix --ext .js,.ts,.tsx .", @@ -19,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": { @@ -46,13 +44,14 @@ "@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", diff --git a/release.config.js b/release.config.js index a13798c..98b3864 100644 --- a/release.config.js +++ b/release.config.js @@ -11,8 +11,7 @@ module.exports = { preset: 'angular', parserOpts: { noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'] - }, - releaseRules: [{ type: 'feat', release: 'patch' }] + } } ], [ diff --git a/serverless.component.yml b/serverless.component.yml index fc7d7ae..8a37f6c 100644 --- a/serverless.component.yml +++ b/serverless.component.yml @@ -1,11 +1,11 @@ name: flask -version: 0.0.5 -author: Tencent Cloud, Inc -org: Tencent Cloud, Inc +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 +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/package.json b/src/package.json index 9a9d323..850fa0c 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,7 @@ { "dependencies": { "download": "^8.0.0", - "tencent-component-toolkit": "^1.13.2", - "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 e961608..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,135 +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 - } - - this.state[curRegion] = { - ...(this.state[curRegion] ? this.state[curRegion] : {}), - ...outputs[curRegion] - } + 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 + } - // default version is $LATEST - outputs[curRegion].lastVersion = scfOutput.LastVersion - ? scfOutput.LastVersion - : this.state.lastVersion || '$LATEST' - - // default traffic is 1.0, it can also be 0, so we should compare to undefined - outputs[curRegion].traffic = - scfOutput.Traffic !== undefined - ? scfOutput.Traffic - : this.state.traffic !== undefined - ? this.state.traffic - : 1 - - if (outputs[curRegion].traffic !== 1 && scfOutput.ConfigTrafficVersion) { - outputs[curRegion].configTrafficVersion = scfOutput.ConfigTrafficVersion - this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion - } + this.state[curRegion] = { + ...(this.state[curRegion] ? this.state[curRegion] : {}), + ...outputs[curRegion] + } - this.state.lastVersion = outputs[curRegion].lastVersion - this.state.traffic = outputs[curRegion].traffic + // 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 } @@ -166,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 @@ -178,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] @@ -225,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({ @@ -239,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 026fa0f..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') @@ -16,10 +16,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( @@ -118,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`) } } @@ -136,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 : {} @@ -212,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, @@ -224,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: '' @@ -249,11 +224,19 @@ 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, eip: tempFunctionConf.eip === true, - l5Enable: tempFunctionConf.l5Enable === 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 + }) } // validate traffic @@ -262,73 +245,55 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { } functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion - functionConf.tags = ensureObject(tempFunctionConf.tags ? tempFunctionConf.tags : inputs.tag, { - default: null - }) - - 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 = inputs.functionConf.environment + } + if (tempFunctionConf.vpcConfig) { + functionConf.vpcConfig = inputs.functionConf.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 : {} + 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 @@ -336,62 +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, uploadCodeToCos, mergeJson, capitalString, getDefaultProtocol, - deleteRecord, prepareInputs }