mirror of
https://github.com/certd/certd.git
synced 2026-07-06 03:47:34 +08:00
Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev
This commit is contained in:
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.41.1](https://github.com/certd/certd/compare/v1.41.0...v1.41.1) (2026-06-05)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 流水线、监控站点支持导出 ([99fd308](https://github.com/certd/certd/commit/99fd3083f259cdb96fd656f04858dd708d1251c7))
|
||||
* 优化邀请注册流程 ([7a71e45](https://github.com/certd/certd/commit/7a71e45799d782d0691606fb42b4236f1d3009b0))
|
||||
* **volcengine-vke:** 火山VKE集群证书支持两种类型的证书保密字典 ([77b8024](https://github.com/certd/certd/commit/77b802445322d576d54d194f7c505da49e0e824c))
|
||||
|
||||
# [1.41.0](https://github.com/certd/certd/compare/v1.40.5...v1.41.0) (2026-06-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -7,6 +7,7 @@ title: 火山引擎-替换VKE证书
|
||||
icon: svg:icon-volcengine
|
||||
group: volcengine
|
||||
desc: 替换火山引擎VKE集群中的TLS Secret证书
|
||||
needPlus: true
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
@@ -15,6 +16,7 @@ input:
|
||||
name: output-selector
|
||||
from:
|
||||
- ':cert:'
|
||||
- VolcengineUploadToCertCenter
|
||||
required: true
|
||||
order: 0
|
||||
certDomains:
|
||||
@@ -118,11 +120,11 @@ input:
|
||||
component:
|
||||
name: a-select
|
||||
options:
|
||||
- label: 按Ingress替换
|
||||
value: ingress
|
||||
- label: 按Secret替换
|
||||
value: secret
|
||||
value: ingress
|
||||
- label: 按Ingress替换
|
||||
value: ingress
|
||||
value: secret
|
||||
required: true
|
||||
order: 0
|
||||
ingressName:
|
||||
@@ -140,23 +142,37 @@ input:
|
||||
secretName:
|
||||
title: Secret名称
|
||||
required: true
|
||||
helper: 存储TLS证书的Secret名称,可填写多个
|
||||
helper: 选择要替换的Secret,可多选
|
||||
component:
|
||||
name: a-select
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
open: false
|
||||
type: plugin
|
||||
action: onGetSecretList
|
||||
search: false
|
||||
pager: false
|
||||
single: false
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
- regionId
|
||||
- clusterId
|
||||
- kubeconfigType
|
||||
- namespace
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form}) => form.targetType === 'secret'),
|
||||
required: ctx.compute(({form}) => form.targetType === 'secret')
|
||||
required: ctx.compute(({form}) => form.targetType === 'secret'),
|
||||
component: {
|
||||
form: ctx.compute(({form}) => form)
|
||||
}
|
||||
}
|
||||
|
||||
order: 0
|
||||
createOnNotFound:
|
||||
title: Secret自动创建
|
||||
helper: 如果Secret不存在,则创建kubernetes.io/tls类型Secret
|
||||
helper: 如果Secret不存在,则创建Opaque类型Secret
|
||||
value: false
|
||||
component:
|
||||
name: a-switch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-server",
|
||||
"version": "1.41.0",
|
||||
"version": "1.41.1",
|
||||
"description": "fast-server base midway",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -54,20 +54,20 @@
|
||||
"@aws-sdk/client-sts": "^3.990.0",
|
||||
"@azure/arm-dns": "^5.1.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@certd/acme-client": "^1.41.0",
|
||||
"@certd/basic": "^1.41.0",
|
||||
"@certd/commercial-core": "^1.41.0",
|
||||
"@certd/acme-client": "^1.41.1",
|
||||
"@certd/basic": "^1.41.1",
|
||||
"@certd/commercial-core": "^1.41.1",
|
||||
"@certd/cv4pve-api-javascript": "^8.4.2",
|
||||
"@certd/jdcloud": "^1.41.0",
|
||||
"@certd/lib-huawei": "^1.41.0",
|
||||
"@certd/lib-k8s": "^1.41.0",
|
||||
"@certd/lib-server": "^1.41.0",
|
||||
"@certd/midway-flyway-js": "^1.41.0",
|
||||
"@certd/pipeline": "^1.41.0",
|
||||
"@certd/plugin-cert": "^1.41.0",
|
||||
"@certd/plugin-lib": "^1.41.0",
|
||||
"@certd/plugin-plus": "^1.41.0",
|
||||
"@certd/plus-core": "^1.41.0",
|
||||
"@certd/jdcloud": "^1.41.1",
|
||||
"@certd/lib-huawei": "^1.41.1",
|
||||
"@certd/lib-k8s": "^1.41.1",
|
||||
"@certd/lib-server": "^1.41.1",
|
||||
"@certd/midway-flyway-js": "^1.41.1",
|
||||
"@certd/pipeline": "^1.41.1",
|
||||
"@certd/plugin-cert": "^1.41.1",
|
||||
"@certd/plugin-lib": "^1.41.1",
|
||||
"@certd/plugin-plus": "^1.41.1",
|
||||
"@certd/plus-core": "^1.41.1",
|
||||
"@google-cloud/dns": "^5.3.1",
|
||||
"@google-cloud/publicca": "^1.3.0",
|
||||
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
|
||||
|
||||
@@ -30,16 +30,23 @@ export class AwsClient {
|
||||
},
|
||||
});
|
||||
|
||||
const cert = certInfo.crt.split("-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----";
|
||||
// Split the full PEM chain: first block is the leaf cert, the rest is the intermediate chain
|
||||
const pemBlocks = certInfo.crt.split(/(?<=-----END CERTIFICATE-----)/);
|
||||
const cert = pemBlocks[0].trim();
|
||||
const chain = pemBlocks
|
||||
.slice(1)
|
||||
.join("")
|
||||
.trim();
|
||||
|
||||
// 构建上传参数
|
||||
const data = await acmClient.send(
|
||||
new ImportCertificateCommand({
|
||||
Certificate: Buffer.from(cert),
|
||||
PrivateKey: Buffer.from(certInfo.key),
|
||||
// CertificateChain: certificateChain, // 可选
|
||||
CertificateChain: chain ? Buffer.from(chain) : undefined,
|
||||
})
|
||||
);
|
||||
console.log("Upload successful:", data);
|
||||
this.logger.info(`Upload successful: ${data.CertificateArn}`);
|
||||
// 返回证书 ARN(Amazon Resource Name)
|
||||
return data.CertificateArn;
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
|
||||
|
||||
this._result.pipelineVars.certEffectiveTime = dayjs(certReader.detail.notBefore).valueOf();
|
||||
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
|
||||
this._result.pipelineVars.certDomains = certReader.getAllDomains();
|
||||
if (!this._result.pipelinePrivateVars) {
|
||||
this._result.pipelinePrivateVars = {};
|
||||
}
|
||||
|
||||
+62
@@ -3,6 +3,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { VolcengineDeployToVKE } from "./plugin-deploy-to-vke.js";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
|
||||
describe("VolcengineDeployToVKE", () => {
|
||||
it("uses a single-select cluster field", () => {
|
||||
@@ -86,4 +87,65 @@ describe("VolcengineDeployToVKE", () => {
|
||||
/当前命名空间可用Ingress:app-web,api-web/
|
||||
);
|
||||
});
|
||||
|
||||
it("creates Secret with cert_center format for new non-tls secrets", async () => {
|
||||
const plugin = new VolcengineDeployToVKE();
|
||||
plugin.namespace = "default";
|
||||
plugin.targetType = "secret";
|
||||
plugin.secretName = "test-tls";
|
||||
plugin.createOnNotFound = true;
|
||||
plugin.logger = { info: () => undefined } as any;
|
||||
plugin.appendTimeSuffix = (s: string) => s + "-test";
|
||||
|
||||
let secretBody: any;
|
||||
await (plugin as any).patchCertSecret({
|
||||
certId: "cert-abc123",
|
||||
k8sClient: {
|
||||
patchSecret: async (opts: any) => {
|
||||
secretBody = opts.body;
|
||||
return {};
|
||||
},
|
||||
client: {
|
||||
readNamespacedSecret: async () => {
|
||||
throw Object.assign(new Error("Not Found"), { response: { body: { code: 404 } } });
|
||||
},
|
||||
},
|
||||
},
|
||||
secretNames: ["test-tls"],
|
||||
});
|
||||
|
||||
assert.equal(secretBody.type, "Opaque");
|
||||
assert.equal(secretBody.data["cert_id"], Buffer.from("cert-abc123").toString("base64"));
|
||||
assert.equal(secretBody.data["cert_source"], Buffer.from("cert_center").toString("base64"));
|
||||
});
|
||||
|
||||
it("uses tls.crt/tls.key format for kubernetes.io/tls secrets", async () => {
|
||||
const plugin = new VolcengineDeployToVKE();
|
||||
plugin.namespace = "default";
|
||||
plugin.targetType = "secret";
|
||||
plugin.secretName = "test-tls";
|
||||
plugin.logger = { info: () => undefined } as any;
|
||||
plugin.appendTimeSuffix = (s: string) => s + "-test";
|
||||
plugin.cert = { crt: "MY_CRT", key: "MY_KEY" } as CertInfo;
|
||||
|
||||
let secretBody: any;
|
||||
await (plugin as any).patchCertSecret({
|
||||
certId: "cert-abc123",
|
||||
k8sClient: {
|
||||
patchSecret: async (opts: any) => {
|
||||
secretBody = opts.body;
|
||||
return {};
|
||||
},
|
||||
client: {
|
||||
readNamespacedSecret: async () => ({
|
||||
body: { type: "kubernetes.io/tls" },
|
||||
}),
|
||||
},
|
||||
},
|
||||
secretNames: ["test-tls"],
|
||||
});
|
||||
|
||||
assert.equal(secretBody.data["tls.crt"], Buffer.from("MY_CRT").toString("base64"));
|
||||
assert.equal(secretBody.data["tls.key"], Buffer.from("MY_KEY").toString("base64"));
|
||||
});
|
||||
});
|
||||
|
||||
+138
-31
@@ -1,9 +1,10 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { utils } from "@certd/basic";
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-plus";
|
||||
import { VolcengineAccess } from "../access.js";
|
||||
import { VolcengineClient } from "../ve-client.js";
|
||||
import { utils } from "@certd/basic";
|
||||
|
||||
const regionOptions = [
|
||||
{ label: "北京", value: "cn-beijing" },
|
||||
@@ -20,23 +21,24 @@ const regionOptions = [
|
||||
icon: "svg:icon-volcengine",
|
||||
group: pluginGroups.volcengine.key,
|
||||
desc: "替换火山引擎VKE集群中的TLS Secret证书",
|
||||
needPlus:true,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
from: [...CertApplyPluginNames, "VolcengineUploadToCertCenter"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
cert!: CertInfo | string;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
@@ -107,11 +109,11 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [
|
||||
{ label: "按Ingress替换", value: "ingress" },
|
||||
{ label: "按Secret替换", value: "secret" },
|
||||
{ label: "按Ingress替换", value: "ingress" },
|
||||
],
|
||||
},
|
||||
value: "ingress",
|
||||
value: "secret",
|
||||
required: true,
|
||||
})
|
||||
targetType!: "ingress" | "secret";
|
||||
@@ -132,17 +134,25 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "Secret名称",
|
||||
required: true,
|
||||
helper: "存储TLS证书的Secret名称,可填写多个",
|
||||
helper: "选择要替换的Secret,可多选",
|
||||
component: {
|
||||
name: "a-select",
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
type: "plugin",
|
||||
action: "onGetSecretList",
|
||||
search: false,
|
||||
pager: false,
|
||||
single: false,
|
||||
watches: ["certDomains", "accessId", "regionId", "clusterId", "kubeconfigType", "namespace"],
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form}) => form.targetType === 'secret'),
|
||||
required: ctx.compute(({form}) => form.targetType === 'secret')
|
||||
required: ctx.compute(({form}) => form.targetType === 'secret'),
|
||||
component: {
|
||||
form: ctx.compute(({form}) => form)
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
@@ -150,7 +160,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
|
||||
@TaskInput({
|
||||
title: "Secret自动创建",
|
||||
helper: "如果Secret不存在,则创建kubernetes.io/tls类型Secret",
|
||||
helper: "如果Secret不存在,则创建Opaque类型Secret",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
@@ -181,6 +191,23 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
this.logger.info("开始替换火山引擎VKE证书");
|
||||
const access = await this.getAccess<VolcengineAccess>(this.accessId);
|
||||
const vkeService = await this.getVkeService(access);
|
||||
|
||||
// 上传证书到证书中心
|
||||
let certId: string;
|
||||
if (typeof this.cert !== "string") {
|
||||
const certInfo = this.cert as CertInfo;
|
||||
this.logger.info("开始上传证书到证书中心");
|
||||
const certService = await this.getCertService(access);
|
||||
certId = await certService.ImportCertificate({
|
||||
certName: this.appendTimeSuffix("certd"),
|
||||
cert: certInfo,
|
||||
});
|
||||
this.logger.info("上传证书到证书中心成功:" + certId);
|
||||
} else {
|
||||
certId = this.cert;
|
||||
this.logger.info("使用已有证书中心ID:" + certId);
|
||||
}
|
||||
|
||||
const kubeconfigId = await this.createKubeconfig(vkeService);
|
||||
|
||||
try {
|
||||
@@ -191,7 +218,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
skipTLSVerify: this.skipTLSVerify,
|
||||
});
|
||||
const secretNames = await this.getTargetSecretNames(k8sClient);
|
||||
await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames });
|
||||
await this.patchCertSecret({ certId, k8sClient, secretNames });
|
||||
} catch (e) {
|
||||
if (e.response?.body) {
|
||||
throw new Error(this.formatK8sError(e.response.body));
|
||||
@@ -205,6 +232,15 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
this.logger.info("VKE证书替换完成");
|
||||
}
|
||||
|
||||
private async getCertService(access: VolcengineAccess) {
|
||||
const client = new VolcengineClient({
|
||||
logger: this.logger,
|
||||
access,
|
||||
http: this.http,
|
||||
});
|
||||
return await client.getCertCenterService();
|
||||
}
|
||||
|
||||
private async getVkeService(access: VolcengineAccess) {
|
||||
const client = new VolcengineClient({
|
||||
logger: this.logger,
|
||||
@@ -293,6 +329,11 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
private formatK8sError(body: any) {
|
||||
if (body?.code === 422 && body?.message?.includes("field is immutable")) {
|
||||
const secretName = body.details?.name || "未知";
|
||||
return `Secret类型不可变:Secret ${secretName} 已是kubernetes.io/tls类型,type字段不可修改。\n请删除该Secret后重试,或选择正确的Secret。\n原始错误:${JSON.stringify(body)}`;
|
||||
}
|
||||
|
||||
if (body?.code !== 403 || body?.reason !== "Forbidden") {
|
||||
return JSON.stringify(body);
|
||||
}
|
||||
@@ -334,34 +375,71 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
return secretNames;
|
||||
}
|
||||
|
||||
private async patchCertSecret(options: { cert: CertInfo; k8sClient: any; secretNames: string[] }) {
|
||||
const { cert, k8sClient, secretNames } = options;
|
||||
private async patchCertSecret(options: { certId: string; k8sClient: any; secretNames: string[] }) {
|
||||
const { certId, 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) {
|
||||
let useTlsFormat = false;
|
||||
try {
|
||||
const res = await k8sClient.client.readNamespacedSecret(secretName, this.namespace);
|
||||
useTlsFormat = res.body?.type === "kubernetes.io/tls";
|
||||
} catch (e) {
|
||||
// Secret 不存在,将走创建逻辑
|
||||
}
|
||||
|
||||
let body: any;
|
||||
if (useTlsFormat) {
|
||||
let crt: string;
|
||||
let key: string;
|
||||
if (typeof this.cert === "string") {
|
||||
const access = await this.getAccess<VolcengineAccess>(this.accessId);
|
||||
const certService = await this.getCertService(access);
|
||||
const detail = await certService.GetCertificateDetail(this.cert);
|
||||
crt = detail.CertificateChain || "";
|
||||
key = detail.PrivateKey || "";
|
||||
this.logger.info("从证书中心获取证书详情成功");
|
||||
} else {
|
||||
crt = this.cert.crt;
|
||||
key = this.cert.key;
|
||||
}
|
||||
body = {
|
||||
data: {
|
||||
"tls.crt": Buffer.from(crt).toString("base64"),
|
||||
"tls.key": Buffer.from(key).toString("base64"),
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd"),
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
body = {
|
||||
type: "Opaque",
|
||||
data: {
|
||||
cert_id: Buffer.from(certId).toString("base64"),
|
||||
cert_source: Buffer.from("cert_center").toString("base64"),
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd"),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
body.metadata.name = secretName;
|
||||
this.logger.info(`开始更新VKE Secret:${secretName}`);
|
||||
this.logger.info("开始更新VKE Secret:" + secretName);
|
||||
await k8sClient.patchSecret({
|
||||
namespace: this.namespace,
|
||||
secretName,
|
||||
body,
|
||||
createOnNotFound: this.createOnNotFound,
|
||||
});
|
||||
this.logger.info(`VKE Secret已更新:${secretName}`);
|
||||
this.logger.info("VKE Secret已更新:" + secretName);
|
||||
}
|
||||
|
||||
if (this.targetType === "ingress" && this.ingressName) {
|
||||
@@ -389,6 +467,35 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
value: item.Id,
|
||||
}));
|
||||
}
|
||||
async onGetSecretList() {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
if (!this.clusterId) {
|
||||
throw new Error("请选择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 res = await k8sClient.getSecrets({ namespace: this.namespace || "default" });
|
||||
const list = res.body?.items || res.items || [];
|
||||
return list.map((item: any) => ({
|
||||
label: item.metadata.name,
|
||||
value: item.metadata.name,
|
||||
}));
|
||||
} finally {
|
||||
await this.deleteKubeconfig(vkeService, kubeconfigId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new VolcengineDeployToVKE();
|
||||
new VolcengineDeployToVKE();
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VolcengineAccess } from "./access.js";
|
||||
import { VolcengineAccess } from "./access.js";
|
||||
import { HttpClient, ILogger } from "@certd/basic";
|
||||
|
||||
export type VolcengineOpts = {
|
||||
|
||||
Reference in New Issue
Block a user