import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; import { utils } from '@certd/basic'; import dayjs from 'dayjs'; import { AbstractPlusTaskPlugin } from '@certd/plugin-plus'; import { CertApplyPluginNames} from '@certd/plugin-cert'; @IsTaskPlugin({ name: 'DeployCertToTencentTKEIngress', title: '腾讯云-部署到TKE-ingress', needPlus: true, icon: 'svg:icon-tencentcloud', group: pluginGroups.tencent.key, desc: 'serverless集群请使用K8S部署插件;Qcloud类型需要【上传到腾讯云】作为前置任务;ApiServer未开启外网访问则需要做域名的内网IP映射', default: { strategy: { runStrategy: RunStrategy.SkipWhenSucceed, }, }, }) export class DeployCertToTencentTKEIngressPlugin extends AbstractPlusTaskPlugin { @TaskInput({ title: '大区', value: 'ap-guangzhou', required: true }) region!: string; @TaskInput({ title: '集群ID', required: true, desc: '例如:cls-6lbj1vee', request: true, }) clusterId!: string; @TaskInput({ title: '集群namespace', value: 'default', required: true }) namespace!: string; @TaskInput({ title: '证书的secret名称', required: true }) secretName!: string | string[]; @TaskInput({ title: 'ingress名称', required: true }) ingressName!: string | string[]; @TaskInput({ title: 'ingress类型', component: { name: 'a-auto-complete', vModel: 'value', options: [{ value: 'qcloud' }, { value: 'nginx' }], }, helper: '可选 qcloud / nginx', }) ingressClass!: string; // @TaskInput({ title: "集群内网ip", helper: "如果开启了外网的话,无需设置" }) // clusterIp!: string; @TaskInput({ title: '集群域名', helper: '可不填,默认为:[clusterId].ccs.tencent-cloud.com', }) clusterDomain!: string; /** * AccessProvider的key,或者一个包含access的具体的对象 */ @TaskInput({ title: 'Access授权', helper: 'access授权', component: { name: 'access-selector', type: 'tencent', }, required: true, }) accessId!: string; @TaskInput({ title: '腾讯云证书id', helper: '请选择“上传证书到腾讯云”前置任务的输出', component: { name: 'output-selector', from: 'UploadCertToTencent', }, mergeScript: ` return { show: ctx.compute(({form})=>{ return form.ingressClass === "qcloud" }) } `, required: true, }) tencentCertId!: string; @TaskInput({ title: '域名证书', helper: '请选择前置任务输出的域名证书', component: { name: 'output-selector', from: [...CertApplyPluginNames], }, mergeScript: ` return { show: ctx.compute(({form})=>{ return form.ingressClass === "nginx" }) } `, required: true, }) cert!: any; K8sClient: any; async onInstance() { // const TkeClient = this.tencentcloud.tke.v20180525.Client; const k8sSdk = await import('@certd/lib-k8s'); this.K8sClient = k8sSdk.K8sClient; } async execute(): Promise { const accessProvider = await this.accessService.getById(this.accessId); const tkeClient = await this.getTkeClient(accessProvider, this.region); const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, this.clusterId); this.logger.info('kubeconfig已成功获取'); const k8sClient = new this.K8sClient({ kubeConfigStr, logger: this.logger, }); // if (this.clusterIp != null) { // if (!this.clusterDomain) { // this.clusterDomain = `${this.clusterId}.ccs.tencent-cloud.com`; // } // // 修改内网解析ip地址 // k8sClient.setLookup({ [this.clusterDomain]: { ip: this.clusterIp } }); // } const ingressType = this.ingressClass || 'qcloud'; if (ingressType === 'qcloud') { await this.patchQcloudCertSecret({ k8sClient }); } else { await this.patchNginxCertSecret({ k8sClient }); } await utils.sleep(5000); // 停留2秒,等待secret部署完成 await this.restartIngress({ k8sClient }); } async getTkeClient(accessProvider: any, region = 'ap-guangzhou') { const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/tke/v20180525/index.js'); const TkeClient = sdk.v20180525.Client; const clientConfig = { credential: { secretId: accessProvider.secretId, secretKey: accessProvider.secretKey, }, region, profile: { httpProfile: { endpoint: 'tke.tencentcloudapi.com', }, }, }; return new TkeClient(clientConfig); } async getTkeKubeConfig(client: any, clusterId: string) { // Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher const params = { ClusterId: clusterId, }; const ret = await client.DescribeClusterKubeconfig(params); this.checkRet(ret); this.logger.info('注意:后续操作需要在【集群->基本信息】中开启外网或内网访问,https://console.cloud.tencent.com/tke2/cluster'); return ret.Kubeconfig; } appendTimeSuffix(name: string) { if (name == null) { name = 'certd'; } return name + '-' + dayjs().format('YYYYMMDD-HHmmss'); } async patchQcloudCertSecret(options: { k8sClient: any }) { if (this.tencentCertId == null) { throw new Error('请先将【上传证书到腾讯云】作为前置任务'); } this.logger.info('腾讯云证书ID:', this.tencentCertId); const certIdBase64 = Buffer.from(this.tencentCertId).toString('base64'); const { namespace, secretName } = this; const body = { data: { qcloud_cert_id: certIdBase64, }, metadata: { labels: { certd: this.appendTimeSuffix('certd'), }, }, }; let secretNames: any = secretName; if (typeof secretName === 'string') { secretNames = [secretName]; } for (const secret of secretNames) { await options.k8sClient.patchSecret({ namespace, secretName: secret, body, }); this.logger.info(`CertSecret已更新:${secret}`); } } async patchNginxCertSecret(options: { k8sClient: any }) { const { k8sClient } = options; const { cert } = this; const crt = cert.crt; const key = cert.key; const crtBase64 = Buffer.from(crt).toString('base64'); const keyBase64 = Buffer.from(key).toString('base64'); const { namespace, secretName } = this; const body = { data: { 'tls.crt': crtBase64, 'tls.key': keyBase64, }, metadata: { labels: { certd: this.appendTimeSuffix('certd'), }, }, }; let secretNames = secretName; if (typeof secretName === 'string') { secretNames = [secretName]; } for (const secret of secretNames) { await k8sClient.patchSecret({ namespace, secretName: secret, body }); this.logger.info(`CertSecret已更新:${secret}`); } } async restartIngress(options: { k8sClient: any }) { const { k8sClient } = options; const { namespace, ingressName } = this; const body = { metadata: { labels: { certd: this.appendTimeSuffix('certd'), }, }, }; let ingressNames = this.ingressName; if (typeof ingressName === 'string') { ingressNames = [ingressName]; } for (const ingress of ingressNames) { await k8sClient.patchIngress({ namespace, ingressName: ingress, body }); this.logger.info(`ingress已重启:${ingress}`); } } checkRet(ret: any) { if (!ret || ret.Error) { throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message); } } }