perf: 支持部署证书到百度CCE

This commit is contained in:
xiaojunnuo
2026-03-31 23:52:12 +08:00
parent 14229c2f00
commit a19ea7489c
5 changed files with 289 additions and 6 deletions

View File

@@ -14,7 +14,8 @@
"build3": "rollup -c", "build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build", "build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"pub": "npm publish" "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.39.7", "@certd/basic": "^1.39.7",

View File

@@ -59,9 +59,9 @@ export class K8sClient {
const yml = loadYaml<KubernetesObject>(manifest); const yml = loadYaml<KubernetesObject>(manifest);
const client = this.getKubeClient(); const client = this.getKubeClient();
try { try {
this.logger.info("apply yaml:", yml);
await client.create(yml); await client.create(yml);
} catch (e) { } catch (e) {
this.logger.error("apply error", e.response?.body);
if (e.response?.body?.reason === "AlreadyExists") { if (e.response?.body?.reason === "AlreadyExists") {
//patch //patch
this.logger.info("patch existing resource: ", yml.metadata?.name); this.logger.info("patch existing resource: ", yml.metadata?.name);
@@ -70,13 +70,26 @@ export class K8sClient {
yml.metadata = {}; yml.metadata = {};
} }
yml.metadata.resourceVersion = existing.body.metadata.resourceVersion; yml.metadata.resourceVersion = existing.body.metadata.resourceVersion;
await client.patch(yml); const res = await client.patch(yml);
return; return res?.body;
} }
throw e; throw e;
} }
} }
async applyPatch(manifest: string) {
const yml = loadYaml<KubernetesObject>(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'} } * @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
@@ -112,6 +125,7 @@ export class K8sClient {
*/ */
async createSecret(opts: { namespace: string; body: V1Secret }) { async createSecret(opts: { namespace: string; body: V1Secret }) {
const namespace = opts.namespace || "default"; const namespace = opts.namespace || "default";
this.logger.info("create secret:", opts.body.metadata);
const created = await this.client.createNamespacedSecret(namespace, opts.body); const created = await this.client.createNamespacedSecret(namespace, opts.body);
this.logger.info("new secrets:", opts.body.metadata); this.logger.info("new secrets:", opts.body.metadata);
return created.body; return created.body;
@@ -152,6 +166,8 @@ export class K8sClient {
this.logger.info(`secret ${secretName} 已创建`); this.logger.info(`secret ${secretName} 已创建`);
return res; return res;
} }
throw new Error(`secret ${secretName} 不存在`);
} }
throw e; throw e;
} }

View File

@@ -1,3 +1,4 @@
export * from "./plugin-deploy-to-cdn.js"; export * from "./plugin-deploy-to-cdn.js";
export * from "./plugin-deploy-to-blb.js"; export * from "./plugin-deploy-to-blb.js";
export * from "./plugin-upload-to-baidu.js"; export * from "./plugin-upload-to-baidu.js";
export * from "./plugin-deploy-to-cce.js";

View File

@@ -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<void> {
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();

View File

@@ -62,6 +62,21 @@ export class K8sApplyPlugin extends AbstractPlusTaskPlugin {
// }) // })
// namespace!: string; // 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({ @TaskInput({
title: "yaml", title: "yaml",
required: true, required: true,
@@ -112,8 +127,13 @@ export class K8sApplyPlugin extends AbstractPlusTaskPlugin {
try { try {
// this.logger.info("apply yaml:", compiledYaml); // this.logger.info("apply yaml:", compiledYaml);
// this.logger.info("apply yamlDoc:", JSON.stringify(doc)); // this.logger.info("apply yamlDoc:", JSON.stringify(doc));
const res = await client.apply(compiledYaml); if (this.strategy === "apply") {
this.logger.info("apply result:", res); await client.apply(compiledYaml);
this.logger.info("apply success");
} else {
await client.applyPatch(compiledYaml);
this.logger.info("patch success");
}
} catch (e) { } catch (e) {
if (e.response?.body) { if (e.response?.body) {
throw new Error(JSON.stringify(e.response.body)); throw new Error(JSON.stringify(e.response.body));