├── .npmignore ├── .npmrc ├── example ├── .gitignore ├── requirements.txt ├── README.md ├── app.py └── serverless.yml ├── .eslintignore ├── .prettierignore ├── src ├── package.json ├── _shims │ ├── sl_handler.py │ └── severless_wsgi.py ├── config.js ├── serverless.js └── utils.js ├── prettier.config.js ├── commitlint.config.js ├── .editorconfig ├── .gitignore ├── serverless.component.yml ├── jest.config.js ├── __tests__ ├── lib │ └── utils.js └── index.test.js ├── LICENSE ├── release.config.js ├── .github └── workflows │ ├── validate.yml │ ├── release.yml │ └── test.yml ├── .eslintrc.js ├── package.json ├── README.md ├── CHANGELOG.md └── docs └── configure.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | requirements 2 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | werkzeug==0.16.0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | example 5 | *.test.js 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | CHANGELOG.md 5 | *.test.js -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "download": "^8.0.0", 4 | "tencent-component-toolkit": "^1.19.8", 5 | "type": "^2.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Flask example 2 | 3 | 本地启动服务: 4 | 5 | ```bash 6 | $ ENV=local python app.py 7 | ``` 8 | 9 | 可以发现 `app.py` 中通过判断环境变量 `ENV` 为 `local` 才启动服务,云函数运行时就不会启动服务。 10 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | printWidth: 100, 4 | semi: false, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'none' 8 | } 9 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | const Configuration = { 2 | /* 3 | * Resolve and load @commitlint/config-conventional from node_modules. 4 | * Referenced packages must be installed 5 | */ 6 | extends: ['@commitlint/config-conventional'] 7 | } 8 | 9 | module.exports = Configuration 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_size = 2 11 | indent_style = space 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/_shims/sl_handler.py: -------------------------------------------------------------------------------- 1 | import app # Replace with your actual application 2 | import severless_wsgi 3 | 4 | # If you need to send additional content types as text, add then directly 5 | # to the whitelist: 6 | # 7 | # serverless_wsgi.TEXT_MIME_TYPES.append("application/custom+json") 8 | 9 | def handler(event, context): 10 | return severless_wsgi.handle_request(app.app, event, context) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | *.log 5 | .serverless 6 | v8-compile-cache-* 7 | jest/* 8 | coverage 9 | .serverless_plugins 10 | testProjects/*/package-lock.json 11 | testProjects/*/yarn.lock 12 | .serverlessUnzipped 13 | node_modules 14 | .vscode/ 15 | .eslintcache 16 | dist 17 | .idea 18 | build/ 19 | .env* 20 | env.js 21 | package-lock.json 22 | test 23 | yarn.lock -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const CONFIGS = { 2 | templateUrl: 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/flask-demo.zip', 3 | compName: 'flask', 4 | compFullname: 'Flask', 5 | handler: 'sl_handler.handler', 6 | runtime: 'Python3.6', 7 | timeout: 3, 8 | memorySize: 128, 9 | namespace: 'default', 10 | description: 'Created by Serverless Component' 11 | } 12 | 13 | module.exports = CONFIGS 14 | -------------------------------------------------------------------------------- /serverless.component.yml: -------------------------------------------------------------------------------- 1 | name: flask 2 | version: 0.0.10 3 | author: 'Tencent Cloud, Inc' 4 | org: 'Tencent Cloud, Inc' 5 | description: Deploy a serverless Flask application onto Tencent SCF and API Gateway. 6 | keywords: 'tencent, serverless, flask' 7 | repo: 'https://github.com/serverless-components/tencent-flask' 8 | readme: 'https://github.com/serverless-components/tencent-flask/tree/master/README.md' 9 | license: MIT 10 | main: ./src 11 | webDeployable: true 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | require('dotenv').config({ path: join(__dirname, '.env.test') }) 3 | 4 | const config = { 5 | verbose: true, 6 | silent: false, 7 | testTimeout: 600000, 8 | testEnvironment: 'node', 9 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$', 10 | testPathIgnorePatterns: ['/node_modules/', '/__tests__/lib/'], 11 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 12 | } 13 | 14 | module.exports = config 15 | -------------------------------------------------------------------------------- /example/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, jsonify 3 | app = Flask(__name__) 4 | 5 | 6 | @app.route("/") 7 | def index(): 8 | return "Flask Restful API Created By Serverless Component" 9 | 10 | 11 | @app.route("/users") 12 | def users(): 13 | users = [{'name': 'test1'}, {'name': 'test2'}] 14 | return jsonify(data=users) 15 | 16 | 17 | @app.route("/users/") 18 | def user(id): 19 | return jsonify(data={'name': 'test1'}) 20 | 21 | isLocal = os.getenv('ENV') == 'local' 22 | if isLocal: 23 | app.run(host='0.0.0.0',port=3000,debug=True) 24 | -------------------------------------------------------------------------------- /__tests__/lib/utils.js: -------------------------------------------------------------------------------- 1 | const { ServerlessSDK } = require('@serverless/platform-client-china') 2 | 3 | /* 4 | * Generate random id 5 | */ 6 | const generateId = () => 7 | Math.random() 8 | .toString(36) 9 | .substring(6) 10 | 11 | /* 12 | * Initializes and returns an instance of the serverless sdk 13 | * @param ${string} orgName - the serverless org name. 14 | */ 15 | const getServerlessSdk = (orgName) => { 16 | const sdk = new ServerlessSDK({ 17 | context: { 18 | orgName 19 | } 20 | }) 21 | return sdk 22 | } 23 | 24 | module.exports = { generateId, getServerlessSdk } 25 | -------------------------------------------------------------------------------- /example/serverless.yml: -------------------------------------------------------------------------------- 1 | org: orgDemo 2 | app: appDemo 3 | stage: dev 4 | component: flask 5 | name: flaskDemo 6 | 7 | inputs: 8 | src: 9 | # TODO: 安装python项目依赖到项目当前目录 10 | hook: 'pip3 install -r requirements.txt -t ./requirements' 11 | dist: ./ 12 | include: 13 | - source: ./requirements 14 | prefix: ../ # prefix, can make ./requirements files/dir to ./ 15 | exclude: 16 | - .env 17 | - 'requirements/**' 18 | region: ap-guangzhou 19 | runtime: Python3.6 20 | apigatewayConf: 21 | protocols: 22 | - http 23 | - https 24 | environment: release 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tencent Cloud, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verifyConditions: [ 3 | '@semantic-release/changelog', 4 | '@semantic-release/git', 5 | '@semantic-release/github' 6 | ], 7 | plugins: [ 8 | [ 9 | '@semantic-release/commit-analyzer', 10 | { 11 | preset: 'angular', 12 | parserOpts: { 13 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'] 14 | } 15 | } 16 | ], 17 | [ 18 | '@semantic-release/release-notes-generator', 19 | { 20 | preset: 'angular', 21 | parserOpts: { 22 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'] 23 | }, 24 | writerOpts: { 25 | commitsSort: ['subject', 'scope'] 26 | } 27 | } 28 | ], 29 | [ 30 | '@semantic-release/changelog', 31 | { 32 | changelogFile: 'CHANGELOG.md' 33 | } 34 | ], 35 | [ 36 | '@semantic-release/git', 37 | { 38 | assets: ['package.json', 'src/**', 'CHANGELOG.md'], 39 | message: 'chore(release): version ${nextRelease.version} \n\n${nextRelease.notes}' 40 | } 41 | ], 42 | [ 43 | '@semantic-release/github', 44 | { 45 | assets: ['!.env'] 46 | } 47 | ] 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | lintAndFormatting: 9 | name: Lint & Formatting 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | with: 15 | # Ensure connection with 'master' branch 16 | fetch-depth: 2 17 | 18 | - name: Install Node.js and npm 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14.x 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Retrieve dependencies from cache 25 | id: cacheNpm 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.npm 30 | node_modules 31 | key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 32 | restore-keys: | 33 | npm-v14-${{ runner.os }}-${{ github.ref }}- 34 | npm-v14-${{ runner.os }}-refs/heads/master- 35 | 36 | - name: Install dependencies 37 | if: steps.cacheNpm.outputs.cache-hit != 'true' 38 | run: | 39 | npm update --no-save 40 | npm update --save-dev --no-save 41 | 42 | - name: Validate Formatting 43 | run: npm run prettier:fix 44 | - name: Validate Lint rules 45 | run: npm run lint:fix 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | env: 12 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | with: 17 | persist-credentials: false 18 | 19 | - name: Install Node.js and npm 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 14.x 23 | registry-url: https://registry.npmjs.org 24 | 25 | - name: Retrieve dependencies from cache 26 | id: cacheNpm 27 | uses: actions/cache@v2 28 | with: 29 | path: | 30 | ~/.npm 31 | node_modules 32 | key: npm-v14-${{ runner.os }}-refs/heads/master-${{ hashFiles('package.json') }} 33 | restore-keys: npm-v14-${{ runner.os }}-refs/heads/master- 34 | 35 | - name: Install dependencies 36 | if: steps.cacheNpm.outputs.cache-hit != 'true' 37 | run: | 38 | npm update --no-save 39 | npm update --save-dev --no-save 40 | - name: Releasing 41 | run: | 42 | npm run release 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 45 | GIT_AUTHOR_NAME: slsplus 46 | GIT_AUTHOR_EMAIL: slsplus.sz@gmail.com 47 | GIT_COMMITTER_NAME: slsplus 48 | GIT_COMMITTER_EMAIL: slsplus.sz@gmail.com 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | with: 15 | # Ensure connection with 'master' branch 16 | fetch-depth: 2 17 | 18 | - name: Install Node.js and npm 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14.x 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Retrieve dependencies from cache 25 | id: cacheNpm 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.npm 30 | node_modules 31 | key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 32 | restore-keys: | 33 | npm-v14-${{ runner.os }}-${{ github.ref }}- 34 | npm-v14-${{ runner.os }}-refs/heads/master- 35 | 36 | - name: Install dependencies 37 | if: steps.cacheNpm.outputs.cache-hit != 'true' 38 | run: | 39 | npm update --no-save 40 | npm update --save-dev --no-save 41 | - name: Running tests 42 | run: npm run test 43 | env: 44 | SERVERLESS_PLATFORM_VENDOR: tencent 45 | GLOBAL_ACCELERATOR_NA: true 46 | TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} 47 | TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} 48 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { generateId, getServerlessSdk } = require('./lib/utils') 2 | 3 | const instanceYaml = { 4 | org: 'orgDemo', 5 | app: 'appDemo', 6 | component: 'flask@dev', 7 | name: `flask-integration-tests-${generateId()}`, 8 | stage: 'dev', 9 | inputs: { 10 | runtime: 'Python3.6', 11 | region: 'ap-guangzhou', 12 | apigatewayConf: { environment: 'test' } 13 | } 14 | } 15 | 16 | const credentials = { 17 | tencent: { 18 | SecretId: process.env.TENCENT_SECRET_ID, 19 | SecretKey: process.env.TENCENT_SECRET_KEY, 20 | } 21 | } 22 | 23 | const sdk = getServerlessSdk(instanceYaml.org) 24 | 25 | it('should successfully deploy flask app', async () => { 26 | const instance = await sdk.deploy(instanceYaml, credentials) 27 | expect(instance).toBeDefined() 28 | expect(instance.instanceName).toEqual(instanceYaml.name) 29 | // get src from template by default 30 | expect(instance.outputs.templateUrl).toBeDefined() 31 | expect(instance.outputs).toBeDefined() 32 | expect(instance.outputs.region).toEqual(instanceYaml.inputs.region) 33 | expect(instance.outputs.scf).toBeDefined() 34 | expect(instance.outputs.scf.runtime).toEqual(instanceYaml.inputs.runtime) 35 | expect(instance.outputs.apigw).toBeDefined() 36 | expect(instance.outputs.apigw.environment).toEqual(instanceYaml.inputs.apigatewayConf.environment) 37 | }) 38 | 39 | it('should successfully remove flask app', async () => { 40 | await sdk.remove(instanceYaml, credentials) 41 | result = await sdk.getInstance( 42 | instanceYaml.org, 43 | instanceYaml.stage, 44 | instanceYaml.app, 45 | instanceYaml.name 46 | ) 47 | 48 | // remove action won't delete the service cause the apigw have the api binded 49 | expect(result.instance.instanceStatus).toEqual('inactive') 50 | }) 51 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['prettier'], 4 | plugins: ['import', 'prettier'], 5 | env: { 6 | es6: true, 7 | jest: true, 8 | node: true 9 | }, 10 | parser: 'babel-eslint', 11 | parserOptions: { 12 | ecmaVersion: 2018, 13 | sourceType: 'module', 14 | ecmaFeatures: { 15 | jsx: true 16 | } 17 | }, 18 | globals: { 19 | on: true // for the Socket file 20 | }, 21 | rules: { 22 | 'array-bracket-spacing': [ 23 | 'error', 24 | 'never', 25 | { 26 | objectsInArrays: false, 27 | arraysInArrays: false 28 | } 29 | ], 30 | 'arrow-parens': ['error', 'always'], 31 | 'arrow-spacing': ['error', { before: true, after: true }], 32 | 'comma-dangle': ['error', 'never'], 33 | curly: 'error', 34 | 'eol-last': 'error', 35 | 'func-names': 'off', 36 | 'id-length': [ 37 | 'error', 38 | { 39 | min: 1, 40 | max: 50, 41 | properties: 'never', 42 | exceptions: ['e', 'i', 'n', 't', 'x', 'y', 'z', '_', '$'] 43 | } 44 | ], 45 | 'no-alert': 'error', 46 | 'no-console': 'off', 47 | 'no-const-assign': 'error', 48 | 'no-else-return': 'error', 49 | 'no-empty': 'off', 50 | 'no-shadow': 'error', 51 | 'no-undef': 'error', 52 | 'no-unused-vars': 'error', 53 | 'no-use-before-define': 'error', 54 | 'no-useless-constructor': 'error', 55 | 'object-curly-newline': 'off', 56 | 'object-shorthand': 'off', 57 | 'prefer-const': 'error', 58 | 'prefer-destructuring': ['error', { object: true, array: false }], 59 | quotes: [ 60 | 'error', 61 | 'single', 62 | { 63 | allowTemplateLiterals: true, 64 | avoidEscape: true 65 | } 66 | ], 67 | semi: ['error', 'never'], 68 | 'spaced-comment': 'error', 69 | strict: ['error', 'global'], 70 | 'prettier/prettier': 'error' 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless/flask", 3 | "main": "src/serverless.js", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "scripts": { 8 | "test": "jest", 9 | "commitlint": "commitlint -f HEAD@{15}", 10 | "lint": "eslint --ext .js,.ts,.tsx .", 11 | "lint:fix": "eslint --fix --ext .js,.ts,.tsx .", 12 | "prettier": "prettier --check '**/*.{css,html,js,json,md,yaml,yml}'", 13 | "prettier:fix": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'", 14 | "release": "semantic-release", 15 | "release-local": "node -r dotenv/config node_modules/semantic-release/bin/semantic-release --no-ci --dry-run", 16 | "check-dependencies": "npx npm-check --skip-unused --update" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "ygsec && lint-staged", 21 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 22 | "pre-push": "ygsec && npm run lint:fix && npm run prettier:fix" 23 | } 24 | }, 25 | "lint-staged": { 26 | "**/*.{js,ts,tsx}": [ 27 | "npm run lint:fix", 28 | "git add ." 29 | ], 30 | "**/*.{css,html,js,json,md,yaml,yml}": [ 31 | "npm run prettier:fix", 32 | "git add ." 33 | ] 34 | }, 35 | "author": "Tencent Cloud, Inc.", 36 | "license": "MIT", 37 | "dependencies": {}, 38 | "devDependencies": { 39 | "@commitlint/cli": "^8.3.5", 40 | "@commitlint/config-conventional": "^8.3.4", 41 | "@semantic-release/changelog": "^5.0.0", 42 | "@semantic-release/commit-analyzer": "^8.0.1", 43 | "@semantic-release/git": "^9.0.0", 44 | "@semantic-release/npm": "^7.0.4", 45 | "@semantic-release/release-notes-generator": "^9.0.1", 46 | "@serverless/platform-client-china": "^1.0.19", 47 | "@ygkit/secure": "0.0.3", 48 | "babel-eslint": "^10.1.0", 49 | "dotenv": "^8.2.0", 50 | "eslint": "^6.8.0", 51 | "eslint-config-prettier": "^6.10.0", 52 | "eslint-plugin-import": "^2.20.1", 53 | "eslint-plugin-prettier": "^3.1.2", 54 | "husky": "^4.2.5", 55 | "jest": "^25.0.1", 56 | "lint-staged": "^10.0.8", 57 | "prettier": "^1.19.1", 58 | "semantic-release": "^17.0.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️⚠️⚠️ 所有框架组件项目迁移到 [tencent-framework-components](https://github.com/serverless-components/tencent-framework-components). 2 | 3 | [![Serverless Python Flask Tencent Cloud](https://img.serverlesscloud.cn/20191226/1577347052683-flask_%E9%95%BF.png)](http://serverless.com) 4 | 5 | # 腾讯云 Flask Serverless Component 6 | 7 | ## 简介 8 | 9 | 腾讯云 [Flask](https://github.com/pallets/flask) Serverless Component, 支持 Restful API 服务的部署,不支持 Flask Command. 10 | 11 | > 注 :任何支持 WSGI(Web Server Gateway Interface) 的 Python 服务端框架都可以通过该组件进行部署,例如 Falcon 框架等。 12 | 13 | ## 目录 14 | 15 | 0. [准备](#0-准备) 16 | 1. [安装](#1-安装) 17 | 1. [配置](#2-配置) 18 | 1. [部署](#3-部署) 19 | 1. [移除](#4-移除) 20 | 21 | ### 0. 准备 22 | 23 | 在使用此组件之前,需要先初始化一个 Flask 项目,然后将 `Flask` 和 `werkzeug` 添加到依赖文件 `requirements.txt` 中,如下: 24 | 25 | ```txt 26 | Flask==1.0.2 27 | werkzeug==0.16.0 28 | ``` 29 | 30 | 同时新增 API 服务 `app.py`,下面代码仅供参考: 31 | 32 | ```python 33 | from flask import Flask, jsonify 34 | app = Flask(__name__) 35 | 36 | @app.route("/") 37 | def index(): 38 | return "Hello Flask" 39 | 40 | @app.route("/users") 41 | def users(): 42 | users = [{'name': 'test1'}, {'name': 'test2'}] 43 | return jsonify(data=users) 44 | 45 | @app.route("/users/") 46 | def user(id): 47 | return jsonify(data={'name': 'test1'}) 48 | ``` 49 | 50 | ### 1. 安装 51 | 52 | 通过 npm 全局安装 [serverless cli](https://github.com/serverless/serverless) 53 | 54 | ```bash 55 | $ npm install -g serverless 56 | ``` 57 | 58 | ### 2. 配置 59 | 60 | 本地创建 `serverless.yml` 文件,在其中进行如下配置 61 | 62 | ```bash 63 | $ touch serverless.yml 64 | ``` 65 | 66 | ```yml 67 | # serverless.yml 68 | 69 | component: flask 70 | name: flashDemo 71 | org: orgDemo 72 | app: appDemo 73 | stage: dev 74 | 75 | inputs: 76 | src: 77 | hook: 'pip install -r requirements.txt -t ./' 78 | dist: ./ 79 | exclude: 80 | - .env 81 | region: ap-guangzhou 82 | runtime: Python3.6 83 | apigatewayConf: 84 | protocols: 85 | - http 86 | - https 87 | environment: release 88 | ``` 89 | 90 | - [更多配置](https://github.com/serverless-components/tencent-flask/tree/master/docs/configure.md) 91 | 92 | ### 3. 部署 93 | 94 | 如您的账号未 [登陆](https://cloud.tencent.com/login) 或 [注册](https://cloud.tencent.com/register) 腾讯云,您可以直接通过 `微信` 扫描命令行中的二维码进行授权登陆和注册。 95 | 96 | 通过 `sls` 命令进行部署,并可以添加 `--debug` 参数查看部署过程中的信息 97 | 98 | ```bash 99 | $ sls deploy --debug 100 | ``` 101 | 102 | ### 4. 移除 103 | 104 | 通过以下命令移除部署的 API 网关 105 | 106 | ```bash 107 | $ sls remove --debug 108 | ``` 109 | 110 | ### 账号配置(可选) 111 | 112 | 当前默认支持 CLI 扫描二维码登录,如您希望配置持久的环境变量/秘钥信息,也可以本地创建 `.env` 文件 113 | 114 | ```bash 115 | $ touch .env # 腾讯云的配置信息 116 | ``` 117 | 118 | 在 `.env` 文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存 119 | 120 | 如果没有腾讯云账号,可以在此 [注册新账号](https://cloud.tencent.com/register)。 121 | 122 | 如果已有腾讯云账号,可以在 [API 密钥管理](https://console.cloud.tencent.com/cam/capi) 中获取 `SecretId` 和`SecretKey`. 123 | 124 | ```text 125 | # .env 126 | TENCENT_SECRET_ID=123 127 | TENCENT_SECRET_KEY=123 128 | ``` 129 | 130 | ### 更多组件 131 | 132 | 可以在 [Serverless Components](https://github.com/serverless/components/blob/master/README.cn.md) repo 中查询更多组件的信息。 133 | 134 | ## License 135 | 136 | MIT License 137 | 138 | Copyright (c) 2020 Tencent Cloud, Inc. 139 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.10](https://github.com/serverless-components/tencent-flask/compare/v0.0.9...v0.0.10) (2021-02-02) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * multi cookie bug ([97043cb](https://github.com/serverless-components/tencent-flask/commit/97043cb2d0a1d66d448f216309aac92289cc2e7d)) 7 | 8 | ## [0.0.9](https://github.com/serverless-components/tencent-flask/compare/v0.0.8...v0.0.9) (2020-12-15) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * 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)) 14 | 15 | ## [0.0.8](https://github.com/serverless-components/tencent-flask/compare/v0.0.7...v0.0.8) (2020-09-07) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * update deploy flow for multi region ([fe034d1](https://github.com/serverless-components/tencent-flask/commit/fe034d170e434f9b3ac31c1b495957e6f5bf3f3e)) 21 | * update deps ([821f3e6](https://github.com/serverless-components/tencent-flask/commit/821f3e65312332335eb804caefdc8fd928b618aa)) 22 | 23 | ## [0.0.7](https://github.com/serverless-components/tencent-flask/compare/v0.0.6...v0.0.7) (2020-09-02) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * update tencnet-component-toolkit for api mark ([86e5a49](https://github.com/serverless-components/tencent-flask/commit/86e5a498820c8f0312405593033fa9b0590f1478)) 29 | 30 | ## [0.0.6](https://github.com/serverless-components/tencent-flask/compare/v0.0.5...v0.0.6) (2020-09-02) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * support cfs config ([27f4374](https://github.com/serverless-components/tencent-flask/commit/27f437462b664930fd0483119d414705b660071b)) 36 | 37 | ## [0.0.5](https://github.com/serverless-components/tencent-flask/compare/v0.0.4...v0.0.5) (2020-08-26) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * deploy error ([5934744](https://github.com/serverless-components/tencent-flask/commit/59347449c62fec0784a06d373a64a9635786108a)) 43 | * prettier config ([46a7011](https://github.com/serverless-components/tencent-flask/commit/46a701142c21b7fd5a069599a0c429a3942b1e38)) 44 | * support eip config ([aaf8ca1](https://github.com/serverless-components/tencent-flask/commit/aaf8ca1dc166e37f9a635695f0dc2c52ff9f3243)) 45 | * traffic zero display bug ([d80d421](https://github.com/serverless-components/tencent-flask/commit/d80d4218bbd51c51cd4a590efc6cffe4fce6f959)) 46 | * update get credential error message ([3964862](https://github.com/serverless-components/tencent-flask/commit/396486273ead2dcacec85607418bc4f06db95ea2)) 47 | * upgrade deps ([4dfde96](https://github.com/serverless-components/tencent-flask/commit/4dfde9610102d0cd7b081b42b329474df8513378)) 48 | * upgrade deps ([b0366d7](https://github.com/serverless-components/tencent-flask/commit/b0366d77d78c754eecf0d06c1ec2d7aa6197ff58)) 49 | * upgrade deps ([7a6aead](https://github.com/serverless-components/tencent-flask/commit/7a6aead877bfe58a0168c6ff9628ec250c292cee)) 50 | 51 | 52 | ### Features 53 | 54 | * init v2 ([245ce3a](https://github.com/serverless-components/tencent-flask/commit/245ce3a09e36e3224ead0381c97ab7d684d67903)) 55 | * support scf publish version and traffic setup ([661bd44](https://github.com/serverless-components/tencent-flask/commit/661bd449a7c51801163f537e4ea12837542f119b)) 56 | * support usage plan & auth ([f8084f5](https://github.com/serverless-components/tencent-flask/commit/f8084f5fa3d506ddc9f8e37fb8b53a0afd6183ad)) 57 | * update config & docs ([cb3f8d4](https://github.com/serverless-components/tencent-flask/commit/cb3f8d4c939041cfcec09a62d371453e5c4ec9f5)) 58 | -------------------------------------------------------------------------------- /src/_shims/severless_wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This module converts an AWS API Gateway proxied request to a WSGI request. 5 | 6 | Inspired by: https://github.com/miserlou/zappa 7 | 8 | Author: Logan Raarup 9 | """ 10 | import base64 11 | import os 12 | import sys 13 | from werkzeug.datastructures import Headers, MultiDict 14 | from werkzeug.wrappers import Response 15 | from werkzeug.urls import url_encode, url_unquote 16 | from werkzeug.http import HTTP_STATUS_CODES 17 | from werkzeug._compat import BytesIO, string_types, to_bytes, wsgi_encoding_dance 18 | 19 | # List of MIME types that should not be base64 encoded. MIME types within `text/*` 20 | # are included by default. 21 | TEXT_MIME_TYPES = [ 22 | "application/json", 23 | "application/javascript", 24 | "application/xml", 25 | "application/vnd.api+json", 26 | "image/svg+xml", 27 | ] 28 | 29 | 30 | def all_casings(input_string): 31 | """ 32 | Permute all casings of a given string. 33 | A pretty algoritm, via @Amber 34 | http://stackoverflow.com/questions/6792803/finding-all-possible-case-permutations-in-python 35 | """ 36 | if not input_string: 37 | yield "" 38 | else: 39 | first = input_string[:1] 40 | if first.lower() == first.upper(): 41 | for sub_casing in all_casings(input_string[1:]): 42 | yield first + sub_casing 43 | else: 44 | for sub_casing in all_casings(input_string[1:]): 45 | yield first.lower() + sub_casing 46 | yield first.upper() + sub_casing 47 | 48 | 49 | def split_headers(headers): 50 | """ 51 | If there are multiple occurrences of headers, create case-mutated variations 52 | in order to pass them through APIGW. This is a hack that's currently 53 | needed. See: https://github.com/logandk/serverless-wsgi/issues/11 54 | Source: https://github.com/Miserlou/Zappa/blob/master/zappa/middleware.py 55 | """ 56 | new_headers = {} 57 | 58 | for key in set(headers.keys()): 59 | values = headers.get_all(key) 60 | if len(values) > 1 and key.lower() != 'set-cookie': 61 | for value, casing in zip(values, all_casings(key)): 62 | new_headers[casing] = value 63 | elif key.lower() == 'set-cookie': 64 | new_headers[key] = values 65 | elif len(values) == 1: 66 | new_headers[key] = values[0] 67 | 68 | return new_headers 69 | 70 | 71 | def group_headers(headers): 72 | new_headers = {} 73 | 74 | for key in headers.keys(): 75 | new_headers[key] = headers.get_all(key) 76 | 77 | return new_headers 78 | 79 | 80 | def encode_query_string(event): 81 | multi = event.get(u"multiValueQueryStringParameters") 82 | if multi: 83 | return url_encode(MultiDict((i, j) for i in multi for j in multi[i])) 84 | else: 85 | return url_encode(event.get(u"queryString") or {}) 86 | 87 | 88 | def handle_request(app, event, context): 89 | # if event.get("source") in ["scf.events", "serverless-plugin-warmup"]: 90 | # print("Tencent Cloud Function warming event received, skipping handler") 91 | # return {} 92 | 93 | if u"multiValueHeaders" in event: 94 | headers = Headers(event["multiValueHeaders"]) 95 | else: 96 | headers = Headers(event["headers"]) 97 | 98 | strip_stage_path = os.environ.get("STRIP_STAGE_PATH", "").lower().strip() in [ 99 | "yes", 100 | "y", 101 | "true", 102 | "t", 103 | "1", 104 | ] 105 | if u"apigw.tencentcs.com" in headers.get(u"Host", u"") and not strip_stage_path: 106 | script_name = "/{}".format(event["requestContext"].get(u"stage", "")) 107 | else: 108 | script_name = "" 109 | 110 | # If a user is using a custom domain on API Gateway, they may have a base 111 | # path in their URL. This allows us to strip it out via an optional 112 | # environment variable. 113 | path_info = event["path"] 114 | base_path = os.environ.get("API_GATEWAY_BASE_PATH") 115 | print(base_path) 116 | if base_path: 117 | script_name = "/" + base_path 118 | 119 | if path_info.startswith(script_name): 120 | path_info = path_info[len(script_name) :] or "/" 121 | 122 | if u"body" in event: 123 | body = event[u"body"] or "" 124 | else: 125 | body = "" 126 | 127 | if event.get("isBase64Encoded", False): 128 | body = base64.b64decode(body) 129 | if isinstance(body, string_types): 130 | body = to_bytes(body, charset="utf-8") 131 | 132 | environ = { 133 | "CONTENT_LENGTH": str(len(body)), 134 | "CONTENT_TYPE": headers.get(u"Content-Type", ""), 135 | "PATH_INFO": url_unquote(path_info), 136 | "QUERY_STRING": encode_query_string(event), 137 | "REMOTE_ADDR": event["requestContext"] 138 | .get(u"identity", {}) 139 | .get(u"sourceIp", ""), 140 | "REMOTE_USER": event["requestContext"] 141 | .get(u"authorizer", {}) 142 | .get(u"principalId", ""), 143 | "REQUEST_METHOD": event["httpMethod"], 144 | "SCRIPT_NAME": script_name, 145 | "SERVER_NAME": headers.get(u"Host", "lambda"), 146 | "SERVER_PORT": headers.get(u"X-Forwarded-Port", "80"), 147 | "SERVER_PROTOCOL": "HTTP/1.1", 148 | "wsgi.errors": sys.stderr, 149 | "wsgi.input": BytesIO(body), 150 | "wsgi.multiprocess": False, 151 | "wsgi.multithread": False, 152 | "wsgi.run_once": False, 153 | "wsgi.url_scheme": headers.get(u"X-Forwarded-Proto", "http"), 154 | "wsgi.version": (1, 0), 155 | "serverless.authorizer": event["requestContext"].get(u"authorizer"), 156 | "serverless.event": event, 157 | "serverless.context": context, 158 | # TODO: Deprecate the following entries, as they do not comply with the WSGI 159 | # spec. For custom variables, the spec says: 160 | # 161 | # Finally, the environ dictionary may also contain server-defined variables. 162 | # These variables should be named using only lower-case letters, numbers, dots, 163 | # and underscores, and should be prefixed with a name that is unique to the 164 | # defining server or gateway. 165 | "API_GATEWAY_AUTHORIZER": event["requestContext"].get(u"authorizer"), 166 | "event": event, 167 | "context": context, 168 | } 169 | 170 | for key, value in environ.items(): 171 | if isinstance(value, string_types): 172 | environ[key] = wsgi_encoding_dance(value) 173 | 174 | for key, value in headers.items(): 175 | key = "HTTP_" + key.upper().replace("-", "_") 176 | if key not in ("HTTP_CONTENT_TYPE", "HTTP_CONTENT_LENGTH"): 177 | environ[key] = value 178 | 179 | response = Response.from_app(app, environ) 180 | 181 | returndict = {u"statusCode": response.status_code} 182 | 183 | if u"multiValueHeaders" in event: 184 | returndict["multiValueHeaders"] = group_headers(response.headers) 185 | else: 186 | returndict["headers"] = split_headers(response.headers) 187 | 188 | if event.get("requestContext").get("elb"): 189 | # If the request comes from ALB we need to add a status description 190 | returndict["statusDescription"] = u"%d %s" % ( 191 | response.status_code, 192 | HTTP_STATUS_CODES[response.status_code], 193 | ) 194 | 195 | if response.data: 196 | mimetype = response.mimetype or "text/plain" 197 | if ( 198 | mimetype.startswith("text/") or mimetype in TEXT_MIME_TYPES 199 | ) and not response.headers.get("Content-Encoding", ""): 200 | returndict["body"] = response.get_data(as_text=True) 201 | returndict["isBase64Encoded"] = False 202 | else: 203 | returndict["body"] = base64.b64encode(response.data).decode("utf-8") 204 | returndict["isBase64Encoded"] = True 205 | 206 | return returndict 207 | -------------------------------------------------------------------------------- /src/serverless.js: -------------------------------------------------------------------------------- 1 | const { Component } = require('@serverless/core') 2 | const { Scf, Apigw, Cns, Cam } = require('tencent-component-toolkit') 3 | const { TypeError } = require('tencent-component-toolkit/src/utils/error') 4 | const { uploadCodeToCos, getDefaultProtocol, prepareInputs, deepClone } = require('./utils') 5 | const CONFIGS = require('./config') 6 | 7 | class ServerlessComponent extends Component { 8 | getCredentials() { 9 | const { tmpSecrets } = this.credentials.tencent 10 | 11 | if (!tmpSecrets || !tmpSecrets.TmpSecretId) { 12 | throw new TypeError( 13 | 'CREDENTIAL', 14 | 'Cannot get secretId/Key, your account could be sub-account and does not have the access to use SLS_QcsRole, please make sure the role exists first, then visit https://cloud.tencent.com/document/product/1154/43006, follow the instructions to bind the role to your account.' 15 | ) 16 | } 17 | 18 | return { 19 | SecretId: tmpSecrets.TmpSecretId, 20 | SecretKey: tmpSecrets.TmpSecretKey, 21 | Token: tmpSecrets.Token 22 | } 23 | } 24 | 25 | getAppId() { 26 | return this.credentials.tencent.tmpSecrets.appId 27 | } 28 | 29 | async deployFunction(credentials, inputs, regionList) { 30 | if (!inputs.role) { 31 | try { 32 | const camClient = new Cam(credentials) 33 | const roleExist = await camClient.CheckSCFExcuteRole() 34 | if (roleExist) { 35 | inputs.role = 'QCS_SCFExcuteRole' 36 | } 37 | } catch (e) { 38 | // no op 39 | } 40 | } 41 | 42 | const outputs = {} 43 | const appId = this.getAppId() 44 | 45 | const funcDeployer = async (curRegion) => { 46 | const code = await uploadCodeToCos(this, appId, credentials, inputs, curRegion) 47 | const scf = new Scf(credentials, curRegion) 48 | const tempInputs = { 49 | ...inputs, 50 | code 51 | } 52 | const scfOutput = await scf.deploy(deepClone(tempInputs)) 53 | outputs[curRegion] = { 54 | functionName: scfOutput.FunctionName, 55 | runtime: scfOutput.Runtime, 56 | namespace: scfOutput.Namespace 57 | } 58 | 59 | this.state[curRegion] = { 60 | ...(this.state[curRegion] ? this.state[curRegion] : {}), 61 | ...outputs[curRegion] 62 | } 63 | 64 | // default version is $LATEST 65 | outputs[curRegion].lastVersion = scfOutput.LastVersion 66 | ? scfOutput.LastVersion 67 | : this.state.lastVersion || '$LATEST' 68 | 69 | // default traffic is 1.0, it can also be 0, so we should compare to undefined 70 | outputs[curRegion].traffic = 71 | scfOutput.Traffic !== undefined 72 | ? scfOutput.Traffic 73 | : this.state.traffic !== undefined 74 | ? this.state.traffic 75 | : 1 76 | 77 | if (outputs[curRegion].traffic !== 1 && scfOutput.ConfigTrafficVersion) { 78 | outputs[curRegion].configTrafficVersion = scfOutput.ConfigTrafficVersion 79 | this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion 80 | } 81 | 82 | this.state.lastVersion = outputs[curRegion].lastVersion 83 | this.state.traffic = outputs[curRegion].traffic 84 | } 85 | 86 | for (let i = 0; i < regionList.length; i++) { 87 | const curRegion = regionList[i] 88 | await funcDeployer(curRegion) 89 | } 90 | this.save() 91 | return outputs 92 | } 93 | 94 | // try to add dns record 95 | async tryToAddDnsRecord(credentials, customDomains) { 96 | try { 97 | const cns = new Cns(credentials) 98 | for (let i = 0; i < customDomains.length; i++) { 99 | const item = customDomains[i] 100 | if (item.domainPrefix) { 101 | await cns.deploy({ 102 | domain: item.subDomain.replace(`${item.domainPrefix}.`, ''), 103 | records: [ 104 | { 105 | subDomain: item.domainPrefix, 106 | recordType: 'CNAME', 107 | recordLine: '默认', 108 | value: item.cname, 109 | ttl: 600, 110 | mx: 10, 111 | status: 'enable' 112 | } 113 | ] 114 | }) 115 | } 116 | } 117 | } catch (e) { 118 | console.log('METHOD_tryToAddDnsRecord', e.message) 119 | } 120 | } 121 | 122 | async deployApigateway(credentials, inputs, regionList) { 123 | if (inputs.isDisabled) { 124 | return {} 125 | } 126 | 127 | const getServiceId = (instance, region) => { 128 | const regionState = instance.state[region] 129 | return inputs.serviceId || (regionState && regionState.serviceId) 130 | } 131 | 132 | const deployTasks = [] 133 | const outputs = {} 134 | regionList.forEach((curRegion) => { 135 | const apigwDeployer = async () => { 136 | const apigw = new Apigw(credentials, curRegion) 137 | 138 | const oldState = this.state[curRegion] || {} 139 | const apigwInputs = { 140 | ...inputs, 141 | oldState: { 142 | apiList: oldState.apiList || [], 143 | customDomains: oldState.customDomains || [] 144 | } 145 | } 146 | // different region deployment has different service id 147 | apigwInputs.serviceId = getServiceId(this, curRegion) 148 | const apigwOutput = await apigw.deploy(deepClone(apigwInputs)) 149 | outputs[curRegion] = { 150 | serviceId: apigwOutput.serviceId, 151 | subDomain: apigwOutput.subDomain, 152 | environment: apigwOutput.environment, 153 | url: `${getDefaultProtocol(inputs.protocols)}://${apigwOutput.subDomain}/${ 154 | apigwOutput.environment 155 | }/` 156 | } 157 | 158 | if (apigwOutput.customDomains) { 159 | // TODO: need confirm add cns authentication 160 | if (inputs.autoAddDnsRecord === true) { 161 | // await this.tryToAddDnsRecord(credentials, apigwOutput.customDomains) 162 | } 163 | outputs[curRegion].customDomains = apigwOutput.customDomains 164 | } 165 | this.state[curRegion] = { 166 | created: true, 167 | ...(this.state[curRegion] ? this.state[curRegion] : {}), 168 | ...outputs[curRegion], 169 | apiList: apigwOutput.apiList 170 | } 171 | } 172 | deployTasks.push(apigwDeployer()) 173 | }) 174 | 175 | await Promise.all(deployTasks) 176 | 177 | this.save() 178 | return outputs 179 | } 180 | 181 | async deploy(inputs) { 182 | console.log(`Deploying ${CONFIGS.compFullname} App...`) 183 | 184 | const credentials = this.getCredentials() 185 | 186 | // 对Inputs内容进行标准化 187 | const { regionList, functionConf, apigatewayConf } = await prepareInputs( 188 | this, 189 | credentials, 190 | inputs 191 | ) 192 | 193 | // 部署函数 + API网关 194 | const outputs = {} 195 | if (!functionConf.code.src) { 196 | outputs.templateUrl = CONFIGS.templateUrl 197 | } 198 | 199 | let apigwOutputs 200 | const functionOutputs = await this.deployFunction( 201 | credentials, 202 | functionConf, 203 | regionList, 204 | outputs 205 | ) 206 | // support apigatewayConf.isDisabled 207 | if (apigatewayConf.isDisabled !== true) { 208 | apigwOutputs = await this.deployApigateway(credentials, apigatewayConf, regionList, outputs) 209 | } else { 210 | this.state.apigwDisabled = true 211 | } 212 | 213 | // optimize outputs for one region 214 | if (regionList.length === 1) { 215 | const [oneRegion] = regionList 216 | outputs.region = oneRegion 217 | outputs['scf'] = functionOutputs[oneRegion] 218 | if (apigwOutputs) { 219 | outputs['apigw'] = apigwOutputs[oneRegion] 220 | } 221 | } else { 222 | outputs['scf'] = functionOutputs 223 | if (apigwOutputs) { 224 | outputs['apigw'] = apigwOutputs 225 | } 226 | } 227 | 228 | this.state.region = regionList[0] 229 | this.state.regionList = regionList 230 | this.state.lambdaArn = functionConf.name 231 | 232 | return outputs 233 | } 234 | 235 | async remove() { 236 | console.log(`Removing ${CONFIGS.compFullname} App...`) 237 | 238 | const { state } = this 239 | const { regionList = [] } = state 240 | 241 | const credentials = this.getCredentials() 242 | 243 | const removeHandlers = [] 244 | for (let i = 0; i < regionList.length; i++) { 245 | const curRegion = regionList[i] 246 | const curState = state[curRegion] 247 | const scf = new Scf(credentials, curRegion) 248 | const apigw = new Apigw(credentials, curRegion) 249 | const handler = async () => { 250 | // if disable apigw, no need to remove 251 | if (state.apigwDisabled !== true) { 252 | await apigw.remove({ 253 | created: curState.created, 254 | environment: curState.environment, 255 | serviceId: curState.serviceId, 256 | apiList: curState.apiList, 257 | customDomains: curState.customDomains 258 | }) 259 | } 260 | await scf.remove({ 261 | functionName: curState.functionName, 262 | namespace: curState.namespace 263 | }) 264 | } 265 | removeHandlers.push(handler()) 266 | } 267 | 268 | await Promise.all(removeHandlers) 269 | 270 | if (this.state.cns) { 271 | const cns = new Cns(credentials) 272 | for (let i = 0; i < this.state.cns.length; i++) { 273 | await cns.remove({ deleteList: this.state.cns[i].records }) 274 | } 275 | } 276 | 277 | this.state = {} 278 | } 279 | } 280 | 281 | module.exports = ServerlessComponent 282 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const { Cos } = require('tencent-component-toolkit') 4 | const ensureObject = require('type/object/ensure') 5 | const ensureIterable = require('type/iterable/ensure') 6 | const ensureString = require('type/string/ensure') 7 | const download = require('download') 8 | const { TypeError } = require('tencent-component-toolkit/src/utils/error') 9 | const CONFIGS = require('./config') 10 | 11 | /* 12 | * Generates a random id 13 | */ 14 | const generateId = () => 15 | Math.random() 16 | .toString(36) 17 | .substring(6) 18 | 19 | const deepClone = (obj) => { 20 | return JSON.parse(JSON.stringify(obj)) 21 | } 22 | 23 | const getType = (obj) => { 24 | return Object.prototype.toString.call(obj).slice(8, -1) 25 | } 26 | 27 | const mergeJson = (sourceJson, targetJson) => { 28 | Object.entries(sourceJson).forEach(([key, val]) => { 29 | targetJson[key] = deepClone(val) 30 | }) 31 | return targetJson 32 | } 33 | 34 | const capitalString = (str) => { 35 | if (str.length < 2) { 36 | return str.toUpperCase() 37 | } 38 | 39 | return `${str[0].toUpperCase()}${str.slice(1)}` 40 | } 41 | 42 | const getDefaultProtocol = (protocols) => { 43 | return String(protocols).includes('https') ? 'https' : 'http' 44 | } 45 | 46 | const getDefaultFunctionName = () => { 47 | return `${CONFIGS.compName}_component_${generateId()}` 48 | } 49 | 50 | const getDefaultServiceName = () => { 51 | return 'serverless' 52 | } 53 | 54 | const getDefaultServiceDescription = () => { 55 | return 'Created by Serverless Component' 56 | } 57 | 58 | const validateTraffic = (num) => { 59 | if (getType(num) !== 'Number') { 60 | throw new TypeError( 61 | `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, 62 | 'traffic must be a number' 63 | ) 64 | } 65 | if (num < 0 || num > 1) { 66 | throw new TypeError( 67 | `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, 68 | 'traffic must be a number between 0 and 1' 69 | ) 70 | } 71 | return true 72 | } 73 | 74 | const getCodeZipPath = async (instance, inputs) => { 75 | console.log(`Packaging ${CONFIGS.compFullname} application...`) 76 | 77 | // unzip source zip file 78 | let zipPath 79 | if (!inputs.code.src) { 80 | // add default template 81 | const downloadPath = `/tmp/${generateId()}` 82 | const filename = 'template' 83 | 84 | console.log(`Installing Default ${CONFIGS.compFullname} App...`) 85 | try { 86 | await download(CONFIGS.templateUrl, downloadPath, { 87 | filename: `${filename}.zip` 88 | }) 89 | } catch (e) { 90 | throw new TypeError(`DOWNLOAD_TEMPLATE`, 'Download default template failed.') 91 | } 92 | zipPath = `${downloadPath}/${filename}.zip` 93 | } else { 94 | zipPath = inputs.code.src 95 | } 96 | 97 | return zipPath 98 | } 99 | 100 | const getDirFiles = async (dirPath) => { 101 | const targetPath = path.resolve(dirPath) 102 | const files = fs.readdirSync(targetPath) 103 | const temp = {} 104 | files.forEach((file) => { 105 | temp[file] = path.join(targetPath, file) 106 | }) 107 | return temp 108 | } 109 | 110 | /** 111 | * Upload code to COS 112 | * @param {Component} instance serverless component instance 113 | * @param {string} appId app id 114 | * @param {object} credentials credentials 115 | * @param {object} inputs component inputs parameters 116 | * @param {string} region region 117 | */ 118 | const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => { 119 | const bucketName = inputs.code.bucket || `sls-cloudfunction-${region}-code` 120 | const objectName = inputs.code.object || `${inputs.name}-${Math.floor(Date.now() / 1000)}.zip` 121 | // if set bucket and object not pack code 122 | if (!inputs.code.bucket || !inputs.code.object) { 123 | const zipPath = await getCodeZipPath(instance, inputs) 124 | console.log(`Code zip path ${zipPath}`) 125 | 126 | // save the zip path to state for lambda to use it 127 | instance.state.zipPath = zipPath 128 | 129 | const cos = new Cos(credentials, region) 130 | 131 | if (!inputs.code.bucket) { 132 | // create default bucket 133 | await cos.deploy({ 134 | bucket: bucketName + '-' + appId, 135 | force: true, 136 | lifecycle: [ 137 | { 138 | status: 'Enabled', 139 | id: 'deleteObject', 140 | filter: '', 141 | expiration: { days: '10' }, 142 | abortIncompleteMultipartUpload: { daysAfterInitiation: '10' } 143 | } 144 | ] 145 | }) 146 | } 147 | 148 | // upload code to cos 149 | if (!inputs.code.object) { 150 | console.log(`Getting cos upload url for bucket ${bucketName}`) 151 | const uploadUrl = await cos.getObjectUrl({ 152 | bucket: bucketName + '-' + appId, 153 | object: objectName, 154 | method: 'PUT' 155 | }) 156 | 157 | // if shims and sls sdk entries had been injected to zipPath, no need to injected again 158 | console.log(`Uploading code to bucket ${bucketName}`) 159 | if (instance.codeInjected === true) { 160 | await instance.uploadSourceZipToCOS(zipPath, uploadUrl, {}, {}) 161 | } else { 162 | const shimFiles = await getDirFiles(path.join(__dirname, '_shims')) 163 | await instance.uploadSourceZipToCOS(zipPath, uploadUrl, shimFiles, {}) 164 | instance.codeInjected = true 165 | } 166 | console.log(`Upload ${objectName} to bucket ${bucketName} success`) 167 | } 168 | } 169 | 170 | // save bucket state 171 | instance.state.bucket = bucketName 172 | instance.state.object = objectName 173 | 174 | return { 175 | bucket: bucketName, 176 | object: objectName 177 | } 178 | } 179 | 180 | const prepareInputs = async (instance, credentials, inputs = {}) => { 181 | // 对function inputs进行标准化 182 | const tempFunctionConf = inputs.functionConf ? inputs.functionConf : {} 183 | const fromClientRemark = `tencent-${CONFIGS.compName}` 184 | const regionList = inputs.region 185 | ? typeof inputs.region == 'string' 186 | ? [inputs.region] 187 | : inputs.region 188 | : ['ap-guangzhou'] 189 | 190 | // chenck state function name 191 | const stateFunctionName = 192 | instance.state[regionList[0]] && instance.state[regionList[0]].functionName 193 | const functionConf = { 194 | code: { 195 | src: inputs.src, 196 | bucket: inputs.srcOriginal && inputs.srcOriginal.bucket, 197 | object: inputs.srcOriginal && inputs.srcOriginal.object 198 | }, 199 | name: 200 | ensureString(inputs.functionName, { isOptional: true }) || 201 | stateFunctionName || 202 | getDefaultFunctionName(), 203 | region: regionList, 204 | role: ensureString(tempFunctionConf.role ? tempFunctionConf.role : inputs.role, { 205 | default: '' 206 | }), 207 | handler: ensureString(tempFunctionConf.handler ? tempFunctionConf.handler : inputs.handler, { 208 | default: CONFIGS.handler 209 | }), 210 | runtime: ensureString(tempFunctionConf.runtime ? tempFunctionConf.runtime : inputs.runtime, { 211 | default: CONFIGS.runtime 212 | }), 213 | namespace: ensureString( 214 | tempFunctionConf.namespace ? tempFunctionConf.namespace : inputs.namespace, 215 | { default: CONFIGS.namespace } 216 | ), 217 | description: ensureString( 218 | tempFunctionConf.description ? tempFunctionConf.description : inputs.description, 219 | { 220 | default: CONFIGS.description 221 | } 222 | ), 223 | fromClientRemark, 224 | layers: ensureIterable(tempFunctionConf.layers ? tempFunctionConf.layers : inputs.layers, { 225 | default: [] 226 | }), 227 | cfs: ensureIterable(tempFunctionConf.cfs ? tempFunctionConf.cfs : inputs.cfs, { 228 | default: [] 229 | }), 230 | publish: inputs.publish, 231 | traffic: inputs.traffic, 232 | lastVersion: instance.state.lastVersion, 233 | eip: tempFunctionConf.eip === true, 234 | l5Enable: tempFunctionConf.l5Enable === true, 235 | timeout: tempFunctionConf.timeout ? tempFunctionConf.timeout : CONFIGS.timeout, 236 | memorySize: tempFunctionConf.memorySize ? tempFunctionConf.memorySize : CONFIGS.memorySize, 237 | tags: ensureObject(tempFunctionConf.tags ? tempFunctionConf.tags : inputs.tag, { 238 | default: null 239 | }) 240 | } 241 | 242 | // validate traffic 243 | if (inputs.traffic !== undefined) { 244 | validateTraffic(inputs.traffic) 245 | } 246 | functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion 247 | 248 | if (tempFunctionConf.environment) { 249 | functionConf.environment = inputs.functionConf.environment 250 | } 251 | if (tempFunctionConf.vpcConfig) { 252 | functionConf.vpcConfig = inputs.functionConf.vpcConfig 253 | } 254 | 255 | // 对apigw inputs进行标准化 256 | const tempApigwConf = inputs.apigatewayConf ? inputs.apigatewayConf : {} 257 | const apigatewayConf = { 258 | serviceId: inputs.serviceId, 259 | region: regionList, 260 | isDisabled: tempApigwConf.isDisabled === true, 261 | fromClientRemark: fromClientRemark, 262 | serviceName: inputs.serviceName || getDefaultServiceName(instance), 263 | description: getDefaultServiceDescription(instance), 264 | protocols: tempApigwConf.protocols || ['http'], 265 | environment: tempApigwConf.environment ? tempApigwConf.environment : 'release', 266 | endpoints: [ 267 | { 268 | path: '/', 269 | enableCORS: tempApigwConf.enableCORS, 270 | serviceTimeout: tempApigwConf.serviceTimeout, 271 | method: 'ANY', 272 | function: { 273 | isIntegratedResponse: true, 274 | functionName: functionConf.name, 275 | functionNamespace: functionConf.namespace 276 | } 277 | } 278 | ], 279 | customDomains: tempApigwConf.customDomains || [] 280 | } 281 | if (tempApigwConf.usagePlan) { 282 | apigatewayConf.endpoints[0].usagePlan = { 283 | usagePlanId: tempApigwConf.usagePlan.usagePlanId, 284 | usagePlanName: tempApigwConf.usagePlan.usagePlanName, 285 | usagePlanDesc: tempApigwConf.usagePlan.usagePlanDesc, 286 | maxRequestNum: tempApigwConf.usagePlan.maxRequestNum 287 | } 288 | } 289 | if (tempApigwConf.auth) { 290 | apigatewayConf.endpoints[0].auth = { 291 | secretName: tempApigwConf.auth.secretName, 292 | secretIds: tempApigwConf.auth.secretIds 293 | } 294 | } 295 | 296 | regionList.forEach((curRegion) => { 297 | const curRegionConf = inputs[curRegion] 298 | if (curRegionConf && curRegionConf.functionConf) { 299 | functionConf[curRegion] = curRegionConf.functionConf 300 | } 301 | if (curRegionConf && curRegionConf.apigatewayConf) { 302 | apigatewayConf[curRegion] = curRegionConf.apigatewayConf 303 | } 304 | }) 305 | 306 | return { 307 | regionList, 308 | functionConf, 309 | apigatewayConf 310 | } 311 | } 312 | 313 | module.exports = { 314 | deepClone, 315 | generateId, 316 | uploadCodeToCos, 317 | mergeJson, 318 | capitalString, 319 | getDefaultProtocol, 320 | prepareInputs 321 | } 322 | -------------------------------------------------------------------------------- /docs/configure.md: -------------------------------------------------------------------------------- 1 | # 配置文档 2 | 3 | ## 全部配置 4 | 5 | ```yml 6 | # serverless.yml 7 | 8 | component: flask # (必选) 组件名称,在该实例中为flask 9 | name: flaskDemo # 必选) 组件实例名称. 10 | org: orgDemo # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid,必须为字符串 11 | app: appDemo # (可选) 用于记录组织信息. 默认与name相同,必须为字符串 12 | stage: dev # (可选) 用于区分环境信息,默认值是 dev 13 | 14 | inputs: 15 | region: ap-guangzhou # 云函数所在区域 16 | functionName: flaskDemo # 云函数名称 17 | serviceName: mytest # api网关服务名称 18 | runtime: Nodejs10.15 # 运行环境 19 | serviceId: service-np1uloxw # api网关服务ID 20 | src: ./src # 第一种为string时,会打包src对应目录下的代码上传到默认cos上。 21 | # src: # 第二种,部署src下的文件代码,并打包成zip上传到bucket上 22 | # src: ./src # 本地需要打包的文件目录 23 | # bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid 24 | # exclude: # 被排除的文件或目录 25 | # - .env 26 | # - node_modules 27 | # src: # 第三种,在指定存储桶bucket中已经存在了object代码,直接部署 28 | # bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid 29 | # object: cos.zip # bucket key 指定存储桶内的文件 30 | layers: 31 | - name: layerName # layer名称 32 | version: 1 # 版本 33 | functionConf: # 函数配置相关 34 | timeout: 10 # 超时时间,单位秒 35 | eip: false # 是否固定出口IP 36 | memorySize: 128 # 内存大小,单位MB 37 | environment: # 环境变量 38 | variables: # 环境变量数组 39 | TEST: vale 40 | vpcConfig: # 私有网络配置 41 | vpcId: '' # 私有网络的Id 42 | subnetId: '' # 子网ID 43 | apigatewayConf: # api网关配置 44 | isDisabled: false # 是否禁用自动创建 API 网关功能 45 | enableCORS: true # 允许跨域 46 | customDomains: # 自定义域名绑定 47 | - domain: abc.com # 待绑定的自定义的域名 48 | certificateId: abcdefg # 待绑定自定义域名的证书唯一 ID 49 | # 如要设置自定义路径映射,请设置为 false 50 | isDefaultMapping: false 51 | # 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 52 | pathMappingSet: 53 | - path: / 54 | environment: release 55 | protocols: # 绑定自定义域名的协议类型,默认与服务的前端协议一致。 56 | - http # 支持http协议 57 | - https # 支持https协议 58 | protocols: 59 | - http 60 | - https 61 | environment: test 62 | serviceTimeout: 15 63 | usagePlan: # 用户使用计划 64 | usagePlanId: 1111 65 | usagePlanName: slscmp 66 | usagePlanDesc: sls create 67 | maxRequestNum: 1000 68 | auth: # 密钥 69 | secretName: secret 70 | secretIds: 71 | - xxx 72 | ``` 73 | 74 | ## 配置描述 75 | 76 | 主要的参数 77 | 78 | | 参数名称 | 是否必选 | 默认值 | 描述 | 79 | | ------------------------------------ | :------: | :-------------: | :------------------------------------------------------------- | 80 | | runtime | 否 | `Python3.6` | 执行环境, 目前支持: Python3.6, Python2.7 | 81 | | region | 否 | `ap-guangzhou` | 项目部署所在区域,默认广州区 | 82 | | functionName | 否 | | 云函数名称 | 83 | | serviceName | 否 | | API 网关服务名称, 默认创建一个新的服务名称 | 84 | | serviceId | 否 | | API 网关服务 ID,如果存在将使用这个 API 网关服务 | 85 | | src | 否 | `process.cwd()` | 默认为当前目录, 如果是对象, 配置参数参考 [执行目录](#执行目录) | 86 | | layers | 否 | | 云函数绑定的 layer, 配置参数参考 [层配置](#层配置) | 87 | | [functionConf](#函数配置) | 否 | | 函数配置 | 88 | | [apigatewayConf](#API-网关配置) | 否 | | API 网关配置 | 89 | | [cloudDNSConf](#DNS-配置) | 否 | | DNS 配置 | 90 | | [Region special config](#指定区配置) | 否 | | 指定区配置 | 91 | 92 | ## 执行目录 93 | 94 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 95 | | -------- | :------: | :-------------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 96 | | src | 否 | String | | 代码路径。与 object 不能同时存在。 | 97 | | exclude | 否 | Array of String | | 不包含的文件或路径, 遵守 [glob 语法](https://github.com/isaacs/node-glob) | 98 | | bucket | 否 | String | | bucket 名称。如果配置了 src,表示部署 src 的代码并压缩成 zip 后上传到 bucket-appid 对应的存储桶中;如果配置了 object,表示获取 bucket-appid 对应存储桶中 object 对应的代码进行部署。 | 99 | | object | 否 | String | | 部署的代码在存储桶中的路径。 | 100 | 101 | ## 层配置 102 | 103 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 104 | | -------- | :------: | :----: | :----: | :------- | 105 | | name | 否 | String | | 层名称 | 106 | | version | 否 | String | | 层版本号 | 107 | 108 | ### DNS 配置 109 | 110 | 参考: https://cloud.tencent.com/document/product/302/8516 111 | 112 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 113 | | ---------- | :------: | -------- | :----: | :---------------------------------------------- | 114 | | ttl | 否 | Number | `600` | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | 115 | | recordLine | 否 | String[] | | 记录的线路名称 | 116 | 117 | ### 指定区配置 118 | 119 | | 参数名称 | 是否必选 | 类型 | 默认值 | 函数 | 120 | | ------------------------------- | :------: | ------ | ------ | ------------ | 121 | | [functionConf](#函数配置) | 否 | Object | | 函数配置 | 122 | | [apigatewayConf](#API-网关配置) | 否 | Object | | API 网关配置 | 123 | | [cloudDNSConf](#DNS-配置) | 否 | Object | | DNS 配置 | 124 | 125 | ### 函数配置 126 | 127 | 参考: https://cloud.tencent.com/document/product/583/18586 128 | 129 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 130 | | ----------- | :------: | :-----: | :-----: | :------------------------------------------------------------------------------ | 131 | | timeout | 否 | Number | `3` | 函数最长执行时间,单位为秒,可选值范围 1-900 秒,默认为 3 秒 | 132 | | memorySize | 否 | Number | `128` | 函数运行时内存大小,默认为 128M,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | 133 | | environment | 否 | Object | | 函数的环境变量, 参考 [环境变量](#环境变量) | 134 | | vpcConfig | 否 | Object | | 函数的 VPC 配置, 参考 [VPC 配置](#VPC-配置) | 135 | | eip | 否 | Boolean | `false` | 是否固定出口 IP | 136 | 137 | ##### 环境变量 138 | 139 | | 参数名称 | 类型 | 描述 | 140 | | --------- | ---- | :---------------------------------------- | 141 | | variables | | 环境变量参数, 包含多对 key-value 的键值对 | 142 | 143 | ##### VPC 配置 144 | 145 | | 参数名称 | 类型 | 描述 | 146 | | -------- | ------ | :------ | 147 | | subnetId | String | 子网 ID | 148 | | vpcId | String | VPC ID | 149 | 150 | ### API 网关配置 151 | 152 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 153 | | -------------- | :------: | :------- | :--------- | :--------------------------------------------------------------------------------- | 154 | | protocols | 否 | String[] | `['http']` | 前端请求的类型,如 http,https,http 与 https | 155 | | environment | 否 | String | `release` | 发布环境. 目前支持三种发布环境: test(测试), prepub(预发布) 与 release(发布). | 156 | | usagePlan | 否 | | | 使用计划配置, 参考 [使用计划](#使用计划) | 157 | | auth | 否 | | | API 密钥配置, 参考 [API 密钥](#API-密钥配置) | 158 | | customDomain | 否 | Object[] | | 自定义 API 域名配置, 参考 [自定义域名](#自定义域名) | 159 | | enableCORS | 否 | Boolean | `false` | 开启跨域。默认值为否。 | 160 | | serviceTimeout | 否 | Number | `15` | Api 超时时间,单位: 秒 | 161 | | isDisabled | 否 | Boolean | `false` | 关闭自动创建 API 网关功能。默认值为否,即默认自动创建 API 网关。 | 162 | 163 | ##### 使用计划 164 | 165 | 参考: https://cloud.tencent.com/document/product/628/14947 166 | 167 | | 参数名称 | 是否必选 | 类型 | 描述 | 168 | | ------------- | :------: | ------ | :------------------------------------------------------ | 169 | | usagePlanId | 否 | String | 用户自定义使用计划 ID | 170 | | usagePlanName | 否 | String | 用户自定义的使用计划名称 | 171 | | usagePlanDesc | 否 | String | 用户自定义的使用计划描述 | 172 | | maxRequestNum | 否 | Int | 请求配额总数,如果为空,将使用-1 作为默认值,表示不开启 | 173 | 174 | ##### API 密钥配置 175 | 176 | 参考: https://cloud.tencent.com/document/product/628/14916 177 | 178 | | 参数名称 | 类型 | 描述 | 179 | | ---------- | :----- | :------- | 180 | | secretName | String | 密钥名称 | 181 | | secretIds | String | 密钥 ID | 182 | 183 | ##### 自定义域名 184 | 185 | Refer to: https://cloud.tencent.com/document/product/628/14906 186 | 187 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 188 | | ---------------- | :------: | :------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 189 | | domain | 是 | String | | 待绑定的自定义的域名。 | 190 | | certificateId | 否 | String | | 待绑定自定义域名的证书唯一 ID,如果设置了 type 为 https,则为必选 | 191 | | isDefaultMapping | 否 | String | `true` | 是否使用默认路径映射。为 false 时,表示自定义路径映射,此时 pathMappingSet 必填。 | 192 | | pathMappingSet | 否 | Object[] | `[]` | 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 | 193 | | protocol | 否 | String[] | | 绑定自定义域名的协议类型,默认与服务的前端协议一致。 | 194 | 195 | - 自定义路径映射 196 | 197 | | 参数名称 | 是否必选 | 类型 | Description | 198 | | ----------- | :------: | :----- | :------------- | 199 | | path | 是 | String | 自定义映射路径 | 200 | | environment | 是 | String | 自定义映射环境 | 201 | --------------------------------------------------------------------------------