From 31a9129a8c526425a14f578eeac93d0b73b2cdcd Mon Sep 17 00:00:00 2001 From: Avril Li Date: Wed, 30 Jun 2021 11:38:30 +0800 Subject: [PATCH] feat: support deploy web type function --- README.md | 21 ++-- __tests__/index.test.js | 91 ++++++++++++++++ docs/configure.md | 64 +++++------ example/web/serverless.yml | 3 +- example/web/src/{index.js => app.js} | 0 example/web/src/scf_bootstrap | 2 +- serverless.component.yml | 2 +- src/config.js | 42 +++++++- src/package.json | 3 +- src/serverless.js | 155 ++++++++++++++------------- src/utils.js | 85 ++++++++++----- 11 files changed, 324 insertions(+), 144 deletions(-) rename example/web/src/{index.js => app.js} (100%) diff --git a/README.md b/README.md index 0298c92..ecd3fac 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ name: scfdemo # (必填) 创建的实例名称,请修改成您的实例名称 inputs: name: ${name}-${stage}-${app} #函数名称 src: ./ #代码路径 - handler: index.main_handler #入口 + type: 'web' # 部署Web函数时需指定该值 + handler: index.main_handler #入口(部署非Web函数时生效) runtime: Nodejs10.15 # 云函数运行时的环境 region: ap-guangzhou # 云函数所在区域 events: # 触发器 @@ -93,14 +94,16 @@ serverless.yml 文件包含的信息: inputs 下的参数为组件配置参数。一个最简单 SCF 组件参数配置由以下几部分: -| 参数名 | 说明 | -| ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| name | 云函数名称。由于云函数又是资源 ID,为了保证资源的唯一性,建议采用 `${name}-${stage}-${app}` 变量方式。 | -| src | 代码路径。 | -| handler | 函数处理方法名称 。 | -| runtime | 云函数运行环境,目前支持: Python2.7、Python3.6、Nodejs6.10、Nodejs8.9、Nodejs10.15、Nodejs12.16、PHP5、PHP7、Go1、Java8 和 CustomRuntime。 | -| region | 云函数所在的区域。 | -| events | 触发器。 支持的触发器为:timer、apigw、cos、cmq、ckafka 。 | +| 参数名 | 说明 | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| name | 云函数名称。由于云函数又是资源 ID,为了保证资源的唯一性,建议采用 `${name}-${stage}-${app}` 变量方式。 | +| src | 代码路径。 | +| type | 函数类型,默认为事件函数。支持的类型为:event(事件函数),web(Web 函数)。 | +| handler | 函数处理方法名称 。 | +| entryFile | 函数入口文件名。(函数类型为 web 且无 scf_bootstrap 文件时生效) | +| runtime | 云函数运行环境,目前支持: Python2.7、Python3.6、Nodejs6.10、Nodejs8.9、Nodejs10.15、Nodejs12.16、PHP5、PHP7、Go1、Java8 和 CustomRuntime。 | +| region | 云函数所在的区域。 | +| events | 触发器。 支持的触发器为:timer、apigw、cos、cmq、ckafka 。 | ### 账号权限 diff --git a/__tests__/index.test.js b/__tests__/index.test.js index d4464c0..7dd73f4 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -59,6 +59,41 @@ describe('Scf', () => { inputs } + const webFuncYaml = { + org: appId, + app: 'appDemo', + component: 'scf@dev', + name: `scf-web-integration-tests-${generateId()}`, + stage: 'dev', + inputs: { + name: `scf-web-integration-tests-${generateId()}`, + src: { + src: path.join(__dirname, '../example/web/src'), + exclude: ['.env'] + }, + type: 'web', + region: 'ap-chengdu', + runtime: 'Nodejs12.16', + events: [ + { + apigw: { + parameters: { + protocols: ['http', 'https'], + description: 'The service of Serverless Framework', + environment: 'test', + endpoints: [ + { + path: '/', + method: 'ANY' + } + ] + } + } + } + ] + } + } + const sdk = getServerlessSdk(instanceYaml.org, appId) let lastVersion = '$LATEST' @@ -370,4 +405,60 @@ describe('Scf', () => { expect(result.instance.instanceStatus).toEqual('inactive') }) + + it('deploy web function', async () => { + const instance = await sdk.deploy(webFuncYaml, credentials) + + expect(instance).toBeDefined() + expect(instance.instanceName).toEqual(webFuncYaml.name) + + const { outputs } = instance + // get src from template by default + expect(outputs.functionName).toEqual(webFuncYaml.inputs.name) + expect(outputs.runtime).toEqual(webFuncYaml.inputs.runtime) + expect(outputs.triggers).toBeDefined() + expect(outputs.triggers.length).toBe(1) + + const { triggers } = outputs + const apiTrigger = triggers[0] + + expect(apiTrigger).toEqual({ + NeedCreate: expect.any(Boolean), + created: expect.any(Boolean), + serviceId: expect.stringContaining('service-'), + serviceName: 'serverless', + subDomain: expect.stringContaining('.cd.apigw.tencentcs.com'), + protocols: 'http&https', + environment: 'test', + url: expect.stringContaining('http'), + apiList: [ + { + created: expect.any(Boolean), + path: '/', + method: 'ANY', + apiId: expect.stringContaining('api-'), + apiName: 'index', + authType: 'NONE', + businessType: 'NORMAL', + internalDomain: expect.any(String), + url: expect.stringContaining('http'), + isBase64Encoded: false + } + ], + urls: expect.any(Array) + }) + }) + + it('remove web function', async () => { + await sleep(5000) + await sdk.remove(webFuncYaml, credentials) + const result = await sdk.getInstance( + webFuncYaml.org, + webFuncYaml.stage, + webFuncYaml.app, + webFuncYaml.name + ) + + expect(result.instance.instanceStatus).toEqual('inactive') + }) }) diff --git a/docs/configure.md b/docs/configure.md index 0c4862d..487dcb9 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -34,7 +34,8 @@ inputs: # bucket: tinatest # bucket name # src: # 指定本地路径 type: event # 函数类型,默认为 event(事件类型),web(web类型) - handler: index.main_handler #入口 + handler: index.main_handler #入口(函数类型为事件类型时生效) + entryFile: app.js #入口文件名(代码中无scf_bootstrap文件,且函数类型为web类型时生效) runtime: Nodejs10.15 # 运行环境 默认 Nodejs10.15 region: ap-guangzhou # 函数所在区域 description: This is a function in ${app} application. @@ -187,36 +188,37 @@ inputs: 参考: https://cloud.tencent.com/document/product/583/18586 -| 参数名称 | 必选 | 类型 | 默认值 | 描述 | -| ----------------- | ---- | --------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| src | 是 | [Src](#Src) | | 函数代码路径。 | -| type | 否 | string | `event` | 函数类型,支持:event、web | -| name | 否 | string | | 创建的函数名称 | -| namespace | 否 | string | `default` | 函数命名空间 | -| handler | 否 | string | `index.main_handler` | 函数处理方法名称,执行方法表明了调用云函数时需要从哪个文件中的哪个函数开始执行 | -| role | 否 | string | | 云函数绑定的运行角色。 | -| runtime | 否 | string | `Nodejs10.15` | 函数运行环境 | -| region | 否 | string | `ap-guangzhou` | 云函数所在区域。详见产品支持的 [地域列表][函数地域列表]。 | -| description | 否 | string | | 函数描述,最大支持 1000 个英文字母、数字、空格、逗号、换行符和英文句号,支持中文 | -| memorySize | 否 | number | `128` | 函数运行时内存大小,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | -| timeout | 否 | number | `3` | 函数最长执行时间,单位为秒,可选值范围 1-900 秒 | -| initTimeout | 否 | number | `3` | 函数初始化超时时间,单位为秒,可选值范围 1-30 秒 秒 | -| eip | 否 | boolean | `false` | 是否[固定出口 IP][固定出口ip] | -| publicAccess | 否 | number | `true` | 是否开启公网访问 | -| environment | 否 | [Environment](#Environment) | | 函数的环境变量,配置参考[环境变量](#环境变量) | -| vpcConfig | 否 | [Vpc](#Vpc) | | 函数的私有网络配置,配置参数参考[私有网络](#私有网络) | -| layers | 否 | [Layer](#Layer)[] | | 云函数绑定的 layer, 配置参数参考[层配置](#层配置) | -| deadLetter | 否 | [DeadLetter](#DeadLetter) | | 死信队列配置,配置参数参考[死信队列](#死信队列) | -| cls | 否 | [Cls](#Cls) | | 函数日志配置,配置参数参考[函数日志](#函数日志) | -| eip | 否 | boolean | `false` | 固定出口 IP。默认为 false,即不启用。 | -| asyncRunEnable | 否 | boolean | `false` | 是否启用异步执行(长时间运行),默认最大支持 `12小时`,如果配置为 `true`,`cls`(函数日志配置) 必须。`此参数只有在函数创建时才有效` | -| traceEnable | 否 | boolean | `false` | 是否启用状态追踪,如果要配置为 `true`,必须配置 `asyncRunEnable` 同时为 `true` | -| installDependency | 否 | boolean | `false` | 是否自动在线安装依赖 | -| tags | 否 | | | 标签设置。可设置多对 key-value 的键值对 | -| cfs | 否 | [Cfs](#Cfs) | | 文件系统挂载配置,用于云函数挂载文件系统。配置参数参考[文件系统](#文件系统)。 | -| ignoreTriggers | 否 | boolean | `false` | 是否忽略触发器,如果设置为 `true`,`events` 参数将不起作用,组件将至更新函数配置和代码 | -| events | 否 | [Event](#Event)[] | | 触发器配置 | -| image | 否 | [Image](#Image) | | 镜像配置 | +| 参数名称 | 必选 | 类型 | 默认值 | 描述 | +| ----------------- | ---- | --------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| src | 是 | [Src](#Src) | | 函数代码路径。 | +| type | 否 | string | `event` | 函数类型,支持:event、web | +| name | 否 | string | | 创建的函数名称 | +| namespace | 否 | string | `default` | 函数命名空间 | +| handler | 否 | string | `index.main_handler` | 函数处理方法名称,执行方法表明了调用云函数时需要从哪个文件中的哪个函数开始执行(函数类型为 web 时生效) | +| entryFile | 否 | string | | 函数入口文件名,默认根据运行环境指定默认文件名。Nodejs 为 app.js,Python 环境为 app.py,php 环境为 hello.php(函数类型为 web 时生效) | +| role | 否 | string | | 云函数绑定的运行角色。 | +| runtime | 否 | string | `Nodejs10.15` | 函数运行环境 (web 函数目前仅支持 Nodejs、Python、Php 三类环境) | +| region | 否 | string | `ap-guangzhou` | 云函数所在区域。详见产品支持的 [地域列表][函数地域列表]。 | +| description | 否 | string | | 函数描述,最大支持 1000 个英文字母、数字、空格、逗号、换行符和英文句号,支持中文 | +| memorySize | 否 | number | `128` | 函数运行时内存大小,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | +| timeout | 否 | number | `3` | 函数最长执行时间,单位为秒,可选值范围 1-900 秒 | +| initTimeout | 否 | number | `3` | 函数初始化超时时间,单位为秒,可选值范围 1-30 秒 秒 | +| eip | 否 | boolean | `false` | 是否[固定出口 IP][固定出口ip] | +| publicAccess | 否 | number | `true` | 是否开启公网访问 | +| environment | 否 | [Environment](#Environment) | | 函数的环境变量,配置参考[环境变量](#环境变量) | +| vpcConfig | 否 | [Vpc](#Vpc) | | 函数的私有网络配置,配置参数参考[私有网络](#私有网络) | +| layers | 否 | [Layer](#Layer)[] | | 云函数绑定的 layer, 配置参数参考[层配置](#层配置) | +| deadLetter | 否 | [DeadLetter](#DeadLetter) | | 死信队列配置,配置参数参考[死信队列](#死信队列) | +| cls | 否 | [Cls](#Cls) | | 函数日志配置,配置参数参考[函数日志](#函数日志) | +| eip | 否 | boolean | `false` | 固定出口 IP。默认为 false,即不启用。 | +| asyncRunEnable | 否 | boolean | `false` | 是否启用异步执行(长时间运行),默认最大支持 `12小时`,如果配置为 `true`,`cls`(函数日志配置) 必须。`此参数只有在函数创建时才有效` | +| traceEnable | 否 | boolean | `false` | 是否启用状态追踪,如果要配置为 `true`,必须配置 `asyncRunEnable` 同时为 `true` | +| installDependency | 否 | boolean | `false` | 是否自动在线安装依赖 | +| tags | 否 | | | 标签设置。可设置多对 key-value 的键值对 | +| cfs | 否 | [Cfs](#Cfs) | | 文件系统挂载配置,用于云函数挂载文件系统。配置参数参考[文件系统](#文件系统)。 | +| ignoreTriggers | 否 | boolean | `false` | 是否忽略触发器,如果设置为 `true`,`events` 参数将不起作用,组件将至更新函数配置和代码 | +| events | 否 | [Event](#Event)[] | | 触发器配置 | +| image | 否 | [Image](#Image) | | 镜像配置 | **重要字段说明** diff --git a/example/web/serverless.yml b/example/web/serverless.yml index 6c4ac9a..6f3e4e7 100644 --- a/example/web/serverless.yml +++ b/example/web/serverless.yml @@ -10,8 +10,9 @@ inputs: type: web name: web-function region: ap-chengdu - handler: scf_bootstrap runtime: Nodejs12.16 + # 无 scf_bootstrap 文件时指定入口文件名 + entryFile: 'app.js' events: - apigw: parameters: diff --git a/example/web/src/index.js b/example/web/src/app.js similarity index 100% rename from example/web/src/index.js rename to example/web/src/app.js diff --git a/example/web/src/scf_bootstrap b/example/web/src/scf_bootstrap index 0bfe354..4cf5cfa 100755 --- a/example/web/src/scf_bootstrap +++ b/example/web/src/scf_bootstrap @@ -1,3 +1,3 @@ #!/usr/bin/env bash -/var/lang/node12/bin/node index.js +/var/lang/node12/bin/node app.js diff --git a/serverless.component.yml b/serverless.component.yml index 8328ff9..4fa5b10 100644 --- a/serverless.component.yml +++ b/serverless.component.yml @@ -1,5 +1,5 @@ name: scf -version: 0.7.5 +version: 0.7.6 author: Tencent Cloud, Inc. org: Tencent Cloud, Inc. description: Deploy a serverless cloud function on Tencent Cloud. diff --git a/src/config.js b/src/config.js index 345c1e2..a35c206 100644 --- a/src/config.js +++ b/src/config.js @@ -8,6 +8,45 @@ const getLang = (runtime) => { return 'Nodejs' } +const WEB_FUNC_CONFIGS = { + 'Python2.7': { + templateUrl: + 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/webfunc-python2-demo.zip', + entryFile: 'app.py', + bootstrapRunner: '/var/lang/python2/bin/python2' + }, + 'Python3.6': { + templateUrl: + 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/webfunc-python3-demo.zip', + entryFile: 'app.py', + bootstrapRunner: '/var/lang/python3/bin/python3' + }, + 'Nodejs10.15': { + templateUrl: + 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/webfunc-node10-demo.zip', + entryFile: 'app.js', + bootstrapRunner: '/var/lang/node10/bin/node' + }, + 'Nodejs12.16': { + templateUrl: + 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/webfunc-node12-demo.zip', + entryFile: 'app.js', + bootstrapRunner: '/var/lang/node12/bin/node' + }, + Php5: { + templateUrl: + 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/webfunc-php5-demo.zip', + entryFile: 'hello.php', + bootstrapRunner: '/var/lang/php5/bin/php -S 0.0.0.0:9000' + }, + Php7: { + templateUrl: + 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/webfunc-php7-demo.zip', + entryFile: 'hello.php', + bootstrapRunner: '/var/lang/php7/bin/php -S 0.0.0.0:9000' + } +} + const CONFIGS = { templateUrl: 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/scf-demo.zip', region: 'ap-guangzhou', @@ -50,7 +89,8 @@ const CONFIGS = { } ] } - } + }, + defaultwebFunc: WEB_FUNC_CONFIGS } module.exports = CONFIGS diff --git a/src/package.json b/src/package.json index 6b3bb1b..d45a1a6 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,8 @@ { "dependencies": { + "adm-zip": "^0.5.5", "download": "^8.0.0", - "tencent-component-toolkit": "^2.12.7", + "tencent-component-toolkit": "^2.12.10", "type": "^2.0.0" } } diff --git a/src/serverless.js b/src/serverless.js index 99175e9..5975603 100644 --- a/src/serverless.js +++ b/src/serverless.js @@ -37,96 +37,103 @@ class ServerlessComponent extends Component { const appId = this.getAppId() const region = inputs.region || CONFIGS.region - const faasType = inputs.type || 'event' + const { scfInputs, useDefault } = await formatInputs(this, credentials, appId, inputs) - const scf = new Scf(credentials, region) - const scfOutput = await scf.deploy(scfInputs) - - const outputs = { - type: faasType, - functionName: scfOutput.FunctionName, - code: scfInputs.code, - description: scfOutput.Description, - region: scfOutput.Region, - namespace: scfOutput.Namespace, - runtime: scfOutput.Runtime, - handler: scfOutput.Handler, - memorySize: scfOutput.MemorySize - } + try { + const scf = new Scf(credentials, region) + const scfOutput = await scf.deploy(scfInputs) + + const outputs = { + type: faasType, + functionName: scfOutput.FunctionName, + code: scfInputs.code, + description: scfOutput.Description, + region: scfOutput.Region, + namespace: scfOutput.Namespace, + runtime: scfOutput.Runtime, + handler: scfOutput.Handler, + memorySize: scfOutput.MemorySize, + entryFile: scfInputs.entryFile + } - if (scfOutput.Layers && scfOutput.Layers.length > 0) { - outputs.layers = scfOutput.Layers.map((item) => ({ - name: item.LayerName, - version: item.LayerVersion - })) - } + if (scfOutput.Layers && scfOutput.Layers.length > 0) { + outputs.layers = scfOutput.Layers.map((item) => ({ + name: item.LayerName, + version: item.LayerVersion + })) + } - // default version is $LATEST - outputs.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.traffic = - scfOutput.Traffic !== undefined - ? scfOutput.Traffic - : this.state.traffic !== undefined - ? this.state.traffic - : 1 - - if (outputs.traffic !== 1 && scfOutput.ConfigTrafficVersion) { - outputs.configTrafficVersion = scfOutput.ConfigTrafficVersion - this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion - } + // default version is $LATEST + outputs.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.traffic = + scfOutput.Traffic !== undefined + ? scfOutput.Traffic + : this.state.traffic !== undefined + ? this.state.traffic + : 1 + + if (outputs.traffic !== 1 && scfOutput.ConfigTrafficVersion) { + outputs.configTrafficVersion = scfOutput.ConfigTrafficVersion + this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion + } - this.state.lastVersion = outputs.lastVersion - this.state.traffic = outputs.traffic - - const stateApigw = {} - outputs.triggers = scfOutput.Triggers.map((item) => { - if (item.serviceId) { - stateApigw[item.serviceName] = item - stateApigw[item.serviceId] = item - item.urls = [] - item.apiList.forEach((apiItem) => { - if (getType(item.subDomain) === 'Array') { - item.subDomain.forEach((domain) => { + this.state.lastVersion = outputs.lastVersion + this.state.traffic = outputs.traffic + this.state.entryFile = outputs.entryFile + + const stateApigw = {} + outputs.triggers = scfOutput.Triggers.map((item) => { + if (item.serviceId) { + stateApigw[item.serviceName] = item + stateApigw[item.serviceId] = item + item.urls = [] + item.apiList.forEach((apiItem) => { + if (getType(item.subDomain) === 'Array') { + item.subDomain.forEach((domain) => { + item.urls.push( + `${getDefaultProtocol(item.protocols)}://${domain}/${item.environment}${ + apiItem.path + }` + ) + }) + } else { item.urls.push( - `${getDefaultProtocol(item.protocols)}://${domain}/${item.environment}${ + `${getDefaultProtocol(item.protocols)}://${item.subDomain}/${item.environment}${ apiItem.path }` ) - }) - } else { - item.urls.push( - `${getDefaultProtocol(item.protocols)}://${item.subDomain}/${item.environment}${ - apiItem.path - }` - ) - } - }) - } - return item - }) - this.state.apigw = stateApigw + } + }) + } + return item + }) + this.state.apigw = stateApigw - if (useDefault) { - outputs.templateUrl = CONFIGS.templateUrl - } + if (useDefault) { + outputs.templateUrl = CONFIGS.templateUrl + } - this.state.region = region - this.state.function = scfOutput + this.state.region = region + this.state.function = scfOutput - // must add this property for debuging online - this.state.lambdaArn = scfOutput.FunctionName + // must add this property for debuging online + this.state.lambdaArn = scfOutput.FunctionName - await this.save() + await this.save() - console.log(`Deploy ${CONFIGS.compFullname} success`) + console.log(`Deploy ${CONFIGS.compFullname} success`) - return outputs + return outputs + } catch (e) { + console.log(`Deploy ${CONFIGS.compFullname} failed`) + return { requestId: e.reqId, message: e.message } + } } // eslint-disable-next-line diff --git a/src/utils.js b/src/utils.js index 915faf6..7c7fa38 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,6 +2,7 @@ const download = require('download') const { Cos } = require('tencent-component-toolkit') const { ApiTypeError } = require('tencent-component-toolkit/lib/utils/error') const CONFIGS = require('./config') +const AdmZip = require('adm-zip') /** * Generate random id @@ -62,20 +63,23 @@ const getDefaultServiceDescription = (instance) => { /** * get default template zip file path */ -const getDefaultZipPath = async () => { +const getDefaultZipPath = async (inputs, isWebFunc) => { // unzip source zip file // add default template const downloadPath = `/tmp/${generateId()}` const filename = 'template' - console.log(`Installing Default ${CONFIGS.compFullname} code...`) + console.log( + `Installing Default ${isWebFunc ? `Web SCF ${inputs.runtime}` : CONFIGS.compFullname} code...` + ) try { - await download(CONFIGS.templateUrl, downloadPath, { - filename: `${filename}.zip` - }) + const templateUrl = isWebFunc + ? CONFIGS.defaultwebFunc[inputs.runtime].templateUrl + : CONFIGS.templateUrl + await download(templateUrl, downloadPath, { filename: `${filename}.zip` }) } catch (e) { throw new ApiTypeError( - `DOWNLOAD_${CONFIGS.compName.toUpperCase()}_TEMPLATE`, + `DOWNLOAD_${isWebFunc ? `WEB_SCF_${inputs.runtime}` : CONFIGS.compFullname}_TEMPLATE`, 'Download default template failed.' ) } @@ -84,6 +88,26 @@ const getDefaultZipPath = async () => { return zipPath } +/** + * check & inject scf_bootstrap file + */ +const handleWebFuncEntryFile = async (inputs) => { + const zip = new AdmZip(inputs.src) + const entries = zip.getEntries() + // check scf_bootstrap exist + const [entry] = entries.filter((e) => e.name === 'scf_bootstrap') + if (!entry) { + // inject scf_bootstrap file into code + const BASH_PREFIX = '#!/usr/bin/env bash \n' + const runner = `${CONFIGS.defaultwebFunc[inputs.runtime].bootstrapRunner}` + const entryFileName = inputs.entryFile || CONFIGS.defaultwebFunc[inputs.runtime].entryFile + const fileContent = `${BASH_PREFIX}${runner} ${entryFileName}` + zip.addFile('scf_bootstrap', Buffer.from(fileContent, 'utf8'), '', 777) + zip.writeZip() + console.log('Inject default scf_bootstrap file into code') + } +} + async function uploadCodeToCos(instance, credentials, appId, inputs) { const region = inputs.region || CONFIGS.region @@ -93,9 +117,7 @@ async function uploadCodeToCos(instance, credentials, appId, inputs) { typeof inputs.srcOriginal === 'object' ? inputs.srcOriginal : typeof inputs.srcOriginal === 'string' - ? { - src: inputs.srcOriginal - } + ? { src: inputs.srcOriginal } : {} const bucketName = removeAppid(tempSrc.bucket, appId) || `sls-cloudfunction-${region}-code` @@ -103,10 +125,7 @@ async function uploadCodeToCos(instance, credentials, appId, inputs) { tempSrc.object || `/${CONFIGS.compName}_component_${generateId()}-${Math.floor(Date.now() / 1000)}.zip` - const code = { - bucket: bucketName, - object: objectName - } + const code = { bucket: bucketName, object: objectName } const cos = new Cos(credentials, region) const bucket = `${code.bucket}-${appId}` @@ -129,22 +148,26 @@ async function uploadCodeToCos(instance, credentials, appId, inputs) { let useDefault if (!tempSrc.object) { + const isWebFunc = inputs.type === 'web' // whether use default template, if so, download it // get default template code let zipPath if (!tempSrc.src) { useDefault = true - zipPath = await getDefaultZipPath() + zipPath = await getDefaultZipPath(inputs, isWebFunc) inputs.src = zipPath } else { + if (isWebFunc) { + handleWebFuncEntryFile(inputs) + } zipPath = inputs.src } console.log(`Uploading code ${code.object} to bucket ${bucket}`) - await cos.upload({ - bucket: `${bucketName}-${appId}`, - file: zipPath, - key: objectName - }) + try { + await cos.upload({ bucket: `${bucketName}-${appId}`, file: zipPath, key: objectName }) + } catch (error) { + throw new ApiTypeError('UPLOAD_CODE', 'Upload code to user cos failed.') + } } return { @@ -189,12 +212,11 @@ const formatInputs = async (instance, credentials, appId, inputs) => { if (imageCode) { inputs.imageConfig = imageCode } else { + inputs.runtime = inputs.runtime || CONFIGS.runtime + inputs.handler = inputs.handler || CONFIGS.handler(inputs.runtime) const uploadResult = await uploadCodeToCos(instance, credentials, appId, inputs) inputs.code = uploadResult.code ;({ useDefault } = uploadResult) - - inputs.runtime = inputs.runtime || CONFIGS.runtime - inputs.handler = inputs.handler || CONFIGS.handler(inputs.runtime) } const oldState = instance.state @@ -253,6 +275,18 @@ const formatInputs = async (instance, credentials, appId, inputs) => { currentEvent.parameters.created = curState.created } currentEvent.parameters.serviceId = serviceId + // 如果为 web 类型函数,添加 api 指定函数类型为 web + if (inputs.type === 'web') { + const { endpoints = [] } = currentEvent.parameters + currentEvent.parameters.endpoints = endpoints.map((item) => { + if (!item.function) { + item.function = {} + } + item.function.type = 'web' + return item + }) + } + apigwName.push(serviceName) } existApigwTrigger = true @@ -277,9 +311,10 @@ const formatInputs = async (instance, credentials, appId, inputs) => { defaultApigw.parameters.created = stateApigw[serviceName].created } if (inputs.type === 'web') { - defaultApigw.parameters.function = { - type: 'web' - } + defaultApigw.parameters.endpoints = defaultApigw.parameters.endpoints.map((ep) => ({ + ...ep, + function: { type: 'web' } + })) } inputs.events.push({ apigw: defaultApigw