mirror of
https://github.com/certd/certd.git
synced 2026-05-04 12:57:25 +08:00
perf: 支持火山云vke
This commit is contained in:
@@ -6,3 +6,4 @@ export * from './plugin-deploy-to-live.js'
|
||||
export * from './plugin-deploy-to-dcdn.js'
|
||||
export * from './plugin-deploy-to-vod.js'
|
||||
export * from './plugin-deploy-to-tos.js'
|
||||
export * from './plugin-deploy-to-vke.js'
|
||||
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { VolcengineAccess } from "../access.js";
|
||||
import { VolcengineClient } from "../ve-client.js";
|
||||
import { utils } from "@certd/basic";
|
||||
|
||||
const regionOptions = [
|
||||
{ label: "北京", value: "cn-beijing" },
|
||||
{ label: "上海", value: "cn-shanghai" },
|
||||
{ label: "广州", value: "cn-guangzhou" },
|
||||
{ label: "香港", value: "cn-hongkong" },
|
||||
{ label: "柔佛", value: "ap-southeast-1" },
|
||||
{ label: "雅加达", value: "ap-southeast-3" }
|
||||
];
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "VolcengineDeployToVKE",
|
||||
title: "火山引擎-替换VKE证书",
|
||||
icon: "svg:icon-volcengine",
|
||||
group: pluginGroups.volcengine.key,
|
||||
desc: "替换火山引擎VKE集群中的TLS Secret证书",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed
|
||||
}
|
||||
}
|
||||
})
|
||||
export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames]
|
||||
},
|
||||
required: true
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "Access授权",
|
||||
helper: "火山引擎AccessKeyId、AccessKeySecret",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "volcengine"
|
||||
},
|
||||
required: true
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "Region",
|
||||
helper: "VKE集群所在地域",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: regionOptions
|
||||
},
|
||||
value: "cn-beijing",
|
||||
required: true
|
||||
})
|
||||
regionId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "VKE集群",
|
||||
helper: "选择要替换证书的VKE集群,也可以手动输入集群ID",
|
||||
action: VolcengineDeployToVKE.prototype.onGetClusterList.name,
|
||||
watches: ["accessId", "regionId"],
|
||||
required: true
|
||||
})
|
||||
)
|
||||
clusterId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "Kubeconfig类型",
|
||||
helper: "Public需要集群API Server已开启公网访问;Private需要Certd能访问集群私网地址",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [
|
||||
{ label: "公网", value: "Public" },
|
||||
{ label: "私网", value: "Private" },
|
||||
{ label: "集群内", value: "TargetCluster" }
|
||||
]
|
||||
},
|
||||
value: "Public",
|
||||
required: true
|
||||
})
|
||||
kubeconfigType!: "Public" | "Private" | "TargetCluster";
|
||||
|
||||
@TaskInput({
|
||||
title: "命名空间",
|
||||
value: "default",
|
||||
component: {
|
||||
placeholder: "命名空间"
|
||||
},
|
||||
required: true
|
||||
})
|
||||
namespace!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "替换方式",
|
||||
helper: "按Ingress会自动读取spec.tls[].secretName;按Secret需要手动填写Secret名称",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [
|
||||
{ label: "按Ingress替换", value: "ingress" },
|
||||
{ label: "按Secret替换", value: "secret" }
|
||||
]
|
||||
},
|
||||
value: "ingress",
|
||||
required: true
|
||||
})
|
||||
targetType!: "ingress" | "secret";
|
||||
|
||||
@TaskInput({
|
||||
title: "IngressName",
|
||||
required: true,
|
||||
helper: "根据Ingress名称查找TLS Secret并替换",
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form}) => form.targetType === 'ingress'),
|
||||
required: ctx.compute(({form}) => form.targetType === 'ingress')
|
||||
}
|
||||
`
|
||||
})
|
||||
ingressName!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "Secret名称",
|
||||
required: true,
|
||||
helper: "存储TLS证书的Secret名称,可填写多个",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form}) => form.targetType === 'secret'),
|
||||
required: ctx.compute(({form}) => form.targetType === 'secret')
|
||||
}
|
||||
`
|
||||
})
|
||||
secretName!: string | string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "Secret自动创建",
|
||||
helper: "如果Secret不存在,则创建kubernetes.io/tls类型Secret",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked"
|
||||
}
|
||||
})
|
||||
createOnNotFound!: boolean;
|
||||
|
||||
@TaskInput({
|
||||
title: "忽略证书校验",
|
||||
helper: "连接Kubernetes API Server时跳过TLS校验",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked"
|
||||
}
|
||||
})
|
||||
skipTLSVerify!: boolean;
|
||||
|
||||
K8sClient: any;
|
||||
|
||||
async onInstance() {
|
||||
const sdk = await import("@certd/lib-k8s");
|
||||
this.K8sClient = sdk.K8sClient;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
this.logger.info("开始替换火山引擎VKE证书");
|
||||
const access = await this.getAccess<VolcengineAccess>(this.accessId);
|
||||
const vkeService = await this.getVkeService(access);
|
||||
const kubeconfigId = await this.createKubeconfig(vkeService);
|
||||
|
||||
try {
|
||||
const kubeconfig = await this.getKubeconfig(vkeService, kubeconfigId);
|
||||
const k8sClient = new this.K8sClient({
|
||||
kubeConfigStr: kubeconfig,
|
||||
logger: this.logger,
|
||||
skipTLSVerify: this.skipTLSVerify
|
||||
});
|
||||
const secretNames = await this.getTargetSecretNames(k8sClient);
|
||||
await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames });
|
||||
} catch (e) {
|
||||
if (e.response?.body) {
|
||||
throw new Error(JSON.stringify(e.response.body));
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
await this.deleteKubeconfig(vkeService, kubeconfigId);
|
||||
}
|
||||
|
||||
await utils.sleep(5000);
|
||||
this.logger.info("VKE证书替换完成");
|
||||
}
|
||||
|
||||
private async getVkeService(access: VolcengineAccess) {
|
||||
const client = new VolcengineClient({
|
||||
logger: this.logger,
|
||||
access,
|
||||
http: this.http
|
||||
});
|
||||
return await client.getVkeService({ region: this.regionId });
|
||||
}
|
||||
|
||||
private async createKubeconfig(vkeService: any) {
|
||||
const res = await vkeService.request({
|
||||
action: "CreateKubeconfig",
|
||||
method: "POST",
|
||||
body: {
|
||||
ClusterId: this.clusterId,
|
||||
Type: this.kubeconfigType,
|
||||
ValidDuration: 3600
|
||||
}
|
||||
});
|
||||
const kubeconfigId = res.Result?.Id || res.Id;
|
||||
if (!kubeconfigId) {
|
||||
throw new Error(`生成VKE Kubeconfig失败:${JSON.stringify(res)}`);
|
||||
}
|
||||
this.logger.info(`已生成临时Kubeconfig:${kubeconfigId}`);
|
||||
return kubeconfigId;
|
||||
}
|
||||
|
||||
private async getKubeconfig(vkeService: any, kubeconfigId: string) {
|
||||
const res = await vkeService.request({
|
||||
action: "ListKubeconfigs",
|
||||
method: "POST",
|
||||
body: {
|
||||
Filter: {
|
||||
ClusterIds: [this.clusterId],
|
||||
Ids: [kubeconfigId],
|
||||
Types: [this.kubeconfigType]
|
||||
},
|
||||
PageNumber: 1,
|
||||
PageSize: 10
|
||||
}
|
||||
});
|
||||
const items = res.Result?.Items || res.Items || [];
|
||||
const item = items.find((it: any) => it.Id === kubeconfigId) || items[0];
|
||||
const kubeconfig = item?.Kubeconfig;
|
||||
if (!kubeconfig) {
|
||||
throw new Error(`获取VKE Kubeconfig失败:${JSON.stringify(res)}`);
|
||||
}
|
||||
return kubeconfig;
|
||||
}
|
||||
|
||||
private async deleteKubeconfig(vkeService: any, kubeconfigId?: string) {
|
||||
if (!kubeconfigId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await vkeService.request({
|
||||
action: "DeleteKubeconfigs",
|
||||
method: "POST",
|
||||
body: {
|
||||
ClusterId: this.clusterId,
|
||||
Ids: [kubeconfigId]
|
||||
}
|
||||
});
|
||||
this.logger.info(`已删除临时Kubeconfig:${kubeconfigId}`);
|
||||
} catch (e) {
|
||||
this.logger.warn(`删除临时Kubeconfig失败:${e.message || e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getTargetSecretNames(k8sClient: any) {
|
||||
if (this.targetType === "secret") {
|
||||
if (typeof this.secretName === "string") {
|
||||
return [this.secretName];
|
||||
}
|
||||
return this.secretName || [];
|
||||
}
|
||||
|
||||
const ingressList = await k8sClient.getIngressList({
|
||||
namespace: this.namespace
|
||||
});
|
||||
const ingress = ingressList.items.find((item: any) => item.metadata.name === this.ingressName);
|
||||
if (!ingress) {
|
||||
throw new Error(`Ingress不存在:${this.ingressName}`);
|
||||
}
|
||||
const secretNames = ingress.spec?.tls?.map((tls: any) => tls.secretName).filter(Boolean) || [];
|
||||
if (secretNames.length === 0) {
|
||||
throw new Error(`Ingress:${this.ingressName} 未找到TLS Secret`);
|
||||
}
|
||||
return secretNames;
|
||||
}
|
||||
|
||||
private async patchCertSecret(options: { cert: CertInfo; k8sClient: any; secretNames: string[] }) {
|
||||
const { cert, k8sClient, secretNames } = options;
|
||||
if (!secretNames || secretNames.length === 0) {
|
||||
throw new Error("Secret名称不能为空");
|
||||
}
|
||||
|
||||
const body: any = {
|
||||
data: {
|
||||
"tls.crt": Buffer.from(cert.crt).toString("base64"),
|
||||
"tls.key": Buffer.from(cert.key).toString("base64")
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const secretName of secretNames) {
|
||||
body.metadata.name = secretName;
|
||||
this.logger.info(`开始更新VKE Secret:${secretName}`);
|
||||
await k8sClient.patchSecret({
|
||||
namespace: this.namespace,
|
||||
secretName,
|
||||
body,
|
||||
createOnNotFound: this.createOnNotFound
|
||||
});
|
||||
this.logger.info(`VKE Secret已更新:${secretName}`);
|
||||
}
|
||||
|
||||
if (this.targetType === "ingress" && this.ingressName) {
|
||||
await k8sClient.restartIngress(this.namespace, [this.ingressName], { certd: this.appendTimeSuffix("certd") });
|
||||
}
|
||||
}
|
||||
|
||||
async onGetClusterList() {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
const access = await this.getAccess<VolcengineAccess>(this.accessId);
|
||||
const service = await this.getVkeService(access);
|
||||
const res = await service.request({
|
||||
action: "ListClusters",
|
||||
method: "POST",
|
||||
body: {
|
||||
PageNumber: 1,
|
||||
PageSize: 100
|
||||
}
|
||||
});
|
||||
const list = res.Result?.Items || res.Items || [];
|
||||
return list.map((item: any) => ({
|
||||
label: `${item.Name || item.Id}<${item.Id}>`,
|
||||
value: item.Id
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
new VolcengineDeployToVKE();
|
||||
@@ -112,6 +112,20 @@ export class VolcengineClient {
|
||||
return service;
|
||||
}
|
||||
|
||||
async getVkeService(opts: { region?: string }) {
|
||||
const CommonService = await this.getServiceCls();
|
||||
|
||||
const service = new CommonService({
|
||||
serviceName: "vke",
|
||||
defaultVersion: "2022-05-12"
|
||||
});
|
||||
service.setAccessKeyId(this.opts.access.accessKeyId);
|
||||
service.setSecretKey(this.opts.access.secretAccessKey);
|
||||
service.setRegion(opts.region);
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
async getDCDNService( opts?: { }) {
|
||||
const CommonService = await this.getServiceCls();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user