From 20c697e24a2aa20748082adbe93c74d7f7beeff7 Mon Sep 17 00:00:00 2001 From: brycewwang <865689594@qq.com> Date: Tue, 23 Apr 2024 14:32:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=83=A8=E7=BD=B2=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=B7=BB=E5=8A=A0=E4=BA=91=E6=A2=AF=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=A0=87=E7=AD=BE(=E8=BF=90=E8=90=A5=E9=83=A8?= =?UTF-8?q?=E9=97=A8,=E8=BF=90=E8=90=A5=E4=BA=A7=E5=93=81,=E8=B4=9F?= =?UTF-8?q?=E8=B4=A3=E4=BA=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 25 ++++--- package.json | 5 +- src/modules/cam/apis.ts | 1 + src/modules/cam/index.ts | 52 ++++++++++++++ src/modules/scf/constants.ts | 1 + src/modules/scf/index.ts | 19 ++++- src/modules/scf/interface.ts | 2 + src/modules/scf/utils.ts | 6 +- src/modules/triggers/apigw.ts | 3 +- src/modules/triggers/interface/index.ts | 4 ++ src/utils/index.ts | 92 +++++++++++++++++++++++++ 11 files changed, 192 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index b905b769..356cc673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3887,6 +3887,15 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/axios": { + "version": "0.14.0", + "resolved": "https://mirrors.tencent.com/npm/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "dev": true, + "requires": { + "axios": "*" + } + }, "@types/babel__core": { "version": "7.1.15", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.15.tgz", @@ -4673,12 +4682,11 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, + "version": "0.21.0", + "resolved": "https://mirrors.tencent.com/npm/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.10.0" } }, "babel-jest": { @@ -7374,10 +7382,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", - "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==", - "dev": true + "version": "1.15.6", + "resolved": "https://mirrors.tencent.com/npm/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-in": { "version": "1.0.2", diff --git a/package.json b/package.json index 2faee77f..96ef2b6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tencent-component-toolkit", - "version": "2.24.2", + "version": "2.24.3", "description": "Tencent component toolkit", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -66,12 +66,12 @@ "@semantic-release/git": "^9.0.0", "@semantic-release/npm": "^7.0.4", "@semantic-release/release-notes-generator": "^9.0.1", + "@types/axios": "^0.14.0", "@types/react-grid-layout": "^1.1.2", "@types/uuid": "^8.3.1", "@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/parser": "^4.14.0", "@ygkit/secure": "^0.0.3", - "axios": "^0.21.0", "dotenv": "^8.2.0", "eslint": "^7.18.0", "eslint-config-prettier": "^6.10.0", @@ -90,6 +90,7 @@ "@types/jest": "^26.0.20", "@types/node": "^14.14.31", "@ygkit/request": "^0.1.8", + "axios": "^0.21.0", "camelcase": "^6.2.0", "cos-nodejs-sdk-v5": "^2.9.20", "dayjs": "^1.10.4", diff --git a/src/modules/cam/apis.ts b/src/modules/cam/apis.ts index 99543d63..e3e5ee24 100644 --- a/src/modules/cam/apis.ts +++ b/src/modules/cam/apis.ts @@ -8,6 +8,7 @@ const ACTIONS = [ 'CreateRole', 'GetRole', 'DeleteRole', + 'GetUserAppId', ] as const; export type ActionType = typeof ACTIONS[number]; diff --git a/src/modules/cam/index.ts b/src/modules/cam/index.ts index f98e5d08..db2b87e5 100644 --- a/src/modules/cam/index.ts +++ b/src/modules/cam/index.ts @@ -2,6 +2,8 @@ import { ActionType } from './apis'; import { CapiCredentials, RegionType, ApiServiceType } from './../interface'; import { Capi } from '@tencent-sdk/capi'; import APIS from './apis'; +import { getYunTiApiUrl } from '../../utils'; +import axios from 'axios'; /** CAM (访问管理)for serverless */ export default class Cam { @@ -112,4 +114,54 @@ export default class Cam { async CheckSCFExcuteRole() { return this.isRoleExist('QCS_SCFExcuteRole'); } + + /** 查询用户AppId */ + async GetUserAppId(): Promise<{ OwnerUin: string; AppId: string; Uin: string }> { + try { + return this.request({ + Action: 'GetUserAppId', + }); + } catch (error) { + return { + OwnerUin: '', + AppId: '', + Uin: '', + }; + } + } + + /** + * checkYunTi 检查是否是云梯账号 + * @returns {boolean} true: 是云梯账号; false: 非云梯账号 + */ + async checkYunTi(): Promise { + let isYunTi = false; + const { OwnerUin: uin } = await this.GetUserAppId(); + try { + const params = JSON.stringify({ + id: '1', + jsonrpc: '2.0', + method: 'checkOwnUin', + params: { ownUin: [uin] }, + }); + const apiUrl = getYunTiApiUrl(); + const res = await axios.post(apiUrl, params, { + headers: { 'content-type': 'application/json' }, + }); + if (res?.data?.error?.message) { + throw new Error(res.data.error.message); + } else { + isYunTi = + res?.data?.result?.data && + res.data.result.data?.some( + (item: { ownUin: string; appId: string }) => item?.ownUin === uin, + ); + console.log('check yunTi ownUin:', isYunTi); + } + } catch (error) { + isYunTi = false; + console.log('checkYunTiOwnUin error:', error); + } + return isYunTi; + } } diff --git a/src/modules/scf/constants.ts b/src/modules/scf/constants.ts index bc6249fa..4dd235d2 100644 --- a/src/modules/scf/constants.ts +++ b/src/modules/scf/constants.ts @@ -1 +1,2 @@ export const WebServerImageDefaultPort = 9000; +export const YunTiTagDocHref = 'https://doc.weixin.qq.com/doc/w3_AQ8AWgYkAOEEeD1cr34R7S66r8ONY?scode=AJEAIQdfAAoiSWrYcZAOMAswb5AFM'; diff --git a/src/modules/scf/index.ts b/src/modules/scf/index.ts index e8ebd768..7119c0b6 100644 --- a/src/modules/scf/index.ts +++ b/src/modules/scf/index.ts @@ -2,8 +2,8 @@ import { ApigwRemoveInputs } from './../apigw/interface'; import { ActionType } from './apis'; import { RegionType, ApiServiceType, CapiCredentials } from './../interface'; import { Capi } from '@tencent-sdk/capi'; -import { ApiTypeError } from '../../utils/error'; -import { deepClone, strip } from '../../utils'; +import { ApiError, ApiTypeError } from '../../utils/error'; +import { deepClone, formatInputTags, isAddedYunTiTags, strip } from '../../utils'; import TagsUtils from '../tag/index'; import ApigwUtils from '../apigw'; import CONFIGS from './config'; @@ -25,6 +25,8 @@ import ScfEntity from './entities/scf'; import AliasEntity from './entities/alias'; import VersionEntity from './entities/version'; import { ConcurrencyEntity } from './entities/concurrency'; +import { default as Cam } from '../cam'; +import { YunTiTagDocHref } from './constants'; /** 云函数组件 */ export default class Scf { @@ -252,6 +254,7 @@ export default class Scf { credentials: this.credentials, region: this.region, }); + const tags: any = trigger?.parameters?.tags ?? trigger?.tags ?? funcInfo.Tags ?? []; const triggerOutput = await triggerInstance.create({ scf: this, region: this.region, @@ -259,6 +262,7 @@ export default class Scf { namespace: funcInfo.Namespace, functionName: funcInfo.FunctionName, ...trigger, + tags: formatInputTags(tags), }, }); @@ -278,6 +282,17 @@ export default class Scf { const functionName = inputs.name; const { ignoreTriggers = false } = inputs; + // 自研账号,需要检查函数标签是否配置云梯标签(运营部门、运营产品、负责人); 非自研账号,不需要检查 + const isYunTi = await new Cam(this.credentials, this.region).checkYunTi(); + if (isYunTi) { + if (!isAddedYunTiTags(inputs)) { + throw new ApiError({ + type: 'API_SCF_DeployFunction', + message: `部署失败:自研用户请按照运营部门、运营产品、负责人正确配置函数标签,标签配置指南请参考:${YunTiTagDocHref}`, + }); + } + } + // 在部署前,检查函数初始状态,如果初始为 CreateFailed,尝试先删除,再重新创建 let funcInfo = await this.scf.getInitialStatus({ namespace, functionName }); diff --git a/src/modules/scf/interface.ts b/src/modules/scf/interface.ts index f5da63ae..f714a7b7 100644 --- a/src/modules/scf/interface.ts +++ b/src/modules/scf/interface.ts @@ -78,6 +78,8 @@ export interface TriggerType { TriggerName?: string; Qualifier?: string; compared?: boolean; + tags?: object; + parameters?: any; } export type OriginTriggerType = { diff --git a/src/modules/scf/utils.ts b/src/modules/scf/utils.ts index 684f95d8..a7da9fa3 100644 --- a/src/modules/scf/utils.ts +++ b/src/modules/scf/utils.ts @@ -61,10 +61,8 @@ export const formatInputs = (inputs: ScfCreateFunctionInputs) => { // 监听端口: -1 表示 job镜像,0 ~ 65526 表示webServer镜像 if (imageConfig.imagePort) { functionInputs.Code!.ImageConfig!.ImagePort = - Number.isInteger(imageConfig?.imagePort) && - imageConfig?.imagePort >= -1 && - imageConfig?.imagePort <= 65535 - ? imageConfig.imagePort + Number.isInteger(imageConfig?.imagePort) && imageConfig?.imagePort === -1 + ? -1 : WebServerImageDefaultPort; } } else { diff --git a/src/modules/triggers/apigw.ts b/src/modules/triggers/apigw.ts index 5438d621..cfaba5f4 100644 --- a/src/modules/triggers/apigw.ts +++ b/src/modules/triggers/apigw.ts @@ -147,7 +147,7 @@ export default class ApigwTrigger extends BaseTrigger funcInfo?: FunctionInfo; inputs: TriggerInputs; }) { - const { parameters, isAutoRelease } = inputs; + const { parameters, isAutoRelease, tags } = inputs; const { oldState, protocols, @@ -192,6 +192,7 @@ export default class ApigwTrigger extends BaseTrigger method: endpoints[0].method ?? 'ANY', }, created: !!parameters?.created, + tags, }; const triggerKey = this.getKey(triggerInputs); diff --git a/src/modules/triggers/interface/index.ts b/src/modules/triggers/interface/index.ts index ca07f7dc..0b7c65d8 100644 --- a/src/modules/triggers/interface/index.ts +++ b/src/modules/triggers/interface/index.ts @@ -1,4 +1,5 @@ import { ApigwDeployInputs, ApiEndpoint } from '../../apigw/interface'; +import { TagInput } from '../../interface'; export interface ApigwTriggerRemoveScfTriggerInputs { serviceId: string; @@ -129,6 +130,9 @@ export interface TriggerInputs

{ + return crypto.createHmac('sha1', key).update(text).digest('hex'); +}; + +/** + * getYunTiApiUrl 查询云梯API地址 + * @returns 云梯API地址 + */ +export const getYunTiApiUrl = (): string => { + const apiKey = process.env.SLS_YUNTI_API_KEY || ''; + const apiSecret = process.env.SLS_YUNTI_API_SECRET || ''; + const apiUrl = process.env.SLS_YUNTI_API_URL; + const timeStamp = Math.floor(Date.now() / 1000); + const apiSign = hmacSha1(`${timeStamp}${apiKey}`, apiSecret); + const url = `${apiUrl}?api_key=${apiKey}&api_ts=${timeStamp}&api_sign=${apiSign}`; + return url; +}; + +/** + * formatInputTags 格式化输入标签 + */ +export const formatInputTags = ( + input: Array | { [key: string]: string }, +): { key: string; value: string }[] => { + let tags: { key: string; value: string }[] = []; + if (Array.isArray(input)) { + tags = input.map((item) => { + return { + key: item?.key ?? item?.Key ?? '', + value: item?.value ?? item?.Value ?? '', + }; + }); + } else if (typeof input === 'object' && input !== null) { + tags = Object.entries(input).map(([key, value]) => { + return { + key: (key ?? '').toString(), + value: (value ?? '').toString(), + }; + }); + } else { + tags = []; + } + return tags; +}; + +/** + * 是否配置云梯标签 + * @param inputs ScfDeployInputs + * @returns true: 已配置云梯标签; false: 未配置云梯标签 + */ +export const isAddedYunTiTags = (inputs: ScfDeployInputs): boolean => { + let result = false; + let isApiAddedTags = false; + let isApiAddedYunTiTags = false; + inputs.events?.forEach((item) => { + const [[key, value]] = Object.entries(item); + console.log(key, value); + const tags = formatInputTags(value?.parameters?.tags ?? []); + if (key === 'apigw' && tags?.length > 0) { + isApiAddedTags = true; + } + if ( + key === 'apigw' && + tags?.length > 0 && + ['运营部门', '运营产品', '负责人'].every((tagKey) => + tags.some((tag) => tag.key === tagKey && !!tag.value), + ) + ) { + isApiAddedYunTiTags = true; + } + }); + + const functionTags: { TagKey: string; TagValue: string }[] = Object.entries( + inputs.tags ?? {}, + ).map(([TagKey, TagValue]) => ({ TagKey, TagValue })); + const isFunAddedYunTiTags = + functionTags?.length > 0 && + ['运营部门', '运营产品', '负责人'].every((key) => + functionTags.some((item) => item.TagKey === key && !!item.TagValue), + ); + result = isApiAddedYunTiTags || (!isApiAddedTags && isFunAddedYunTiTags); + return result; +};