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;
abort: AbortController = new AbortController();
_inited = false;
onChanged: (history: RunHistory) => Promise<void>;
constructor(options: ExecutorOptions) {
this.options = options;
@ -50,6 +52,10 @@ export class Executor {
}
async init() {
if (this._inited) {
return;
}
this._inited = true;
const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`);
this.lastRuntime = lastRuntime;
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) {
this.collection[runnable.id] = runnable;
}

View File

@ -45,6 +45,10 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important;
}
.pointer{
cursor: pointer;
}
.flex-center{
display: flex;
justify-content: center;
@ -122,3 +126,8 @@ h1, h2, h3, h4, h5, h6 {
padding-bottom:3px;
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({
url: apiPrefix + "/trigger",
method: "post",
params: { id }
params: { id, stepId }
});
}

View File

@ -2,7 +2,7 @@ import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
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 { nanoid } from "nanoid";
import { message, Modal } from "ant-design-vue";
@ -29,9 +29,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
};
const addRequest = async ({ form }: AddReq) => {
form.content = JSON.stringify({
title: form.title
});
if (form.content == null) {
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);
lastResRef.value = 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" } });
}
},
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: {
order: 1,
title: null,

View File

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

View File

@ -5,7 +5,7 @@
</template>
<p>
<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 }}
</a-tag>
<a-tag v-if="isCurrent" class="pointer" color="green" :closable="true" @close="cancel"></a-tag>

View File

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

View File

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

View File

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

View File

@ -87,9 +87,9 @@ export class PipelineController extends CrudController<PipelineService> {
}
@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.service.trigger(id);
await this.service.trigger(id, stepId);
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({
name: `pipeline.${id}.trigger.once`,
cron: null,
job: async () => {
logger.info('用户手动启动job');
try {
await this.run(id, null);
await this.run(id, null, stepId);
} catch (e) {
logger.error('手动job执行失败', e);
}
@ -279,7 +279,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
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 pipeline = JSON.parse(entity.content);
@ -333,6 +333,10 @@ export class PipelineService extends BaseService<PipelineEntity> {
try {
runningTasks.set(historyId, executor);
await executor.init();
if (stepId) {
// 清除该step的状态
executor.clearLastStatus(stepId);
}
await executor.run(historyId, triggerType);
} catch (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 {
AbstractDnsProvider,
CreateRecordOptions,
IsDnsProvider,
RemoveRecordOptions,
} from '@certd/plugin-cert';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { AliyunAccess } from '../access/index.js';
import { AliyunAccess } from '@certd/plugin-plus';
@IsDnsProvider({
name: 'aliyun',
@ -110,11 +105,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
};
try {
const ret = await this.client.request(
'AddDomainRecord',
params,
requestOption
);
const ret = await this.client.request('AddDomainRecord', params, requestOption);
this.logger.info('添加域名解析成功:', value, value, ret.RecordId);
return ret.RecordId;
} catch (e: any) {
@ -136,11 +127,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
method: 'POST',
};
const ret = await this.client.request(
'DeleteDomainRecord',
params,
requestOption
);
const ret = await this.client.request('DeleteDomainRecord', params, requestOption);
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId);
return ret.RecordId;
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
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';
@IsTaskPlugin({

View File

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