Skip to content

Commit

Permalink
Merge pull request backstage#26730 from drodil/scaffolder_events_service
Browse files Browse the repository at this point in the history
feat(scaffolder): add support for EventsService
  • Loading branch information
benjdlambert authored Nov 26, 2024
2 parents a5c4d7c + c05a343 commit 79c864b
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-moles-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': minor
---

Emit scaffolder events using the optional `EventsService`
1 change: 1 addition & 0 deletions plugins/scaffolder-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@backstage/plugin-bitbucket-cloud-common": "workspace:^",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-azure": "workspace:^",
Expand Down
4 changes: 4 additions & 0 deletions plugins/scaffolder-backend/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Config } from '@backstage/config';
import { DatabaseService } from '@backstage/backend-plugin-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { Duration } from 'luxon';
import { EventsService } from '@backstage/plugin-events-node';
import { executeShellCommand as executeShellCommand_2 } from '@backstage/plugin-scaffolder-node';
import { ExecuteShellCommandOptions } from '@backstage/plugin-scaffolder-node';
import express from 'express';
Expand Down Expand Up @@ -520,6 +521,7 @@ export class DatabaseTaskStore implements TaskStore {
// @public
export type DatabaseTaskStoreOptions = {
database: PluginDatabaseManager | Knex;
events?: EventsService;
};

// @public @deprecated
Expand Down Expand Up @@ -552,6 +554,8 @@ export interface RouterOptions {
// (undocumented)
discovery?: DiscoveryService;
// (undocumented)
events?: EventsService;
// (undocumented)
httpAuth?: HttpAuthService;
// (undocumented)
identity?: IdentityApi;
Expand Down
6 changes: 5 additions & 1 deletion plugins/scaffolder-backend/src/ScaffolderPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
*/

import {
createBackendPlugin,
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { loggerToWinstonLogger } from '@backstage/backend-common';
import { ScmIntegrations } from '@backstage/integration';
Expand Down Expand Up @@ -51,6 +51,7 @@ import {
createWaitAction,
} from './scaffolder';
import { createRouter } from './service/router';
import { eventsServiceRef } from '@backstage/plugin-events-node';

/**
* Scaffolder plugin
Expand Down Expand Up @@ -115,6 +116,7 @@ export const scaffolderPlugin = createBackendPlugin({
httpRouter: coreServices.httpRouter,
httpAuth: coreServices.httpAuth,
catalogClient: catalogServiceRef,
events: eventsServiceRef,
},
async init({
logger,
Expand All @@ -128,6 +130,7 @@ export const scaffolderPlugin = createBackendPlugin({
httpAuth,
catalogClient,
permissions,
events,
}) {
const log = loggerToWinstonLogger(logger);
const integrations = ScmIntegrations.fromConfig(config);
Expand Down Expand Up @@ -191,6 +194,7 @@ export const scaffolderPlugin = createBackendPlugin({
permissions,
autocompleteHandlers,
additionalWorkspaceProviders,
events,
});
httpRouter.use(router);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { ConflictError } from '@backstage/errors';
import { createMockDirectory } from '@backstage/backend-test-utils';
import fs from 'fs-extra';
import { EventsService } from '@backstage/plugin-events-node';

const createStore = async () => {
const createStore = async (events?: EventsService) => {
const manager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
Expand All @@ -35,6 +36,7 @@ const createStore = async () => {
).forPlugin('scaffolder');
const store = await DatabaseTaskStore.create({
database: manager,
events,
});
return { store, manager };
};
Expand All @@ -52,6 +54,14 @@ const workspaceDir = createMockDirectory({
});

describe('DatabaseTaskStore', () => {
const eventsService = {
publish: jest.fn(),
} as unknown as EventsService;

beforeEach(() => {
jest.resetAllMocks();
});

it('should create the database store and run migration', async () => {
const { store, manager } = await createStore();
expect(store).toBeDefined();
Expand Down Expand Up @@ -222,7 +232,7 @@ describe('DatabaseTaskStore', () => {
});

it('should sent an event to start cancelling the task', async () => {
const { store } = await createStore();
const { store } = await createStore(eventsService);

const { taskId } = await store.createTask({
spec: {} as TaskSpec,
Expand All @@ -244,10 +254,24 @@ describe('DatabaseTaskStore', () => {
const event = events[0];
expect(event.taskId).toBe(taskId);
expect(event.body.status).toBe('cancelled');

expect(eventsService.publish).toHaveBeenCalledWith({
topic: 'scaffolder.task',
eventPayload: {
id: 1,
taskId,
status: 'cancelled',
body: {
message: `Step 2 has been cancelled.`,
stepId: 2,
status: 'cancelled',
},
},
});
});

it('should emit a log event', async () => {
const { store } = await createStore();
const { store } = await createStore(eventsService);
const { taskId } = await store.createTask({
spec: {} as TaskSpec,
createdBy: 'me',
Expand Down Expand Up @@ -310,7 +334,7 @@ describe('DatabaseTaskStore', () => {
});

it('should complete the task', async () => {
const { store } = await createStore();
const { store } = await createStore(eventsService);
const { taskId } = await store.createTask({
spec: {} as TaskSpec,
createdBy: 'me',
Expand All @@ -327,10 +351,21 @@ describe('DatabaseTaskStore', () => {

const taskAfterCompletion = await store.getTask(taskId);
expect(taskAfterCompletion.status).toBe('cancelled');

expect(eventsService.publish).toHaveBeenCalledWith({
topic: 'scaffolder.task',
eventPayload: {
id: taskId,
status: 'cancelled',
createdAt: expect.any(String),
lastHeartbeatAt: null,
createdBy: 'me',
},
});
});

it('should claim a new task', async () => {
const { store } = await createStore();
const { store } = await createStore(eventsService);
const { taskId } = await store.createTask({
spec: {} as TaskSpec,
createdBy: 'me',
Expand All @@ -341,10 +376,22 @@ describe('DatabaseTaskStore', () => {

const claimedTask = await store.getTask(taskId);
expect(claimedTask.status).toBe('processing');

expect(eventsService.publish).toHaveBeenCalledWith({
topic: 'scaffolder.task',
eventPayload: {
id: taskId,
status: 'processing',
createdAt: expect.any(String),
lastHeartbeatAt: null,
createdBy: 'me',
spec: {},
},
});
});

it('should restore the state of the task after the task recovery', async () => {
const { store } = await createStore();
const { store } = await createStore(eventsService);
const { taskId } = await store.createTask({
spec: {} as TaskSpec,
createdBy: 'me',
Expand Down Expand Up @@ -379,10 +426,22 @@ describe('DatabaseTaskStore', () => {

const claimedTask = await store.getTask(taskId);
expect(claimedTask.state).toEqual({ state: state.state });

expect(eventsService.publish).toHaveBeenCalledWith({
topic: 'scaffolder.task',
eventPayload: {
id: 1,
taskId,
body: {
recoverStrategy: 'none',
},
status: 'recovered',
},
});
});

it('should shutdown the running task', async () => {
const { store } = await createStore();
const { store } = await createStore(eventsService);
const { taskId } = await store.createTask({
spec: {} as TaskSpec,
createdBy: 'me',
Expand All @@ -394,6 +453,17 @@ describe('DatabaseTaskStore', () => {

const claimedTask = await store.getTask(taskId);
expect(claimedTask.status).toBe('failed');

expect(eventsService.publish).toHaveBeenCalledWith({
topic: 'scaffolder.task',
eventPayload: {
id: taskId,
status: 'failed',
createdAt: expect.any(String),
lastHeartbeatAt: expect.any(String),
createdBy: 'me',
},
});
});

it('should be not possible to shutdown not running task', async () => {
Expand Down
Loading

0 comments on commit 79c864b

Please sign in to comment.