diff --git a/packages/libs/lib-k8s/package.json b/packages/libs/lib-k8s/package.json index 037286f10..ff142bd1a 100644 --- a/packages/libs/lib-k8s/package.json +++ b/packages/libs/lib-k8s/package.json @@ -14,7 +14,8 @@ "build3": "rollup -c", "build2": "vue-tsc --noEmit && vite build", "preview": "vite preview", - "pub": "npm publish" + "pub": "npm publish", + "compile": "tsc --skipLibCheck --watch" }, "dependencies": { "@certd/basic": "^1.39.7", diff --git a/packages/libs/lib-k8s/src/lib/k8s.client.ts b/packages/libs/lib-k8s/src/lib/k8s.client.ts index 99db8b2ff..7988da388 100644 --- a/packages/libs/lib-k8s/src/lib/k8s.client.ts +++ b/packages/libs/lib-k8s/src/lib/k8s.client.ts @@ -59,9 +59,9 @@ export class K8sClient { const yml = loadYaml(manifest); const client = this.getKubeClient(); try { + this.logger.info("apply yaml:", yml); await client.create(yml); } catch (e) { - this.logger.error("apply error", e.response?.body); if (e.response?.body?.reason === "AlreadyExists") { //patch this.logger.info("patch existing resource: ", yml.metadata?.name); @@ -70,13 +70,26 @@ export class K8sClient { yml.metadata = {}; } yml.metadata.resourceVersion = existing.body.metadata.resourceVersion; - await client.patch(yml); - return; + const res = await client.patch(yml); + return res?.body; } throw e; } } + async applyPatch(manifest: string) { + const yml = loadYaml(manifest); + const client = this.getKubeClient(); + this.logger.info("patch yaml:", yml); + const existing = await client.read(yml as any); + if (!yml.metadata) { + yml.metadata = {}; + } + yml.metadata.resourceVersion = existing.body.metadata.resourceVersion; + const res = await client.patch(yml); + return res?.body; + } + /** * * @param localRecords { [domain]:{ip:'xxx.xx.xxx'} } @@ -112,6 +125,7 @@ export class K8sClient { */ async createSecret(opts: { namespace: string; body: V1Secret }) { const namespace = opts.namespace || "default"; + this.logger.info("create secret:", opts.body.metadata); const created = await this.client.createNamespacedSecret(namespace, opts.body); this.logger.info("new secrets:", opts.body.metadata); return created.body; @@ -152,6 +166,8 @@ export class K8sClient { this.logger.info(`secret ${secretName} 已创建`); return res; } + + throw new Error(`secret ${secretName} 不存在`); } throw e; } diff --git a/packages/ui/certd-server/src/plugins/plugin-plus/baidu/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-plus/baidu/plugins/index.ts index 02a15f578..60f2b557e 100644 --- a/packages/ui/certd-server/src/plugins/plugin-plus/baidu/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-plus/baidu/plugins/index.ts @@ -1,3 +1,4 @@ export * from "./plugin-deploy-to-cdn.js"; export * from "./plugin-deploy-to-blb.js"; export * from "./plugin-upload-to-baidu.js"; +export * from "./plugin-deploy-to-cce.js"; diff --git a/packages/ui/certd-server/src/plugins/plugin-plus/baidu/plugins/plugin-deploy-to-cce.ts b/packages/ui/certd-server/src/plugins/plugin-plus/baidu/plugins/plugin-deploy-to-cce.ts new file mode 100644 index 000000000..45d251e14 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-plus/baidu/plugins/plugin-deploy-to-cce.ts @@ -0,0 +1,245 @@ +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { utils } from "@certd/basic"; + +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { BaiduAccess } from "../access.js"; +import { BaiduYunClient } from "../client.js"; + +@IsTaskPlugin({ + name: "DeployCertToBaiduCce", + title: "百度云-部署到CCE", + icon: "ant-design:cloud-outlined", + desc: "部署到百度云CCE集群Ingress等通过Secret管理证书的应用", + group: pluginGroups.baidu.key, + needPlus: true, + input: {}, + output: {}, + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class DeployCertToBaiduCcePlugin extends AbstractTaskPlugin { + @TaskInput({ + title: "域名证书", + helper: "请选择前置任务输出的域名证书", + component: { + name: "output-selector", + from: [...CertApplyPluginNames], + }, + required: true, + }) + cert!: CertInfo; + @TaskInput({ + title: "Access授权", + helper: "百度云授权AccessKey、SecretKey", + component: { + name: "access-selector", + type: "baidu", + }, + required: true, + }) + accessId!: string; + + @TaskInput({ + title: "大区", + component: { + name: "a-auto-complete", + vModel: "value", + options: [ + { value: "bj", label: "北京" }, + { value: "gz", label: "广州" }, + { value: "su", label: "苏州" }, + { value: "bd", label: "保定" }, + { value: "fwh", label: "武汉" }, + { value: "hkg", label: "香港" }, + { value: "yq", label: "阳泉" }, + { value: "cd", label: "成都" }, + { value: "nj", label: "南京" }, + ], + placeholder: "集群所属大区", + }, + required: true, + }) + regionId!: string; + + @TaskInput({ + title: "集群id", + component: { + placeholder: "集群id", + }, + required: true, + }) + clusterId!: string; + + @TaskInput({ + title: "保密字典Id", + component: { + placeholder: "保密字典Id", + }, + helper: "原本存储证书的secret的name", + required: true, + }) + secretName!: string | string[]; + + @TaskInput({ + title: "命名空间", + value: "default", + component: { + placeholder: "命名空间", + }, + required: true, + }) + namespace = "default"; + + @TaskInput({ + title: "Kubeconfig类型", + value: "public", + component: { + name: "a-auto-complete", + vModel: "value", + options: [ + { value: "vpc", label: "VPC私网IP (BLB VPCIP)" }, + { value: "public", label: "公网IP (BLB EIP)" }, + ], + placeholder: "选择集群连接端点类型", + }, + helper: "VPC类型使用私网IP连接,需要certd运行在同一网络环境;public类型使用公网IP连接", + required: true, + }) + kubeconfigType!: string; + + @TaskInput({ + title: "忽略证书校验", + required: false, + helper: "是否忽略证书校验", + component: { + name: "a-switch", + vModel: "checked", + }, + }) + skipTLSVerify!: boolean; + + @TaskInput({ + title: "Secret自动创建", + helper: "如果Secret不存在,则创建,百度云的自动创建secret有问题", + value: false, + component: { + name: "a-switch", + vModel: "checked", + }, + }) + createOnNotFound: boolean; + + K8sClient: any; + async onInstance() { + const sdk = await import("@certd/lib-k8s"); + this.K8sClient = sdk.K8sClient; + } + async execute(): Promise { + this.logger.info("开始部署证书到百度云CCE"); + const { regionId, clusterId, kubeconfigType, cert } = this; + const access = (await this.getAccess(this.accessId)) as BaiduAccess; + const client = new BaiduYunClient({ + access, + logger: this.logger, + http: this.ctx.http, + }); + const kubeConfigStr = await this.getKubeConfig(client, clusterId, regionId, kubeconfigType); + + this.logger.info("kubeconfig已成功获取"); + const k8sClient = new this.K8sClient({ + kubeConfigStr, + logger: this.logger, + skipTLSVerify: this.skipTLSVerify, + }); + await this.patchCertSecret({ cert, k8sClient }); + + await utils.sleep(5000); + + try { + await this.restartIngress({ k8sClient }); + } catch (e) { + this.logger.warn(`重启ingress失败:${e.message}`); + } + } + + async restartIngress(options: { k8sClient: any }) { + const { k8sClient } = options; + const { namespace } = this; + + const body = { + metadata: { + labels: { + certd: this.appendTimeSuffix("certd"), + }, + }, + }; + const ingressList = await k8sClient.getIngressList({ namespace }); + this.logger.info("ingressList:", JSON.stringify(ingressList)); + if (!ingressList || !ingressList.items) { + return; + } + const ingressNames = ingressList.items + .filter((item: any) => { + if (!item.spec.tls) { + return false; + } + for (const tls of item.spec.tls) { + if (tls.secretName === this.secretName) { + return true; + } + } + return false; + }) + .map((item: any) => { + return item.metadata.name; + }); + for (const ingress of ingressNames) { + await k8sClient.patchIngress({ namespace, ingressName: ingress, body, createOnNotFound: this.createOnNotFound }); + this.logger.info(`ingress已重启:${ingress}`); + } + } + + async patchCertSecret(options: { cert: CertInfo; k8sClient: any }) { + const { cert, k8sClient } = options; + 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: any = secretName; + if (typeof secretName === "string") { + secretNames = [secretName]; + } + for (const secret of secretNames) { + await k8sClient.patchSecret({ namespace, secretName: secret, body ,createOnNotFound: this.createOnNotFound}); + this.logger.info(`cert secret已更新: ${secret}`); + } + } + + async getKubeConfig(client: BaiduYunClient, clusterId: string, regionId: string, kubeconfigType: string) { + const res = await client.doRequest({ + host: `cce.${regionId}.baidubce.com`, + uri: `/v2/kubeconfig/${clusterId}/${kubeconfigType}`, + method: "get", + }); + return res.kubeConfig; + } +} + +new DeployCertToBaiduCcePlugin(); \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-plus/k8s/plugins/plugin-apply.ts b/packages/ui/certd-server/src/plugins/plugin-plus/k8s/plugins/plugin-apply.ts index d51b75864..b859494e5 100644 --- a/packages/ui/certd-server/src/plugins/plugin-plus/k8s/plugins/plugin-apply.ts +++ b/packages/ui/certd-server/src/plugins/plugin-plus/k8s/plugins/plugin-apply.ts @@ -62,6 +62,21 @@ export class K8sApplyPlugin extends AbstractPlusTaskPlugin { // }) // namespace!: string; + @TaskInput({ + title: "应用策略", + helper: "选择使用apply(创建或更新)还是patch(补丁更新)", + component: { + name: "a-select", + options: [ + { label: "apply(创建)", value: "apply" }, + { label: "patch(更新)", value: "patch" }, + ], + }, + value: "apply", + required: true, + }) + strategy!: string; + @TaskInput({ title: "yaml", required: true, @@ -112,8 +127,13 @@ export class K8sApplyPlugin extends AbstractPlusTaskPlugin { try { // this.logger.info("apply yaml:", compiledYaml); // this.logger.info("apply yamlDoc:", JSON.stringify(doc)); - const res = await client.apply(compiledYaml); - this.logger.info("apply result:", res); + if (this.strategy === "apply") { + await client.apply(compiledYaml); + this.logger.info("apply success"); + } else { + await client.applyPatch(compiledYaml); + this.logger.info("patch success"); + } } catch (e) { if (e.response?.body) { throw new Error(JSON.stringify(e.response.body));