perf: 支持已跳过的步骤重新运行

pull/148/head
xiaojunnuo 2024-09-02 18:36:12 +08:00
parent 724a85028b
commit ea775adae1
20 changed files with 105 additions and 87 deletions

View File

@ -36,6 +36,8 @@ export class Executor {
options: ExecutorOptions; options: ExecutorOptions;
abort: AbortController = new AbortController(); abort: AbortController = new AbortController();
_inited = false;
onChanged: (history: RunHistory) => Promise<void>; onChanged: (history: RunHistory) => Promise<void>;
constructor(options: ExecutorOptions) { constructor(options: ExecutorOptions) {
this.options = options; this.options = options;
@ -50,6 +52,10 @@ export class Executor {
} }
async init() { async init() {
if (this._inited) {
return;
}
this._inited = true;
const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`); const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`);
this.lastRuntime = lastRuntime; this.lastRuntime = lastRuntime;
this.lastStatusMap = new RunnableCollection(lastRuntime?.pipeline); this.lastStatusMap = new RunnableCollection(lastRuntime?.pipeline);
@ -315,4 +321,8 @@ export class Executor {
} }
} }
} }
clearLastStatus(stepId: string) {
this.lastStatusMap.clearById(stepId);
}
} }

View File

@ -166,6 +166,14 @@ export class RunnableCollection {
}); });
} }
clearById(id: string) {
const runnable = this.collection[id];
if (runnable?.status) {
runnable.status.status = ResultType.none;
runnable.status.result = ResultType.none;
}
}
add(runnable: Runnable) { add(runnable: Runnable) {
this.collection[runnable.id] = runnable; this.collection[runnable.id] = runnable;
} }

View File

@ -45,6 +45,10 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important; vertical-align: 0 !important;
} }
.pointer{
cursor: pointer;
}
.flex-center{ .flex-center{
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -122,3 +126,8 @@ h1, h2, h3, h4, h5, h6 {
padding-bottom:3px; padding-bottom:3px;
border-bottom: 1px solid #dedede; border-bottom: 1px solid #dedede;
} }
.color-blue{
color: #1890ff;
}

View File

@ -59,11 +59,11 @@ export function Save(pipelineEntity: any) {
}); });
} }
export function Trigger(id: any) { export function Trigger(id: any, stepId?: string) {
return request({ return request({
url: apiPrefix + "/trigger", url: apiPrefix + "/trigger",
method: "post", method: "post",
params: { id } params: { id, stepId }
}); });
} }

View File

@ -2,7 +2,7 @@ import * as api from "./api";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status"; import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { message, Modal } from "ant-design-vue"; import { message, Modal } from "ant-design-vue";
@ -29,9 +29,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}; };
const addRequest = async ({ form }: AddReq) => { const addRequest = async ({ form }: AddReq) => {
form.content = JSON.stringify({ if (form.content == null) {
title: form.title form.content = JSON.stringify({
}); title: form.title
});
} else {
const content = JSON.parse(form.content);
content.title = form.title;
form.content = JSON.stringify(content);
}
const res = await api.AddObj(form); const res = await api.AddObj(form);
lastResRef.value = res; lastResRef.value = res;
return res; return res;
@ -136,6 +143,18 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } }); router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } });
} }
}, },
copy: {
click: async (context) => {
const { ui } = useUi();
// @ts-ignore
const row = context[ui.tableColumn.row];
row.title = row.title + "_copy";
await crudExpose.openCopy({
row: row,
index: context.index
});
}
},
config: { config: {
order: 1, order: 1,
title: null, title: null,

View File

@ -54,9 +54,9 @@ export default defineComponent({
content: JSON.stringify(pipelineConfig) content: JSON.stringify(pipelineConfig)
}); });
}, },
async doTrigger(options: { pipelineId: number }) { async doTrigger(options: { pipelineId: number; stepId?: string }) {
const { pipelineId } = options; const { pipelineId, stepId } = options;
await api.Trigger(pipelineId); await api.Trigger(pipelineId, stepId);
} }
}; };

View File

