diff --git a/.changeset/eighty-jokes-deny.md b/.changeset/eighty-jokes-deny.md new file mode 100644 index 0000000000..58944252f4 --- /dev/null +++ b/.changeset/eighty-jokes-deny.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Add support for status filtering in scaffolder tasks endpoint diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index a3beee2b53..2b0661ab59 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -427,7 +427,7 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: { createdBy?: string }): Promise<{ + list(options: { createdBy?: string; status?: TaskStatus_2 }): Promise<{ tasks: SerializedTask_2[]; }>; // (undocumented) @@ -646,7 +646,7 @@ export interface TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list?(options: { createdBy?: string }): Promise<{ + list?(options: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts index f4fae0715b..78a362b5c7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts @@ -95,6 +95,33 @@ describe('DatabaseTaskStore', () => { expect(tasks[0].id).toBeDefined(); }); + it('should list filtered created tasks by status', async () => { + const { store } = await createStore(); + + const { taskId } = await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'me', + }); + + await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'him', + }); + + const message = `This task was marked as stale as it exceeded its timeout`; + await store.completeTask({ + taskId, + status: 'cancelled', + eventBody: { message }, + }); + + const { tasks } = await store.list({ status: 'open' }); + expect(tasks.length).toBe(1); + expect(tasks[0].createdBy).toBe('him'); + expect(tasks[0].status).toBe('open'); + expect(tasks[0].id).toBeDefined(); + }); + it('should sent an event to start cancelling the task', async () => { const { store } = await createStore(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index fe62572af9..ba675b2cc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -22,19 +22,19 @@ import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { TaskStore, - TaskStoreEmitOptions, - TaskStoreListEventsOptions, TaskStoreCreateTaskOptions, TaskStoreCreateTaskResult, - TaskStoreShutDownTaskOptions, + TaskStoreEmitOptions, + TaskStoreListEventsOptions, TaskStoreRecoverTaskOptions, + TaskStoreShutDownTaskOptions, } from './types'; import { - SerializedTaskEvent, SerializedTask, - TaskStatus, + SerializedTaskEvent, TaskEventType, TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { DateTime, Duration } from 'luxon'; import { TaskRecovery, TaskSpec } from '@backstage/plugin-scaffolder-common'; @@ -182,6 +182,7 @@ export class DatabaseTaskStore implements TaskStore { async list(options: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { const queryBuilder = this.db('tasks'); @@ -191,6 +192,10 @@ export class DatabaseTaskStore implements TaskStore { }); } + if (options.status) { + queryBuilder.where({ status: options.status }); + } + const results = await queryBuilder.orderBy('created_at', 'desc').select(); const tasks = results.map(result => ({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index f4946e6652..d17fb33b3b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -20,13 +20,14 @@ import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; import { - TaskSecrets, SerializedTask, SerializedTaskEvent, TaskBroker, TaskBrokerDispatchOptions, TaskCompletionState, TaskContext, + TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { InternalTaskSecrets, TaskStore } from './types'; import { readDuration } from './helper'; @@ -279,13 +280,17 @@ export class StorageTaskBroker implements TaskBroker { async list(options?: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { if (!this.storage.list) { throw new Error( 'TaskStore does not implement the list method. Please implement the list method to be able to list tasks', ); } - return await this.storage.list({ createdBy: options?.createdBy }); + return await this.storage.list({ + createdBy: options?.createdBy, + status: options?.status, + }); } private deferredDispatch = defer(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 0e0ebd706b..89bcce3cc9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { JsonValue, JsonObject, HumanDuration } from '@backstage/types'; +import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { - TemplateAction, - TaskStatus as _TaskStatus, - TaskCompletionState as _TaskCompletionState, SerializedTask as _SerializedTask, - TaskEventType as _TaskEventType, SerializedTaskEvent as _SerializedTaskEvent, - TaskBrokerDispatchResult as _TaskBrokerDispatchResult, - TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, - TaskContext as _TaskContext, TaskBroker as _TaskBroker, + TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, + TaskBrokerDispatchResult as _TaskBrokerDispatchResult, + TaskCompletionState as _TaskCompletionState, + TaskContext as _TaskContext, + TaskEventType as _TaskEventType, + TaskSecrets, + TaskStatus as _TaskStatus, + TemplateAction, } from '@backstage/plugin-scaffolder-node'; /** @@ -190,7 +190,10 @@ export interface TaskStore { tasks: { taskId: string }[]; }>; - list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; emitLogEvent(options: TaskStoreEmitOptions): Promise; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index af06fc4034..01a2fba6e8 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -432,10 +432,11 @@ describe('createRouter', () => { }); const response = await request(app).get( - `/v2/tasks?createdBy=user:default/foo`, + `/v2/tasks?createdBy=user:default/foo&status=completed`, ); expect(taskBroker.list).toHaveBeenCalledWith({ createdBy: 'user:default/foo', + status: 'completed', }); expect(response.status).toEqual(200); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 769a042a78..a25731ce45 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,10 +15,10 @@ */ import { + createLegacyAuthAdapters, HostDiscovery, PluginDatabaseManager, UrlReader, - createLegacyAuthAdapters, } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; @@ -35,22 +35,22 @@ import { ScmIntegrations } from '@backstage/integration'; import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, + TemplateEntityStepV1beta3, TemplateEntityV1beta3, templateEntityV1beta3Validator, TemplateParametersV1beta3, - TemplateEntityStepV1beta3, } from '@backstage/plugin-scaffolder-common'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, scaffolderActionPermissions, + scaffolderTaskPermissions, scaffolderTemplatePermissions, taskCancelPermission, taskCreatePermission, taskReadPermission, templateParameterReadPermission, templateStepReadPermission, - scaffolderTaskPermissions, } from '@backstage/plugin-scaffolder-common/alpha'; import express from 'express'; import Router from 'express-promise-router'; @@ -58,8 +58,9 @@ import { validate } from 'jsonschema'; import { Logger } from 'winston'; import { z } from 'zod'; import { - TemplateAction, TaskBroker, + TaskStatus, + TemplateAction, TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; @@ -587,8 +588,17 @@ export async function createRouter( ); } + const [statusQuery] = [req.query.status].flat(); + if ( + typeof statusQuery !== 'string' && + typeof statusQuery !== 'undefined' + ) { + throw new InputError('status query parameter must be a string'); + } + const tasks = await taskBroker.list({ createdBy: userEntityRef, + status: statusQuery ? (statusQuery as TaskStatus) : undefined, }); res.status(200).json(tasks); diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 66e8ccd678..515a169cb2 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -315,7 +315,7 @@ export interface TaskBroker { // (undocumented) get(taskId: string): Promise; // (undocumented) - list?(options?: { createdBy?: string }): Promise<{ + list?(options?: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index a2adb97b9c..8324e352f2 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -180,5 +180,8 @@ export interface TaskBroker { get(taskId: string): Promise; - list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options?: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; }