From 7d653f58b1d1ec8236f4d8d3219267b6bc599f66 Mon Sep 17 00:00:00 2001 From: Bogdan Nechyporenko Date: Mon, 16 Oct 2023 13:38:10 +0200 Subject: [PATCH] Preserve time execution for a non-running task. Signed-off-by: Bogdan Nechyporenko --- .changeset/hungry-coats-judge.md | 5 ++ plugins/scaffolder-backend/api-report.md | 4 +- plugins/scaffolder-node/api-report.md | 4 +- plugins/scaffolder/package.json | 1 + plugins/scaffolder/src/api.test.ts | 97 ++++++++++++++++++++++++ plugins/scaffolder/src/api.ts | 61 +++++++++++++-- yarn.lock | 1 + 7 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 .changeset/hungry-coats-judge.md diff --git a/.changeset/hungry-coats-judge.md b/.changeset/hungry-coats-judge.md new file mode 100644 index 0000000000..7e5b634237 --- /dev/null +++ b/.changeset/hungry-coats-judge.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Preserve time execution for a non-running task diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index ba7576b1bb..1b0930eaee 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -741,8 +741,8 @@ export function createRouter(options: RouterOptions): Promise; export const createTemplateAction: < TInputParams extends JsonObject = JsonObject, TOutputParams extends JsonObject = JsonObject, - TInputSchema extends ZodType | Schema = {}, - TOutputSchema extends ZodType | Schema = {}, + TInputSchema extends Schema | ZodType = {}, + TOutputSchema extends Schema | ZodType = {}, TActionInput extends JsonObject = TInputSchema extends ZodType< any, any, diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 52ce3c8d13..0c97fb2e40 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -53,8 +53,8 @@ export type ActionContext< export const createTemplateAction: < TInputParams extends JsonObject = JsonObject, TOutputParams extends JsonObject = JsonObject, - TInputSchema extends z.ZodType | Schema = {}, - TOutputSchema extends z.ZodType | Schema = {}, + TInputSchema extends Schema | z.ZodType = {}, + TOutputSchema extends Schema | z.ZodType = {}, TActionInput extends JsonObject = TInputSchema extends z.ZodType< any, any, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 68066a5a79..7766669411 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -58,6 +58,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/types": "workspace:^", diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index b9b1881066..e10363b61f 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -21,6 +21,11 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ScaffolderClient } from './api'; import { EventSourcePolyfill } from 'event-source-polyfill'; +import { + SerializedTask, + SerializedTaskEvent, +} from '@backstage/plugin-scaffolder-node'; +import { ScaffolderStep } from '@backstage/plugin-scaffolder-react'; const MockedEventSource = EventSourcePolyfill as jest.MockedClass< typeof EventSourcePolyfill @@ -327,6 +332,98 @@ describe('api', () => { }); }); + describe('getTask', () => { + it('should return the task enriched with timestamps', async () => { + const taskId = 'e4e4cb25-e743-4b79-8572-87e9d35aa998'; + server.use( + rest.get(`${mockBaseUrl}/v2/tasks/:taskId`, (_req, res, ctx) => { + return res( + ctx.json({ + createdAt: '2023-10-16T08:21:32.038Z', + id: taskId, + lastHeartbeatAt: '2023-10-16T08:21:54.304Z', + spec: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + action: 'fetch:template', + id: 'fetch', + }, + { + action: 'debug:wait', + id: 'mock-step-1', + }, + { + action: 'debug:wait', + id: 'mock-step-2', + }, + ], + templateInfo: { + entityRef: 'template:default/docs-long-running-template', + }, + }, + } as SerializedTask), + ); + }), + rest.get(`${mockBaseUrl}/v2/tasks/:taskId/events`, (_req, res, ctx) => { + return res( + ctx.json([ + { + createdAt: '2023-10-16T08:21:32.062Z', + body: {}, + id: 109, + taskId, + type: 'log', + }, + { + body: { stepId: 'fetch' }, + createdAt: '2023-10-16T08:21:32.062Z', + id: 110, + taskId, + type: 'log', + }, + { + body: { stepId: 'fetch' }, + createdAt: '2023-10-16T08:21:33.062Z', + id: 111, + taskId, + type: 'log', + }, + { + body: { stepId: 'fetch' }, + createdAt: '2023-10-16T08:21:34.062Z', + id: 112, + taskId, + type: 'log', + }, + { + body: { stepId: 'mock-step-1' }, + createdAt: '2023-10-16T08:21:37.062Z', + id: 120, + taskId, + type: 'log', + }, + ] as SerializedTaskEvent[]), + ); + }), + ); + + const task = await apiClient.getTask(taskId); + + const getStep = (stepId: string) => + task.spec.steps.find( + step => step.id === stepId, + ) as unknown as ScaffolderStep; + + expect(getStep('fetch').startedAt).toBe('2023-10-16T08:21:32.062Z'); + expect(getStep('fetch').endedAt).toBe('2023-10-16T08:21:34.062Z'); + expect(getStep('mock-step-1').startedAt).toBe('2023-10-16T08:21:37.062Z'); + expect(getStep('mock-step-1').endedAt).toBe('2023-10-16T08:21:37.062Z'); + expect(getStep('mock-step-2').startedAt).toBeUndefined(); + expect(getStep('mock-step-2').endedAt).toBeUndefined(); + }); + }); + describe('listTasks', () => { it('should list all tasks', async () => { server.use( diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index c67a5c944e..6aa7fac780 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -38,10 +38,13 @@ import { ScaffolderDryRunOptions, ScaffolderDryRunResponse, TemplateParameterSchema, + ScaffolderStep, } from '@backstage/plugin-scaffolder-react'; +import { TaskStep } from '@backstage/plugin-scaffolder-common'; import queryString from 'qs'; import { EventSourcePolyfill } from 'event-source-polyfill'; +import { SerializedTaskEvent } from '@backstage/plugin-scaffolder-node'; /** * An API to interact with the scaffolder backend. @@ -170,14 +173,62 @@ export class ScaffolderClient implements ScaffolderApi { async getTask(taskId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); - const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; + const taskUrl = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; - const response = await this.fetchApi.fetch(url); - if (!response.ok) { - throw await ResponseError.fromResponse(response); + const taskEventsUrl = `${baseUrl}/v2/tasks/${encodeURIComponent( + taskId, + )}/events`; + + const taskResponse = await this.fetchApi.fetch(taskUrl); + if (!taskResponse.ok) { + throw await ResponseError.fromResponse(taskResponse); } - return await response.json(); + const taskEventsResponse = await this.fetchApi.fetch(taskEventsUrl); + if (!taskEventsResponse.ok) { + throw await ResponseError.fromResponse(taskEventsResponse); + } + + const task = (await taskResponse.json()) as ScaffolderTask; + const taskEvents = + (await taskEventsResponse.json()) as SerializedTaskEvent[]; + + const stepIdToTimestamps = taskEvents + .filter(event => event.type === 'log') + .reduce((acc, event) => { + const stepId = event.body.stepId as string; + if (stepId) { + acc.set(stepId, [...(acc.get(stepId) ?? []), event.createdAt]); + } + return acc; + }, new Map()); + + const toStartedAt = (stepId: string) => { + const timestamps = stepIdToTimestamps.get(stepId); + return timestamps ? timestamps[0] : undefined; + }; + + const toEndedAt = (stepId: string) => { + const timestamps = stepIdToTimestamps.get(stepId); + return timestamps ? timestamps[timestamps.length - 1] : undefined; + }; + + const enrichedTask = { + ...task, + spec: { + ...task.spec, + steps: task.spec.steps.map( + step => + ({ + ...step, + startedAt: toStartedAt(step.id), + endedAt: toEndedAt(step.id), + } as TaskStep & ScaffolderStep), + ), + }, + }; + + return enrichedTask as ScaffolderTask; } streamLogs(options: ScaffolderStreamLogsOptions): Observable { diff --git a/yarn.lock b/yarn.lock index 2533a51f32..e7cd435d9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8975,6 +8975,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^"