@ -5,7 +5,7 @@
</template> </template>
<p> <p>
<fs-date-format :model-value="runnable.status?.startTime"></fs-date-format> <fs-date-format :model-value="runnable.status?.startTime"></fs-date-format>
<a-tag class="ml-1" :color="status.color" :closable="status.value === 'start'" @close="cancelTask"> <a-tag class="ml-5" :color="status.color" :closable="status.value === 'start'" @close="cancelTask">
{{ status.label }} {{ status.label }}
</a-tag> </a-tag>
<a-tag v-if="isCurrent" class="pointer" color="green" :closable="true" @close="cancel"></a-tag> <a-tag v-if="isCurrent" class="pointer" color="green" :closable="true" @close="cancel"></a-tag>

View File

@ -1,5 +1,5 @@
<template> <template>
<span v-if="statusRef" class="pi-status-show"> <span v-if="statusRef" class="pi-status-show flex-o">
<template v-if="type === 'icon'"> <template v-if="type === 'icon'">
<fs-icon class="status-icon" v-bind="statusRef" :style="{ color: statusRef.color }" /> <fs-icon class="status-icon" v-bind="statusRef" :style="{ color: statusRef.color }" />
</template> </template>

View File

@ -82,10 +82,20 @@
</div> </div>
<div class="task"> <div class="task">
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)"> <a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
<span class="flex-o w-100"> <a-popover title="步骤">
<span class="ellipsis flex-1" :class="{ 'mr-15': editMode }">{{ task.title }}</span> <!-- :open="true"-->
<pi-status-show :status="task.status?.result"></pi-status-show> <template #content>
</span> <div v-for="(item, index) of task.steps" class="flex-o w-100">
<span class="ellipsis flex-1">{{ index + 1 }}. {{ item.title }} </span>
<pi-status-show :status="item.status?.result"></pi-status-show>
<fs-icon class="pointer color-blue" title="重新运行此步骤" icon="SyncOutlined" @click="run(item.id)"></fs-icon>
</div>
</template>
<span class="flex-o w-100">
<span class="ellipsis flex-1" :class="{ 'mr-15': editMode }">{{ task.title }}</span>
<pi-status-show :status="task.status?.result"></pi-status-show>
</span>
</a-popover>
</a-button> </a-button>
<fs-icon v-if="editMode" class="copy" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon> <fs-icon v-if="editMode" class="copy" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
</div> </div>
@ -226,10 +236,11 @@ import { nanoid } from "nanoid";
import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./type"; import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./type";
import type { Runnable } from "@certd/pipeline"; import type { Runnable } from "@certd/pipeline";
import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue"; import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue";
import { FsIcon } from "@fast-crud/fast-crud";
export default defineComponent({ export default defineComponent({
name: "PipelineEdit", name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components // eslint-disable-next-line vue/no-unused-components
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm }, components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
props: { props: {
pipelineId: { pipelineId: {
type: [Number, String], type: [Number, String],
@ -529,7 +540,7 @@ export default defineComponent({
function useActions() { function useActions() {
const saveLoading = ref(); const saveLoading = ref();
const run = async () => { const run = async (stepId?: string) => {
if (props.editMode) { if (props.editMode) {
message.warn("请先保存,再运行管道"); message.warn("请先保存,再运行管道");
return; return;
@ -549,11 +560,12 @@ export default defineComponent({
//@ts-ignore //@ts-ignore
await changeCurrentHistory(null); await changeCurrentHistory(null);
watchNewHistoryList(); watchNewHistoryList();
await props.options.doTrigger({ pipelineId: pipeline.value.id }); await props.options.doTrigger({ pipelineId: pipeline.value.id, stepId: stepId });
notification.success({ message: "管道已经开始运行" }); notification.success({ message: "管道已经开始运行" });
} }
}); });
}; };
function toggleEditMode(editMode: boolean) { function toggleEditMode(editMode: boolean) {
ctx.emit("update:editMode", editMode); ctx.emit("update:editMode", editMode);
} }

View File

@ -117,7 +117,7 @@ export class PluginGroups {
} }
export type PipelineOptions = { export type PipelineOptions = {
doTrigger(options: { pipelineId: number }): Promise<void>; doTrigger(options: { pipelineId: number; stepId?: string }): Promise<void>;
doSave(pipelineConfig: Pipeline): Promise<void>; doSave(pipelineConfig: Pipeline): Promise<void>;
getPipelineDetail(query: { pipelineId: number }): Promise<PipelineDetail>; getPipelineDetail(query: { pipelineId: number }): Promise<PipelineDetail>;
getHistoryList(query: { pipelineId: number }): Promise<RunHistory[]>; getHistoryList(query: { pipelineId: number }): Promise<RunHistory[]>;

View File

@ -87,9 +87,9 @@ export class PipelineController extends CrudController<PipelineService> {
} }
@Post('/trigger', { summary: Constants.per.authOnly }) @Post('/trigger', { summary: Constants.per.authOnly })
async trigger(@Query('id') id: number) { async trigger(@Query('id') id: number, @Query('stepId') stepId?: string) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), id); await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
await this.service.trigger(id); await this.service.trigger(id, stepId);
return this.ok({}); return this.ok({});
} }

View File

@ -197,14 +197,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
} }
async trigger(id) { async trigger(id: any, stepId?: string) {
this.cron.register({ this.cron.register({
name: `pipeline.${id}.trigger.once`, name: `pipeline.${id}.trigger.once`,
cron: null, cron: null,
job: async () => { job: async () => {
logger.info('用户手动启动job'); logger.info('用户手动启动job');
try { try {
await this.run(id, null); await this.run(id, null, stepId);
} catch (e) { } catch (e) {
logger.error('手动job执行失败', e); logger.error('手动job执行失败', e);
} }
@ -279,7 +279,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
logger.info('当前定时器数量:', this.cron.getTaskSize()); logger.info('当前定时器数量:', this.cron.getTaskSize());
} }
async run(id: number, triggerId: string) { async run(id: number, triggerId: string, stepId?: string) {
const entity: PipelineEntity = await this.info(id); const entity: PipelineEntity = await this.info(id);
const pipeline = JSON.parse(entity.content); const pipeline = JSON.parse(entity.content);
@ -333,6 +333,10 @@ export class PipelineService extends BaseService<PipelineEntity> {
try { try {
runningTasks.set(historyId, executor); runningTasks.set(historyId, executor);
await executor.init(); await executor.init();
if (stepId) {
// 清除该step的状态
executor.clearLastStatus(stepId);
}
await executor.run(historyId, triggerType); await executor.run(historyId, triggerType);
} catch (e) { } catch (e) {
logger.error('执行失败:', e); logger.error('执行失败:', e);

View File

@ -1,30 +0,0 @@
import { IsAccess, AccessInput } from '@certd/pipeline';
@IsAccess({
name: 'aliyun',
title: '阿里云授权',
desc: '',
})
export class AliyunAccess {
@AccessInput({
title: 'accessKeyId',
component: {
placeholder: 'accessKeyId',
},
helper: '登录阿里云控制台->AccessKey管理页面获取。',
required: true,
})
accessKeyId = '';
@AccessInput({
title: 'accessKeySecret',
component: {
placeholder: 'accessKeySecret',
},
required: true,
encrypt: true,
helper: '注意证书申请需要dns解析权限其他阿里云插件需要对应的权限比如证书上传需要证书管理权限嫌麻烦就用主账号的全量权限的accessKey',
})
accessKeySecret = '';
}
new AliyunAccess();

View File

@ -1 +0,0 @@
export * from './aliyun-access.js';

View File

@ -1,12 +1,7 @@
import Core from '@alicloud/pop-core'; import Core from '@alicloud/pop-core';
import { import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
AbstractDnsProvider,
CreateRecordOptions,
IsDnsProvider,
RemoveRecordOptions,
} from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline'; import { Autowire, ILogger } from '@certd/pipeline';
import { AliyunAccess } from '../access/index.js'; import { AliyunAccess } from '@certd/plugin-plus';
@IsDnsProvider({ @IsDnsProvider({
name: 'aliyun', name: 'aliyun',
@ -110,11 +105,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
}; };
try { try {
const ret = await this.client.request( const ret = await this.client.request('AddDomainRecord', params, requestOption);
'AddDomainRecord',
params,
requestOption
);
this.logger.info('添加域名解析成功:', value, value, ret.RecordId); this.logger.info('添加域名解析成功:', value, value, ret.RecordId);
return ret.RecordId; return ret.RecordId;
} catch (e: any) { } catch (e: any) {
@ -136,11 +127,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
method: 'POST', method: 'POST',
}; };
const ret = await this.client.request( const ret = await this.client.request('DeleteDomainRecord', params, requestOption);
'DeleteDomainRecord',
params,
requestOption
);
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId); this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId);
return ret.RecordId; return ret.RecordId;
} }

View File

@ -1,3 +1,2 @@
export * from './access/index.js';
export * from './dns-provider/index.js'; export * from './dns-provider/index.js';
export * from './plugin/index.js'; export * from './plugin/index.js';

View File

@ -1,7 +1,5 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
// @ts-ignore import { AliyunAccess } from '@certd/plugin-plus';
import { ROAClient } from '@alicloud/pop-core';
import { AliyunAccess } from '../../access/index.js';
import { appendTimeSuffix } from '../../utils/index.js'; import { appendTimeSuffix } from '../../utils/index.js';
import { CertInfo } from '@certd/plugin-cert'; import { CertInfo } from '@certd/plugin-cert';
@ -113,7 +111,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
console.log('开始部署证书到阿里云cdn'); console.log('开始部署证书到阿里云cdn');
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this; const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this;
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess; const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = this.getClient(access, regionId); const client = await this.getClient(access, regionId);
const kubeConfigStr = await this.getKubeConfig(client, clusterId, isPrivateIpAddress); const kubeConfigStr = await this.getKubeConfig(client, clusterId, isPrivateIpAddress);
this.logger.info('kubeconfig已成功获取'); this.logger.info('kubeconfig已成功获取');
@ -200,8 +198,10 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
} }
} }
getClient(aliyunProvider: any, regionId: string) { async getClient(aliyunProvider: any, regionId: string) {
return new ROAClient({ const ROAClient = await import('@alicloud/pop-core');
return new ROAClient.default({
accessKeyId: aliyunProvider.accessKeyId, accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret, accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: `https://cs.${regionId}.aliyuncs.com`, endpoint: `https://cs.${regionId}.aliyuncs.com`,

View File

@ -1,9 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Core from '@alicloud/pop-core'; import { AliyunAccess } from '@certd/plugin-plus';
import RPCClient from '@alicloud/pop-core'; import type RPCClient from '@alicloud/pop-core';
import { AliyunAccess } from '../../access/index.js';
@IsTaskPlugin({ @IsTaskPlugin({
name: 'DeployCertToAliyunCDN', name: 'DeployCertToAliyunCDN',
title: '部署证书至阿里云CDN', title: '部署证书至阿里云CDN',
@ -54,14 +52,16 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
async execute(): Promise<void> { async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn'); console.log('开始部署证书到阿里云cdn');
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess; const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = this.getClient(access); const client = await this.getClient(access);
const params = await this.buildParams(); const params = await this.buildParams();
await this.doRequest(client, params); await this.doRequest(client, params);
console.log('部署完成'); console.log('部署完成');
} }
getClient(access: AliyunAccess) { async getClient(access: AliyunAccess) {
return new Core({ const Core = await import('@alicloud/pop-core');
return new Core.default({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
endpoint: 'https://cdn.aliyuncs.com', endpoint: 'https://cdn.aliyuncs.com',

View File

@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import Core from '@alicloud/pop-core'; import Core from '@alicloud/pop-core';
import { AliyunAccess } from '../../access/index.js'; import { AliyunAccess } from '@certd/plugin-plus';
import { appendTimeSuffix, checkRet, ZoneOptions } from '../../utils/index.js'; import { appendTimeSuffix, checkRet, ZoneOptions } from '../../utils/index.js';
@IsTaskPlugin({ @IsTaskPlugin({

View File

@ -29,6 +29,7 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
component: { component: {
name: 'a-textarea', name: 'a-textarea',
vModel: 'value', vModel: 'value',
rows: 6,
}, },
required: true, required: true,
}) })