Merge branch 'v2-dev' into codex_i18n

This commit is contained in:
xiaojunnuo
2026-05-01 00:07:29 +08:00
52 changed files with 795 additions and 84 deletions
+13
View File
@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
* 修复腾讯云clb部署报缺少sslmode参数的bug ([2f1ad72](https://github.com/certd/certd/commit/2f1ad7201f5ed9e00368a28b9e40907d4b415852))
### Performance Improvements
* 524错误时重试3次 ([00e6d58](https://github.com/certd/certd/commit/00e6d580c2f54af70fe96a214aff87c4b96426c2))
* 阿里云证书订单支持获取2.0的订单 ([64b3184](https://github.com/certd/certd/commit/64b3184b286fee996002d857b0de588452abdadd))
* 优化流水线执行时的状态保存性能 ([e00830b](https://github.com/certd/certd/commit/e00830bebcfe6344499e490bc174de96f9fb22d6))
* 支持页脚自定义 ([c985a13](https://github.com/certd/certd/commit/c985a13544aa31b0eb0783f9a3193a7e8bdc6ed6))
## [1.39.11](https://github.com/certd/certd/compare/v1.39.10...v1.39.11) (2026-04-26)
### Bug Fixes
@@ -6,7 +6,7 @@ name: CertApplyGetFormAliyun
icon: ph:certificate
title: 获取阿里云订阅证书
group: cert
desc: 从阿里云拉取订阅模式的商用证书
desc: 从阿里云拉取订阅模式的商用证书(支持 API 1.0 和 2.0
input:
domains:
title: 证书域名
@@ -49,27 +49,28 @@ input:
order: -1
helper: 请输入邮箱
accessId:
title: Access授权
helper: 阿里云授权AccessKeyId、AccessKeySecret
title: Access 授权
helper: 阿里云授权 AccessKeyId、AccessKeySecret
component:
name: access-selector
type: aliyun
required: true
order: 0
orderType:
title: 订单类型
value: CPACK
apiVersion:
title: 证书API 版本
value: v1
component:
name: a-select
vModel: value
options:
- label: 资源虚拟订单(一般选这个)
value: CPACK
- label: 售卖订单
value: BUY
- label: API 1.0 (旧版)
value: v1
- label: API 2.0 (新版)
value: v2
helper: 选择阿里云证书 API 版本
order: 0
orderId:
title: 证书订单ID
title: 证书订单 ID
component:
name: RemoteSelect
vModel: value
@@ -95,7 +96,7 @@ input:
},
}
helper: 订阅模式的证书订单Id
helper: 订阅模式的证书订单 Id(在新建流水线时暂时无法获取,可以先随便填个数字,先创建,进入流水线编辑页面再获取选择即可)
order: 0
pfxPassword:
title: 证书加密密码
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.39.11",
"version": "1.39.12",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -52,20 +52,20 @@
"@aws-sdk/client-sts": "^3.990.0",
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1",
"@certd/acme-client": "^1.39.11",
"@certd/basic": "^1.39.11",
"@certd/commercial-core": "^1.39.11",
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/commercial-core": "^1.39.12",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.39.11",
"@certd/lib-huawei": "^1.39.11",
"@certd/lib-k8s": "^1.39.11",
"@certd/lib-server": "^1.39.11",
"@certd/midway-flyway-js": "^1.39.11",
"@certd/pipeline": "^1.39.11",
"@certd/plugin-cert": "^1.39.11",
"@certd/plugin-lib": "^1.39.11",
"@certd/plugin-plus": "^1.39.11",
"@certd/plus-core": "^1.39.11",
"@certd/jdcloud": "^1.39.12",
"@certd/lib-huawei": "^1.39.12",
"@certd/lib-k8s": "^1.39.12",
"@certd/lib-server": "^1.39.12",
"@certd/midway-flyway-js": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-cert": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/plugin-plus": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185",
@@ -702,11 +702,16 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
await doSaveHistory(latest);
}
async start(){
this.started = true
//先存一次,确保有数据
await this.save();
setTimeout(()=>{
//2秒后保存一次,尽快显示第一个任务的状态
this.save();
}, 1000 * 2);
this.interval = setInterval(()=>{
//之后每5秒保存一次
this.save();
}, 1000 * 5);
}
@@ -55,7 +55,7 @@ export class CertApplyGetFormAliyunPlugin extends CertApplyBasePlugin {
@TaskInput(
createRemoteSelectInputDefine({
title: "证书订单 ID",
helper: "订阅模式的证书订单 Id",
helper: "订阅模式的证书订单 Id(在新建流水线时暂时无法获取,可以先随便填个数字,先创建,进入流水线编辑页面再获取选择即可)",
typeName: "CertApplyGetFormAliyun",
pageSize: 50,
component: {
@@ -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'
@@ -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();