diff --git a/.npmignore b/.npmignore index af73284..fb526c7 100644 --- a/.npmignore +++ b/.npmignore @@ -13,3 +13,7 @@ prettier.config.js release.config.js commitlint.config.js .editorconfig +src +*.ts +tsconfig.json +babel.config.js \ No newline at end of file diff --git a/package.json b/package.json index de58b67..3583236 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@semantic-release/npm": "^7.0.4", "@semantic-release/release-notes-generator": "^9.0.1", "@types/axios": "^0.14.0", + "@types/lodash": "^4.17.17", "@types/react-grid-layout": "^1.1.2", "@types/uuid": "^8.3.1", "@typescript-eslint/eslint-plugin": "^4.14.0", @@ -94,6 +95,7 @@ "camelcase": "^6.2.0", "cos-nodejs-sdk-v5": "^2.9.20", "dayjs": "^1.10.4", + "lodash": "^4.17.21", "moment": "^2.29.1", "tencent-cloud-sdk": "^1.0.5", "type-fest": "^0.20.2", diff --git a/src/modules/scf/apis.ts b/src/modules/scf/apis.ts index f0cc8e1..f0bb4c7 100644 --- a/src/modules/scf/apis.ts +++ b/src/modules/scf/apis.ts @@ -10,6 +10,7 @@ const ACTIONS = [ 'GetFunctionEventInvokeConfig', 'UpdateFunctionEventInvokeConfig', 'CreateTrigger', + 'UpdateTrigger', 'DeleteTrigger', 'PublishVersion', 'ListVersionByFunction', diff --git a/src/modules/triggers/base.ts b/src/modules/triggers/base.ts index c7182a2..150a06c 100644 --- a/src/modules/triggers/base.ts +++ b/src/modules/triggers/base.ts @@ -116,4 +116,4 @@ export const TRIGGER_STATUS_MAP = { 0: 'CLOSE', }; -export const CAN_UPDATE_TRIGGER = ['apigw', 'cls', 'mps', 'clb']; +export const CAN_UPDATE_TRIGGER = ['apigw', 'cls', 'mps', 'clb','http','ckafka']; \ No newline at end of file diff --git a/src/modules/triggers/ckafka.ts b/src/modules/triggers/ckafka.ts index 652eb89..d25f849 100644 --- a/src/modules/triggers/ckafka.ts +++ b/src/modules/triggers/ckafka.ts @@ -1,8 +1,9 @@ import { CapiCredentials, RegionType } from './../interface'; -import { TriggerInputs, CkafkaTriggerInputsParams, CreateTriggerReq } from './interface'; +import { TriggerInputs, CkafkaTriggerInputsParams, CreateTriggerReq,TriggerAction } from './interface'; import Scf from '../scf'; -import { TRIGGER_STATUS_MAP } from './base'; +import { TRIGGER_STATUS_MAP } from './base'; import { TriggerManager } from './manager'; +import { getScfTriggerByName } from './utils'; export default class CkafkaTrigger { credentials: CapiCredentials; @@ -29,20 +30,22 @@ export default class CkafkaTrigger { return `${triggerInputs.Type}-${triggerInputs.TriggerName}-${desc}-${Enable}-${triggerInputs.Qualifier}`; } - formatInputs({ inputs }: { inputs: TriggerInputs }) { + formatInputs({ inputs,action = 'CreateTrigger'}: { inputs: TriggerInputs,action?: TriggerAction }) { const { parameters } = inputs; + const triggerName = parameters?.name || `${parameters?.instanceId}-${parameters?.topic}`; const triggerInputs: CreateTriggerReq = { - Action: 'CreateTrigger', + Action: action, FunctionName: inputs.functionName, Namespace: inputs.namespace, Type: 'ckafka', Qualifier: parameters?.qualifier ?? '$DEFAULT', - TriggerName: `${parameters?.name}-${parameters?.topic}`, + TriggerName: triggerName, TriggerDesc: JSON.stringify({ maxMsgNum: parameters?.maxMsgNum ?? 100, offset: parameters?.offset ?? 'latest', retry: parameters?.retry ?? 10000, timeOut: parameters?.timeout ?? 60, + consumerGroupName: parameters?.consumerGroupName ?? '', }), Enable: parameters?.enable ? 'OPEN' : 'CLOSE', }; @@ -57,16 +60,35 @@ export default class CkafkaTrigger { async create({ scf, inputs, + region }: { scf: Scf | TriggerManager; region: RegionType; inputs: TriggerInputs; }) { - const { triggerInputs } = this.formatInputs({ inputs }); - console.log(`Creating ${triggerInputs.Type} trigger ${triggerInputs.TriggerName}`); - const { TriggerInfo } = await scf.request(triggerInputs as any); - TriggerInfo.Qualifier = TriggerInfo.Qualifier || triggerInputs.Qualifier; - return TriggerInfo; + // 查询当前触发器是否已存在 + const existTrigger = await getScfTriggerByName({ scf, region, inputs }); + // 更新触发器 + if (existTrigger) { + const { triggerInputs } = this.formatInputs({ inputs, action: 'UpdateTrigger' }); + console.log(`${triggerInputs.Type} trigger ${triggerInputs.TriggerName} is exist`) + console.log(`Updating ${triggerInputs.Type} trigger ${triggerInputs.TriggerName}`); + try { + // 更新触发器 + await scf.request(triggerInputs as any); + // 更新成功后,查询最新的触发器信息 + const trigger = await getScfTriggerByName({ scf, region, inputs }); + return trigger; + } catch (error) { + return {} + } + } else { // 创建触发器 + const { triggerInputs } = this.formatInputs({ inputs }); + console.log(`Creating ${triggerInputs.Type} trigger ${triggerInputs.TriggerName}`); + const { TriggerInfo } = await scf.request(triggerInputs as any); + TriggerInfo.Qualifier = TriggerInfo.Qualifier || triggerInputs.Qualifier; + return TriggerInfo; + } } async delete({ scf, inputs }: { scf: Scf; inputs: TriggerInputs }) { console.log(`Removing ${inputs.type} trigger ${inputs.triggerName}`); diff --git a/src/modules/triggers/http.ts b/src/modules/triggers/http.ts index dc20dfb..f5e26f0 100644 --- a/src/modules/triggers/http.ts +++ b/src/modules/triggers/http.ts @@ -2,7 +2,9 @@ import Scf from '../scf'; import { TriggerManager } from './manager'; import { CapiCredentials, RegionType } from './../interface'; import BaseTrigger from './base'; -import { HttpTriggerInputsParams, TriggerInputs, CreateTriggerReq } from './interface'; +import { HttpTriggerInputsParams, TriggerInputs, CreateTriggerReq,TriggerAction } from './interface'; +import { caseForObject } from '../../utils'; +import { getScfTriggerByName } from './utils'; export default class HttpTrigger extends BaseTrigger { credentials: CapiCredentials; @@ -15,31 +17,33 @@ export default class HttpTrigger extends BaseTrigger { } getKey(triggerInputs: CreateTriggerReq) { - const triggerDesc = JSON.parse(triggerInputs.TriggerDesc!); - const tempDest = JSON.stringify({ - authType: triggerDesc?.AuthType, - enableIntranet: triggerDesc?.NetConfig?.EnableIntranet, - enableExtranet: triggerDesc?.NetConfig?.EnableExtranet, - }); - return `http-${tempDest}-${triggerInputs.Qualifier}`; + return `http-${triggerInputs?.TriggerName}`; } - formatInputs({ inputs }: { region: RegionType; inputs: TriggerInputs }) { + formatInputs({ inputs,action = 'CreateTrigger' }: { region: RegionType; inputs: TriggerInputs ,action?: TriggerAction}) { const { parameters } = inputs; + const triggerName = parameters?.name || 'url-trigger'; + const { origins,headers,methods,exposeHeaders } = parameters?.corsConfig || {} const triggerInputs: CreateTriggerReq = { - Action: 'CreateTrigger', + Action: action, FunctionName: inputs.functionName, Namespace: inputs.namespace, - Type: 'http', Qualifier: parameters?.qualifier || '$DEFAULT', - TriggerName: parameters?.name || 'url-trigger', + TriggerName: triggerName, TriggerDesc: JSON.stringify({ AuthType: parameters?.authType || 'NONE', NetConfig: { EnableIntranet: parameters?.netConfig?.enableIntranet ?? false, EnableExtranet: parameters?.netConfig?.enableExtranet ?? false, }, + CorsConfig: parameters?.corsConfig ? caseForObject({ + ...parameters?.corsConfig, + origins: typeof origins === 'string' ? origins?.split(',') : origins, + methods: typeof methods === 'string' ? methods?.split(',') : methods, + headers: typeof headers === 'string' ? headers?.split(',') : headers, + exposeHeaders: typeof exposeHeaders === 'string' ? exposeHeaders?.split(',') : exposeHeaders, + },'upper') : undefined }), Enable: 'OPEN', }; @@ -61,12 +65,29 @@ export default class HttpTrigger extends BaseTrigger { region: RegionType; inputs: TriggerInputs; }) { - const { triggerInputs } = this.formatInputs({ region, inputs }); - console.log(`Creating ${triggerInputs.Type} trigger ${triggerInputs.TriggerName}`); - const { TriggerInfo } = await scf.request(triggerInputs); - TriggerInfo.Qualifier = TriggerInfo.Qualifier || triggerInputs.Qualifier; - - return TriggerInfo; + // 查询当前触发器是否已存在 + const existTrigger = await getScfTriggerByName({ scf, region, inputs }); + // 更新触发器 + if (existTrigger) { + const { triggerInputs } = this.formatInputs({ region, inputs, action: 'UpdateTrigger' }); + console.log(`${triggerInputs.Type} trigger ${triggerInputs.TriggerName} is exist`) + console.log(`Updating ${triggerInputs.Type} trigger ${triggerInputs.TriggerName}`); + try { + // 更新触发器 + await scf.request(triggerInputs); + // 更新成功后,查询最新的触发器信息 + const trigger = await getScfTriggerByName({ scf, region, inputs }); + return trigger; + } catch (error) { + return {} + } + } else { // 创建触发器 + const { triggerInputs } = this.formatInputs({ region, inputs }); + console.log(`Creating ${triggerInputs.Type} trigger ${triggerInputs.TriggerName}`); + const { TriggerInfo } = await scf.request(triggerInputs); + TriggerInfo.Qualifier = TriggerInfo.Qualifier || triggerInputs.Qualifier; + return TriggerInfo; + } } async delete({ diff --git a/src/modules/triggers/interface/index.ts b/src/modules/triggers/interface/index.ts index 3717e08..20ce7e0 100644 --- a/src/modules/triggers/interface/index.ts +++ b/src/modules/triggers/interface/index.ts @@ -1,6 +1,7 @@ import { ApigwDeployInputs, ApiEndpoint } from '../../apigw/interface'; import { TagInput } from '../../interface'; +export type TriggerAction = 'CreateTrigger' | 'UpdateTrigger' export interface ApigwTriggerRemoveScfTriggerInputs { serviceId: string; apiId: string; @@ -42,7 +43,7 @@ export interface ApigwTriggerInputsParams extends ApigwDeployInputs { export type TriggerType = 'scf' | 'timer' | string; export interface CreateTriggerReq { - Action?: 'CreateTrigger'; + Action?: TriggerAction; ResourceId?: string; FunctionName?: string; Namespace?: string; @@ -57,11 +58,13 @@ export interface CreateTriggerReq { export interface CkafkaTriggerInputsParams extends TriggerInputsParams { qualifier?: string; name?: string; - topic?: string; + instanceId?: string; //ckafka实例ID + topic?: string; //ckafka主题名称 maxMsgNum?: number; offset?: number; retry?: number; timeout?: number; + consumerGroupName?: string; enable?: boolean; } @@ -100,6 +103,15 @@ export interface HttpTriggerInputsParams { enableIntranet?: boolean; enableExtranet?: boolean; }; + corsConfig: { + enable: boolean + origins: Array | string + methods: Array | string + headers: Array | string + exposeHeaders: Array | string + credentials: boolean + maxAge: number + } } export interface MpsTriggerInputsParams { @@ -120,6 +132,7 @@ export interface TimerTriggerInputsParams { export interface TriggerInputs

