mirror of
https://github.com/certd/certd.git
synced 2026-05-17 13:57:31 +08:00
perf(证书流水线): 添加批量更新证书申请参数功能
实现批量更新证书申请参数功能,包括前端界面和后端处理逻辑 - 添加批量修改证书申请参数的按钮和对话框 - 实现后端批量更新证书申请参数的接口和服务 - 添加相关测试用例验证功能正确性
This commit is contained in:
@@ -110,6 +110,14 @@ export async function BatchUpdateNotificaiton(pipelineIds: number[], notificatio
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function BatchUpdateCertApplyOptions(pipelineIds: number[], options: any): Promise<void> {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/batchUpdateCertApplyOptions",
|
||||||
|
method: "post",
|
||||||
|
data: { ids: pipelineIds, options },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function BatchUpdateProject(pipelineIds: number[], toProjectId: number): Promise<void> {
|
export async function BatchUpdateProject(pipelineIds: number[], toProjectId: number): Promise<void> {
|
||||||
return await request({
|
return await request({
|
||||||
url: apiPrefix + "/batchTransfer",
|
url: apiPrefix + "/batchTransfer",
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<fs-button icon="ph:certificate" class="need-plus" type="link" text="修改证书申请参数" @click="openFormDialog"></fs-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useFormWrapper } from "@fast-crud/fast-crud";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
import * as api from "../api";
|
||||||
|
import { useSettingStore } from "/@/store/settings";
|
||||||
|
import { usePluginStore } from "/@/store/plugin";
|
||||||
|
import { useReference } from "/@/use/use-refrence";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedRowKeys: any[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const batchUpdateFields = ["renewDays", "privateKeyType"];
|
||||||
|
|
||||||
|
function hasFormValue(form: any, field: string) {
|
||||||
|
return form[field] != null && form[field] !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBatchUpdateOptions(form: any) {
|
||||||
|
const options: any = {};
|
||||||
|
for (const field of batchUpdateFields) {
|
||||||
|
if (hasFormValue(form, field)) {
|
||||||
|
options[field] = form[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchUpdateRequest(form: any) {
|
||||||
|
const options = buildBatchUpdateOptions(form);
|
||||||
|
await api.BatchUpdateCertApplyOptions(props.selectedRowKeys, options);
|
||||||
|
emit("change");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { openCrudFormDialog } = useFormWrapper();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const pluginStore = usePluginStore();
|
||||||
|
|
||||||
|
function createInputColumn(inputDefine: any) {
|
||||||
|
const form = cloneDeep(inputDefine);
|
||||||
|
useReference(form);
|
||||||
|
delete form.value;
|
||||||
|
delete form.rules;
|
||||||
|
form.required = false;
|
||||||
|
if (form.component) {
|
||||||
|
form.component.allowClear = true;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: inputDefine.title,
|
||||||
|
form,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createColumns(inputDefines: any) {
|
||||||
|
const columns: any = {};
|
||||||
|
for (const field of batchUpdateFields) {
|
||||||
|
columns[field] = createInputColumn(inputDefines[field]);
|
||||||
|
}
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAnyBatchUpdateValue(form: any) {
|
||||||
|
return batchUpdateFields.some(field => hasFormValue(form, field));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openFormDialog() {
|
||||||
|
settingStore.checkPlus();
|
||||||
|
const certApplyPlugin: any = await pluginStore.getPluginDefine("CertApply");
|
||||||
|
const certApplyInput = certApplyPlugin?.input || {};
|
||||||
|
|
||||||
|
const crudOptions: any = {
|
||||||
|
columns: createColumns(certApplyInput),
|
||||||
|
form: {
|
||||||
|
mode: "edit",
|
||||||
|
//@ts-ignore
|
||||||
|
async doSubmit({ form }) {
|
||||||
|
if (!hasAnyBatchUpdateValue(form)) {
|
||||||
|
throw new Error("请至少选择一个要修改的参数");
|
||||||
|
}
|
||||||
|
await batchUpdateRequest(form);
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
span: 22,
|
||||||
|
},
|
||||||
|
labelCol: {
|
||||||
|
style: {
|
||||||
|
width: "120px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
title: "批量修改证书申请参数",
|
||||||
|
width: 620,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
await openCrudFormDialog({ crudOptions });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
<fs-button v-if="hasActionPermission('write')" icon="ion:trash-outline" class="color-red" type="link" :text="t('certd.batchDelete')" @click="batchDelete"></fs-button>
|
<fs-button v-if="hasActionPermission('write')" icon="ion:trash-outline" class="color-red" type="link" :text="t('certd.batchDelete')" @click="batchDelete"></fs-button>
|
||||||
<batch-rerun :selected-row-keys="selectedRowKeys" @change="batchFinished"></batch-rerun>
|
<batch-rerun :selected-row-keys="selectedRowKeys" @change="batchFinished"></batch-rerun>
|
||||||
<change-group v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group>
|
<change-group v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group>
|
||||||
|
<change-cert-apply-options v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-cert-apply-options>
|
||||||
<change-notification v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification>
|
<change-notification v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification>
|
||||||
<change-trigger v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
|
<change-trigger v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
|
||||||
<change-project v-if="hasActionPermission('write') && settingStore.isEnterprise" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-project>
|
<change-project v-if="hasActionPermission('write') && settingStore.isEnterprise" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-project>
|
||||||
@@ -57,6 +58,7 @@ import { computed, onActivated, onMounted, provide, ref } from "vue";
|
|||||||
import { dict, useFs } from "@fast-crud/fast-crud";
|
import { dict, useFs } from "@fast-crud/fast-crud";
|
||||||
import createCrudOptions from "./crud";
|
import createCrudOptions from "./crud";
|
||||||
import ChangeGroup from "./components/change-group.vue";
|
import ChangeGroup from "./components/change-group.vue";
|
||||||
|
import ChangeCertApplyOptions from "./components/change-cert-apply-options.vue";
|
||||||
import ChangeTrigger from "./components/change-trigger.vue";
|
import ChangeTrigger from "./components/change-trigger.vue";
|
||||||
import ChangeProject from "./components/change-project.vue";
|
import ChangeProject from "./components/change-project.vue";
|
||||||
|
|
||||||
|
|||||||
@@ -349,6 +349,14 @@ export class PipelineController extends CrudController<PipelineService> {
|
|||||||
return this.ok({});
|
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: "批量重新运行流水线" })
|
@Post('/batchRerun', { description: Constants.per.authOnly, summary: "批量重新运行流水线" })
|
||||||
async batchRerun(@Body('ids') ids: number[], @Body('force') force: boolean) {
|
async batchRerun(@Body('ids') ids: number[], @Body('force') force: boolean) {
|
||||||
await this.checkPermissionCall(async ({userId,projectId})=>{
|
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 { executorQueue } from "@certd/lib-server";
|
||||||
import parser from "cron-parser";
|
import parser from "cron-parser";
|
||||||
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
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();
|
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) {
|
async batchRerun(ids: number[], force: boolean, userId: any, projectId?: number) {
|
||||||
if (!isPlus()) {
|
if (!isPlus()) {
|
||||||
throw new NeedVIPException("此功能需要升级Certd专业版");
|
throw new NeedVIPException("此功能需要升级Certd专业版");
|
||||||
|
|||||||
Reference in New Issue
Block a user