perf(证书流水线): 添加批量更新证书申请参数功能

实现批量更新证书申请参数功能,包括前端界面和后端处理逻辑
- 添加批量修改证书申请参数的按钮和对话框
- 实现后端批量更新证书申请参数的接口和服务
- 添加相关测试用例验证功能正确性
This commit is contained in:
xiaojunnuo
2026-05-07 22:54:29 +08:00
parent b75c625ddc
commit 63be1c1cbd
7 changed files with 375 additions and 0 deletions
@@ -349,6 +349,14 @@ export class PipelineController extends CrudController<PipelineService> {
return this.ok({});
}
@Post('/batchUpdateCertApplyOptions', { description: Constants.per.authOnly, summary: "批量更新证书申请任务配置" })
async batchUpdateCertApplyOptions(@Body('ids') ids: number[], @Body('options') options: any) {
await this.checkPermissionCall(async ({userId,projectId})=>{
await this.service.batchUpdateCertApplyOptions(ids, options, userId,projectId);
})
return this.ok({});
}
@Post('/batchRerun', { description: Constants.per.authOnly, summary: "批量重新运行流水线" })
async batchRerun(@Body('ids') ids: number[], @Body('force') force: boolean) {
await this.checkPermissionCall(async ({userId,projectId})=>{
@@ -0,0 +1,166 @@
import assert from "node:assert/strict";
import { updateCertApplyStepInputs } from "./pipeline-batch-update.js";
describe("pipeline batch update", () => {
it("updates only cert apply step inputs", () => {
const pipeline: any = {
stages: [
{
tasks: [
{
steps: [
{
type: "CertApply",
input: {
renewDays: 20,
privateKeyType: "rsa_2048",
domains: ["example.com"],
},
},
{
type: "DeployToHost",
input: {
renewDays: 1,
privateKeyType: "rsa_1024",
},
},
],
},
],
},
],
};
const count = updateCertApplyStepInputs(pipeline, {
renewDays: 10,
privateKeyType: "ec_256",
});
assert.equal(count, 1);
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
renewDays: 10,
privateKeyType: "ec_256",
domains: ["example.com"],
});
assert.deepEqual(pipeline.stages[0].tasks[0].steps[1].input, {
renewDays: 1,
privateKeyType: "rsa_1024",
});
});
it("does not overwrite fields omitted from the patch", () => {
const pipeline: any = {
stages: [
{
tasks: [
{
steps: [
{
type: "CertApply",
input: {
renewDays: 20,
privateKeyType: "ec_256",
},
},
],
},
],
},
],
};
updateCertApplyStepInputs(pipeline, {
renewDays: 15,
});
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
renewDays: 15,
privateKeyType: "ec_256",
});
});
it("updates uploaded cert pipelines only for fields defined by the plugin", () => {
const pipeline: any = {
stages: [
{
tasks: [
{
steps: [
{
type: "CertApplyUpload",
input: {
renewDays: 20,
},
},
{
type: "CertApply",
input: {
renewDays: 20,
},
},
],
},
],
},
],
};
const inputDefines: Record<string, Record<string, unknown>> = {
CertApplyUpload: {
renewDays: {},
},
CertApply: {
renewDays: {},
privateKeyType: {},
},
};
assert.equal(updateCertApplyStepInputs(pipeline, {}, stepType => inputDefines[stepType]), 0);
assert.equal(
updateCertApplyStepInputs(
pipeline,
{
renewDays: 12,
privateKeyType: "ec_256",
},
stepType => inputDefines[stepType]
),
2
);
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
renewDays: 12,
});
assert.deepEqual(pipeline.stages[0].tasks[0].steps[1].input, {
renewDays: 12,
privateKeyType: "ec_256",
});
});
it("skips lego cert apply steps", () => {
const pipeline: any = {
stages: [
{
tasks: [
{
steps: [
{
type: "CertApplyLego",
input: {
renewDays: 20,
privateKeyType: "ec256",
},
},
],
},
],
},
],
};
assert.equal(updateCertApplyStepInputs(pipeline, { renewDays: 12, privateKeyType: "ec_256" }), 0);
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
renewDays: 20,
privateKeyType: "ec256",
});
});
});
@@ -0,0 +1,53 @@
export type CertApplyStepInputPatch = {
renewDays?: number;
privateKeyType?: string;
};
export type GetStepInputDefine = (stepType: string) => Record<string, unknown> | undefined;
function isCertApplyStep(step: any) {
return typeof step?.type === "string" && step.type !== "CertApplyLego" && step.type.startsWith("CertApply");
}
function hasPatchValue(patch: CertApplyStepInputPatch, key: keyof CertApplyStepInputPatch) {
return Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== undefined;
}
function hasInputDefine(inputDefine: Record<string, unknown> | undefined, key: keyof CertApplyStepInputPatch) {
return inputDefine == null || Object.prototype.hasOwnProperty.call(inputDefine, key);
}
function applyPatchFields(target: Record<string, unknown>, patch: CertApplyStepInputPatch, inputDefine: Record<string, unknown> | undefined, fields: (keyof CertApplyStepInputPatch)[]) {
let changed = false;
for (const field of fields) {
if (!hasPatchValue(patch, field) || !hasInputDefine(inputDefine, field)) {
continue;
}
target[field] = patch[field];
changed = true;
}
return changed;
}
export function updateCertApplyStepInputs(pipeline: any, patch: CertApplyStepInputPatch, getStepInputDefine?: GetStepInputDefine) {
const fields: (keyof CertApplyStepInputPatch)[] = ["renewDays", "privateKeyType"];
let count = 0;
for (const stage of pipeline?.stages || []) {
for (const task of stage?.tasks || []) {
for (const step of task?.steps || []) {
if (!isCertApplyStep(step)) {
continue;
}
const inputDefine = getStepInputDefine?.(step.type);
if (step.input == null) {
step.input = {};
}
if (!applyPatchFields(step.input, patch, inputDefine, fields)) {
continue;
}
count++;
}
}
}
return count;
}
@@ -52,6 +52,7 @@ import { set } from "lodash-es";
import { executorQueue } from "@certd/lib-server";
import parser from "cron-parser";
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js";
const runningTasks: Map<string | number, Executor> = new Map();
@@ -1211,6 +1212,37 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
async batchUpdateCertApplyOptions(ids: number[], options: CertApplyStepInputPatch, userId: any, projectId?: number) {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级Certd专业版");
}
const query: any = {}
if (userId && userId > 0) {
query.userId = userId;
}
if (projectId) {
query.projectId = projectId;
}
const list = await this.find({
where: {
id: In(ids),
...query
}
});
for (const item of list) {
const pipeline = JSON.parse(item.content);
const updatedCount = updateCertApplyStepInputs(pipeline, options, stepType => {
const pluginDefine: any = pluginRegistry.getDefine(stepType);
return pluginDefine?.input;
});
if (updatedCount === 0) {
continue;
}
await this.doUpdatePipelineJson(item, pipeline);
}
}
async batchRerun(ids: number[], force: boolean, userId: any, projectId?: number) {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级Certd专业版");