{ functionName: string; + Type?: string; // 兼容scf组件触发器类型字段 type?: string; triggerDesc?: string; triggerName?: string; diff --git a/src/modules/triggers/manager.ts b/src/modules/triggers/manager.ts index 18f6d17..e62f77d 100644 --- a/src/modules/triggers/manager.ts +++ b/src/modules/triggers/manager.ts @@ -277,11 +277,17 @@ export class TriggerManager { // 1. 删除老的无法更新的触发器 for (let i = 0, len = deleteList.length; i < len; i++) { const trigger = deleteList[i]; - await this.removeTrigger({ - name, - namespace, - trigger, - }); + // 若类型触发器不支持编辑,需要先删除,后重新创建; + if (!CAN_UPDATE_TRIGGER.includes(trigger?.Type)) { + await this.removeTrigger({ + name, + namespace, + trigger, + }); + } else { + // 若触发器类型支持编辑,直接跳过删除 + continue; + } } // 2. 创建新的触发器 diff --git a/src/modules/triggers/utils/index.ts b/src/modules/triggers/utils/index.ts new file mode 100644 index 0000000..5b3e1e8 --- /dev/null +++ b/src/modules/triggers/utils/index.ts @@ -0,0 +1,41 @@ +import { RegionType } from "../../interface"; +import Scf from "../../scf"; +import { CkafkaTriggerInputsParams, HttpTriggerInputsParams, TriggerDetail, TriggerInputs } from "../interface"; +import { TriggerManager } from "../manager"; + +// 获取函数下指定类型以及指定触发器名称的触发器 +export async function getScfTriggerByName({ + scf, + inputs + }: { + scf: Scf | TriggerManager; + region: RegionType; + inputs: TriggerInputs; + }): Promise { + const filters = [ + { + Name: 'Type', + Values: [inputs?.type || inputs?.Type] + } + ] + if (inputs?.parameters?.name) { + filters.push({ + Name: 'TriggerName', + Values: [inputs?.parameters?.name] + }) + } + if (inputs?.parameters?.qualifier) { + filters.push({ + Name: 'Qualifier', + Values: [inputs?.parameters?.qualifier?.toString()] + }) + } + const response = await scf.request({ + Action: 'ListTriggers', + FunctionName: inputs?.functionName, + Namespace: inputs?.namespace, + Limit: 1000, + Filters: filters + }); + return response?.Triggers?.[0]; +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 13d5d70..8446dc1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,6 +4,8 @@ import camelCase from 'camelcase'; import { PascalCase } from 'type-fest'; import { CamelCasedProps, PascalCasedProps } from '../modules/interface'; import crypto from 'crypto'; +import _ from 'lodash'; + // TODO: 将一些库换成 lodash @@ -314,3 +316,24 @@ export const getYunTiApiUrl = (): string => { const url = `${apiUrl}?api_key=${apiKey}&api_ts=${timeStamp}&api_sign=${apiSign}`; return url; }; + + + +/** + * 首字母转换大小写 + * @param {*} obj + * @param {*} type + * @returns + */ +export function caseForObject(obj: object,type : 'upper' | 'lower') { + if (!_.isPlainObject(obj)) return obj; + return _.transform(obj, (result: { [key: string]: any }, value, key) => { + let newKey:string = ''; + if (type === 'upper') { + newKey = _.upperFirst(key) + } else { + newKey = _.lowerFirst(key); + } + result[newKey] = _.isPlainObject(value) ? caseForObject(value,type) : value; + }, {}); +} \ No newline at end of file