diff --git a/package-lock.json b/package-lock.json index b905b76..356cc67 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 2faee77..96ef2b6 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/apigw/index.ts b/src/modules/apigw/index.ts index 4885223..cff92c1 100644 --- a/src/modules/apigw/index.ts +++ b/src/modules/apigw/index.ts @@ -151,23 +151,6 @@ export default class Apigw { outputs.usagePlan = usagePlan; } - try { - const { tags } = inputs; - if (tags) { - await this.tagClient.deployResourceTags({ - tags: tags.map(({ key, value }) => ({ TagKey: key, TagValue: value })), - resourceId: serviceId, - serviceType: ApiServiceType.apigw, - resourcePrefix: 'service', - }); - if (tags.length > 0) { - outputs.tags = tags; - } - } - } catch (e) { - console.log(`[TAG] ${e.message}`); - } - // return this.formatApigwOutputs(outputs); return outputs; } diff --git a/src/modules/cam/apis.ts b/src/modules/cam/apis.ts index 99543d6..e3e5ee2 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 f98e5d0..9646b1b 100644 --- a/src/modules/cam/index.ts +++ b/src/modules/cam/index.ts @@ -112,4 +112,19 @@ 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: '', + }; + } + } } diff --git a/src/modules/scf/constants.ts b/src/modules/scf/constants.ts index bc6249f..4dd235d 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 e8ebd76..25bffdc 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,9 @@ 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'; +import { checkYunTi } from '../../utils/api'; /** 云函数组件 */ export default class Scf { @@ -252,6 +255,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 +263,7 @@ export default class Scf { namespace: funcInfo.Namespace, functionName: funcInfo.FunctionName, ...trigger, + tags: formatInputTags(tags), }, }); @@ -278,6 +283,19 @@ export default class Scf { const functionName = inputs.name; const { ignoreTriggers = false } = inputs; + // 自研账号,需要检查函数标签是否配置云梯标签(运营部门、运营产品、负责人); 非自研账号,不需要检查 + const cam = new Cam(this.credentials, this.region); + const userInfo = await cam.GetUserAppId(); + const isYunTi = await checkYunTi(userInfo?.OwnerUin); + 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 f5da63a..f714a7b 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/triggers/apigw.ts b/src/modules/triggers/apigw.ts index 5438d62..cfaba5f 100644 --- a/src/modules/triggers/apigw.ts +++ b/src/modules/triggers/apigw.ts @@ -147,7 +147,7 @@ export default class ApigwTrigger extends BaseTrigger<ApigwTriggerInputsParams> funcInfo?: FunctionInfo; inputs: TriggerInputs<ApigwTriggerInputsParams>; }) { - const { parameters, isAutoRelease } = inputs; + const { parameters, isAutoRelease, tags } = inputs; const { oldState, protocols, @@ -192,6 +192,7 @@ export default class ApigwTrigger extends BaseTrigger<ApigwTriggerInputsParams> 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 ca07f7d..0b7c65d 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<P extends TriggerInputsParams = TriggerInputsPara // 是否自动发布服务(API 网关特有) isAutoRelease?: boolean; + + // 标签列表 + tags?: TagInput[]; } export interface TriggerDetail { diff --git a/src/utils/api.ts b/src/utils/api.ts index cb6d497..c9ac121 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,5 +1,6 @@ import { Capi } from '@tencent-sdk/capi'; -import { deepClone } from '.'; +import axios from 'axios'; +import { deepClone, getYunTiApiUrl } from '.'; import { ApiServiceType } from '../modules/interface'; import { ApiError } from './error'; @@ -94,3 +95,38 @@ export function ApiFactory<ACTIONS_T extends readonly string[]>({ return APIS; } + +/** + * checkYunTi 查询账号是否是自研账号 + * @param uin 客户主账号 + * @returns true: 是自研账号; false: 不是自研账号 + */ +export const checkYunTi = async (uin: string) => { + let isYunTi = false; + 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/utils/index.ts b/src/utils/index.ts index 03c85cd..f902142 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,6 +3,8 @@ import path from 'path'; import camelCase from 'camelcase'; import { PascalCase } from 'type-fest'; import { CamelCasedProps, PascalCasedProps } from '../modules/interface'; +import crypto from 'crypto'; +import { ScfDeployInputs } from '../modules/scf/interface'; // TODO: 将一些库换成 lodash @@ -273,3 +275,93 @@ export const getQcsResourceId = (service: string, region: string, uin: string, s // 云资源六段式 return `qcs::${service}:${region}:uin/${uin}:${suffix}`; }; + +/** + * hmacSha1 加密HmacSHA1 + * @param text 加密文本 + * @param key 加密密钥 + * @returns + */ +export const hmacSha1 = (text: string, key: string) => { + 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<any> | { [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; +}; \ No newline at end of file