diff --git a/plugins/aap-backend/README.md b/plugins/aap-backend/README.md index 204cf078fd..d7c4f5d574 100644 --- a/plugins/aap-backend/README.md +++ b/plugins/aap-backend/README.md @@ -8,108 +8,7 @@ The Ansible Automation Platform (AAP) Backstage provider plugin synchronizes the The AAP Backstage provider plugin allows the configuration of one or multiple providers using the `app-config.yaml` configuration file of Backstage. -#### Legacy Backend Procedure - -1. Run the following command to install the AAP Backstage provider plugin: - - ```console - yarn workspace backend add @janus-idp/backstage-plugin-aap-backend - ``` - -1. Use `aap` marker to configure the `app-config.yaml` file of Backstage as follows: - - ```yaml title="app-config.yaml" - catalog: - providers: - aap: - dev: - baseUrl: - authorization: 'Bearer ${AAP_AUTH_TOKEN}' - owner: - system: - schedule: # optional; same options as in TaskScheduleDefinition - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 1 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 1 } - ``` - -1. Configure the scheduler using one of the following options: - - - **Method 1**: If the scheduler is configured inside the `app-config.yaml` using the schedule config key mentioned previously, add the following code to `packages/backend/src/plugins/catalog.ts` file: - - ```ts title="packages/backend/src/plugins/catalog.ts" - /* highlight-add-next-line */ - import { AapResourceEntityProvider } from '@janus-idp/backstage-plugin-aap-backend'; - - export default async function createPlugin( - env: PluginEnvironment, - ): Promise { - const builder = await CatalogBuilder.create(env); - - /* ... other processors and/or providers ... */ - /* highlight-add-start */ - builder.addEntityProvider( - AapResourceEntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), - ); - /* highlight-add-end */ - - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; - } - ``` - - *** - - **NOTE** - - If you have made any changes to the schedule in the `app-config.yaml` file, then restart to apply the changes. - - *** - - - **Method 2**: Add a schedule directly inside the `packages/backend/src/plugins/catalog.ts` file as follows: - - ```ts title="packages/backend/src/plugins/catalog.ts" - /* highlight-add-next-line */ - import { AapResourceEntityProvider } from '@janus-idp/backstage-plugin-aap-backend'; - - export default async function createPlugin( - env: PluginEnvironment, - ): Promise { - const builder = await CatalogBuilder.create(env); - - /* ... other processors and/or providers ... */ - /* highlight-add-start */ - builder.addEntityProvider( - AapResourceEntityProvider.fromConfig(env.config, { - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - }), - ); - /* highlight-add-end */ - - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; - } - ``` - - *** - - **NOTE** - - If both the `schedule` (hard-coded schedule) and `scheduler` (`app-config.yaml` schedule) option are provided in the `packages/backend/src/plugins/catalog.ts`, the `scheduler` option takes precedence. However, if the schedule inside the `app-config.yaml` file is not configured, then the `schedule` option is used. - - *** - -#### New Backend Procedure +#### Backend Procedure 1. Run the following command to install the AAP Backstage provider plugin: @@ -141,7 +40,7 @@ The AAP Backstage provider plugin allows the configuration of one or multiple pr const backend = createBackend(); /* highlight-add-next-line */ - backend.add(import('@janus-idp/backstage-plugin-aap-backend/alpha')); + backend.add(import('@janus-idp/backstage-plugin-aap-backend')); backend.start(); ``` diff --git a/plugins/aap-backend/config.d.ts b/plugins/aap-backend/config.d.ts index 524feb65b6..e45661297b 100644 --- a/plugins/aap-backend/config.d.ts +++ b/plugins/aap-backend/config.d.ts @@ -1,4 +1,4 @@ -import { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks'; +import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api'; export interface Config { catalog?: { @@ -13,7 +13,7 @@ export interface Config { authorization: string; system?: string; owner?: string; - schedule?: TaskScheduleDefinitionConfig; + schedule?: SchedulerServiceTaskScheduleDefinitionConfig; }; }; }; diff --git a/plugins/aap-backend/dev/index.ts b/plugins/aap-backend/dev/index.ts new file mode 100644 index 0000000000..1485e71670 --- /dev/null +++ b/plugins/aap-backend/dev/index.ts @@ -0,0 +1,11 @@ +import { createBackend } from '@backstage/backend-defaults'; + +import { catalogModuleAapResourceEntityProvider } from '../src/module'; + +const backend = createBackend(); + +// api endpoints from here: https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/createRouter.ts +backend.add(import('@backstage/plugin-catalog-backend/alpha')); +backend.add(catalogModuleAapResourceEntityProvider); + +backend.start(); diff --git a/plugins/aap-backend/dist-dynamic/package.json b/plugins/aap-backend/dist-dynamic/package.json index ebe3a08d3f..9356557f61 100644 --- a/plugins/aap-backend/dist-dynamic/package.json +++ b/plugins/aap-backend/dist-dynamic/package.json @@ -18,10 +18,6 @@ "require": "./dist/index.cjs.js", "default": "./dist/index.cjs.js" }, - "./alpha": { - "require": "./dist/alpha.cjs.js", - "default": "./dist/alpha.cjs.js" - }, "./package.json": "./package.json" }, "scripts": {}, @@ -30,8 +26,7 @@ "files": [ "dist", "config.d.ts", - "app-config.janus-idp.yaml", - "alpha" + "app-config.janus-idp.yaml" ], "configSchema": "config.d.ts", "repository": { @@ -53,13 +48,10 @@ "author": "Red Hat", "bundleDependencies": true, "peerDependencies": { - "@backstage/backend-common": "^0.23.3", "@backstage/backend-plugin-api": "^0.7.0", - "@backstage/backend-tasks": "^0.5.27", "@backstage/catalog-model": "^1.5.0", - "@backstage/config": "^1.2.0", - "@backstage/plugin-catalog-node": "^1.12.4", - "@backstage/backend-dynamic-feature-service": "^0.2.15" + "@backstage/errors": "^1.2.4", + "@backstage/plugin-catalog-node": "^1.12.4" }, "overrides": { "@aws-sdk/util-utf8-browser": { diff --git a/plugins/aap-backend/dist-dynamic/yarn.lock b/plugins/aap-backend/dist-dynamic/yarn.lock index 3ec78ad6cb..facf409d9a 100644 --- a/plugins/aap-backend/dist-dynamic/yarn.lock +++ b/plugins/aap-backend/dist-dynamic/yarn.lock @@ -26,6 +26,6 @@ tslib "^2.6.2" tslib@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== diff --git a/plugins/aap-backend/package.json b/plugins/aap-backend/package.json index a9fbda8b94..a5ad4bdc3c 100644 --- a/plugins/aap-backend/package.json +++ b/plugins/aap-backend/package.json @@ -15,14 +15,10 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] @@ -41,16 +37,17 @@ "tsc": "tsc" }, "dependencies": { - "@backstage/backend-common": "^0.23.3", "@backstage/backend-plugin-api": "^0.7.0", - "@backstage/backend-tasks": "^0.5.27", "@backstage/catalog-model": "^1.5.0", - "@backstage/config": "^1.2.0", - "@backstage/plugin-catalog-node": "^1.12.4", - "@backstage/backend-dynamic-feature-service": "^0.2.15" + "@backstage/errors": "^1.2.4", + "@backstage/plugin-catalog-node": "^1.12.4" }, "devDependencies": { + "@backstage/backend-defaults": "0.4.1", + "@backstage/config": "1.2.0", + "@backstage/backend-test-utils": "0.4.4", "@backstage/cli": "0.26.11", + "@backstage/plugin-catalog-backend": "1.24.0", "@janus-idp/cli": "1.15.0", "@types/supertest": "2.0.16", "msw": "1.3.3", @@ -61,7 +58,6 @@ "config.d.ts", "dist-dynamic/*.*", "dist-dynamic/dist/**", - "dist-dynamic/alpha/*", "app-config.janus-idp.yaml" ], "configSchema": "config.d.ts", diff --git a/plugins/aap-backend/src/alpha.ts b/plugins/aap-backend/src/alpha.ts deleted file mode 100644 index 1abf0f3cd6..0000000000 --- a/plugins/aap-backend/src/alpha.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 The Janus IDP Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { catalogModuleAapResourceEntityProvider as default } from './module'; diff --git a/plugins/aap-backend/src/clients/AapResourceConnector.ts b/plugins/aap-backend/src/clients/AapResourceConnector.ts index f4c4fb12ca..f6ff7bc663 100644 --- a/plugins/aap-backend/src/clients/AapResourceConnector.ts +++ b/plugins/aap-backend/src/clients/AapResourceConnector.ts @@ -1,39 +1,43 @@ import { JobTemplates } from './types'; -export function listJobTemplates( +export async function listJobTemplates( baseUrl: string, access_token: string, ): Promise { - return fetch(`${baseUrl}/api/v2/job_templates`, { + const res = await fetch(`${baseUrl}/api/v2/job_templates`, { headers: { 'Content-Type': 'application/json', Authorization: access_token, }, method: 'GET', - }).then(async response => { - if (!response.ok) { - throw new Error(response.statusText); - } - const resData = await response.json(); - return resData.results as Promise; }); + + if (!res.ok) { + throw new Error(res.statusText); + } + + const data = (await res.json()) as { results: JobTemplates }; + + return data.results; } -export function listWorkflowJobTemplates( +export async function listWorkflowJobTemplates( baseUrl: string, access_token: string, ): Promise { - return fetch(`${baseUrl}/api/v2/workflow_job_templates`, { + const res = await fetch(`${baseUrl}/api/v2/workflow_job_templates`, { headers: { 'Content-Type': 'application/json', Authorization: access_token, }, method: 'GET', - }).then(async response => { - if (!response.ok) { - throw new Error(response.statusText); - } - const resData = await response.json(); - return resData.results as Promise; }); + + if (!res.ok) { + throw new Error(res.statusText); + } + + const data = (await res.json()) as { results: JobTemplates }; + + return data.results; } diff --git a/plugins/aap-backend/src/dynamic/index.ts b/plugins/aap-backend/src/dynamic/index.ts deleted file mode 100644 index bc81c3a885..0000000000 --- a/plugins/aap-backend/src/dynamic/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2024 The Janus IDP Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BackendDynamicPluginInstaller } from '@backstage/backend-dynamic-feature-service'; - -import { AapResourceEntityProvider } from '../providers'; - -export const dynamicPluginInstaller: BackendDynamicPluginInstaller = { - kind: 'legacy', - async catalog(builder, env) { - builder.addEntityProvider( - AapResourceEntityProvider.fromConfig(env.config, { - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - scheduler: env.scheduler, - }), - ); - }, -}; diff --git a/plugins/aap-backend/src/index.ts b/plugins/aap-backend/src/index.ts index 6537beea7a..cf49e69d0d 100644 --- a/plugins/aap-backend/src/index.ts +++ b/plugins/aap-backend/src/index.ts @@ -1,3 +1,3 @@ export * from './clients'; +export { catalogModuleAapResourceEntityProvider as default } from './module'; export * from './providers'; -export * from './dynamic/index'; diff --git a/plugins/aap-backend/src/module.test.ts b/plugins/aap-backend/src/module.test.ts new file mode 100644 index 0000000000..f3a7664615 --- /dev/null +++ b/plugins/aap-backend/src/module.test.ts @@ -0,0 +1,209 @@ +import type { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import catalogPlugin from '@backstage/plugin-catalog-backend/alpha'; +import type { EntityProvider } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; + +import { catalogModuleAapResourceEntityProvider } from './module'; + +const AUTH_HEADER = 'Bearer xxxx'; // NOSONAR + +const CONFIG = { + catalog: { + providers: { + aap: { + dev: { + baseUrl: 'http://localhost:8080', + authorization: AUTH_HEADER, + }, + }, + }, + }, +} as const; + +describe('catalogModuleAapResourceEntityProvider', () => { + it('should return an empty array for AAP if no providers are configured', async () => { + let addedProviders: EntityProvider[] | EntityProvider[][] | undefined; + const extensionPoint = { + addEntityProvider: ( + ...providers: EntityProvider[] | EntityProvider[][] + ) => { + addedProviders = providers; + }, + }; + + await startTestBackend({ + extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + features: [ + catalogModuleAapResourceEntityProvider, + mockServices.rootConfig.factory({ data: {} }), + ], + }); + + // Only the AAP provider should be in the array + expect((addedProviders as EntityProvider[][]).length).toEqual(1); + + // AAP returns an array of entity providers + expect((addedProviders as EntityProvider[][])[0].length).toEqual(0); + }); + + it('should not run without a authorization', async () => { + await expect( + startTestBackend({ + features: [ + catalogPlugin, + catalogModuleAapResourceEntityProvider, + mockServices.rootConfig.factory({ + data: { + catalog: { + providers: { + aap: { + dev: { + baseUrl: 'http://localhost:8080', + }, + }, + }, + }, + }, + }), + ], + }), + ).rejects.toThrow( + "Missing required config value at 'catalog.providers.aap.dev.authorization", + ); + }); + + it('should return a single provider with the default schedule', async () => { + let usedSchedule: SchedulerServiceTaskScheduleDefinition | undefined; + const runner = jest.fn(); + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { + usedSchedule = schedule; + return { run: runner }; + }, + }); + + await startTestBackend({ + features: [ + catalogPlugin, + catalogModuleAapResourceEntityProvider, + mockServices.rootConfig.factory({ data: CONFIG }), + scheduler.factory, + ], + }); + + expect(usedSchedule?.frequency).toEqual({ minutes: 30 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); + }); + + it('should return a single provider with a specified schedule', async () => { + let usedSchedule: SchedulerServiceTaskScheduleDefinition | undefined; + const runner = jest.fn(); + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { + usedSchedule = schedule; + return { run: runner }; + }, + }); + + await startTestBackend({ + features: [ + catalogPlugin, + catalogModuleAapResourceEntityProvider, + mockServices.rootConfig.factory({ + data: { + catalog: { + providers: { + aap: { + dev: { + baseUrl: 'http://localhost:8080', + authorization: AUTH_HEADER, + schedule: { + frequency: 'P1M', + timeout: 'PT5M', + }, + }, + }, + }, + }, + }, + }), + scheduler.factory, + ], + }); + + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 5 }); + }); + + it('should return multiple providers', async () => { + let addedProviders: EntityProvider[] | EntityProvider[][] | undefined; + const extensionPoint = { + addEntityProvider: ( + ...providers: EntityProvider[] | EntityProvider[][] + ) => { + addedProviders = providers; + }, + }; + + await startTestBackend({ + extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + features: [ + catalogModuleAapResourceEntityProvider, + mockServices.rootConfig.factory({ + data: { + catalog: { + providers: { + aap: { + dev: { + baseUrl: 'http://localhost:8080', + authorization: AUTH_HEADER, + }, + production: { + baseUrl: 'http://localhost:8080', + authorization: AUTH_HEADER, + }, + }, + }, + }, + }, + }), + ], + }); + + // Only the AAP provider should be in the array + expect((addedProviders as EntityProvider[][]).length).toEqual(1); + + // AAP returns an array of entity providers + expect((addedProviders as EntityProvider[][])[0].length).toEqual(2); + }); + + it('should return provider name', async () => { + let addedProviders: EntityProvider[] | EntityProvider[][] | undefined; + const extensionPoint = { + addEntityProvider: ( + ...providers: EntityProvider[] | EntityProvider[][] + ) => { + addedProviders = providers; + }, + }; + + await startTestBackend({ + extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + features: [ + catalogModuleAapResourceEntityProvider, + mockServices.rootConfig.factory({ + data: CONFIG, + }), + ], + }); + + // Only the AAP provider should be in the array + expect((addedProviders as EntityProvider[][]).length).toEqual(1); + + // AAP returns an array of entity providers + expect( + (addedProviders as EntityProvider[][])[0][0].getProviderName(), + ).toEqual('AapResourceEntityProvider:dev'); + }); +}); diff --git a/plugins/aap-backend/src/module.ts b/plugins/aap-backend/src/module.ts index ebbdbc511a..342b43c242 100644 --- a/plugins/aap-backend/src/module.ts +++ b/plugins/aap-backend/src/module.ts @@ -34,14 +34,16 @@ export const catalogModuleAapResourceEntityProvider = createBackendModule({ }, async init({ catalog, config, logger, scheduler }) { catalog.addEntityProvider( - AapResourceEntityProvider.fromConfig(config, { - logger, - schedule: scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - scheduler: scheduler, - }), + AapResourceEntityProvider.fromConfig( + { config, logger }, + { + schedule: scheduler.createScheduledTaskRunner({ + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }), + scheduler: scheduler, + }, + ), ); }, }); diff --git a/plugins/aap-backend/src/providers/AapResourceEntityProvider.test.ts b/plugins/aap-backend/src/providers/AapResourceEntityProvider.test.ts index 21dd532bcc..fb3d9b950c 100644 --- a/plugins/aap-backend/src/providers/AapResourceEntityProvider.test.ts +++ b/plugins/aap-backend/src/providers/AapResourceEntityProvider.test.ts @@ -1,7 +1,11 @@ -import { getVoidLogger } from '@backstage/backend-common'; -import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; -import { ConfigReader } from '@backstage/config'; -import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import type { + SchedulerServiceTaskInvocationDefinition, + SchedulerServiceTaskRunner, + SchedulerServiceTaskScheduleDefinition, +} from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; +import { ErrorLike } from '@backstage/errors'; +import type { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { listJobTemplates, @@ -11,19 +15,7 @@ import { AapResourceEntityProvider } from './AapResourceEntityProvider'; const AUTH_HEADER = 'Bearer xxxx'; // NOSONAR -const BASIC_VALID_CONFIG = { - catalog: { - providers: { - aap: { - dev: { - baseUrl: 'http://localhost:8080', - }, - }, - }, - }, -} as const; - -const BASIC_VALID_CONFIG_2 = { +const CONFIG = { catalog: { providers: { aap: { @@ -47,130 +39,45 @@ jest.mock('../clients/AapResourceConnector', () => ({ listWorkflowJobTemplates: jest.fn().mockReturnValue({}), })); -class FakeAbortSignal implements AbortSignal { - readonly aborted = false; - readonly reason = undefined; - onabort() { - return null; - } - throwIfAborted() { - return null; - } - addEventListener() { - return null; - } - removeEventListener() { - return null; - } - dispatchEvent() { - return true; - } -} - -class ManualTaskRunner implements TaskRunner { - private tasks: TaskInvocationDefinition[] = []; - async run(task: TaskInvocationDefinition) { +class SchedulerServiceTaskRunnerMock implements SchedulerServiceTaskRunner { + private tasks: SchedulerServiceTaskInvocationDefinition[] = []; + async run(task: SchedulerServiceTaskInvocationDefinition) { this.tasks.push(task); } async runAll() { - const abortSignal = new FakeAbortSignal(); + const abortSignal = jest.fn() as unknown as AbortSignal; for await (const task of this.tasks) { await task.fn(abortSignal); } } - clear() { - this.tasks = []; - } } -describe('AapResourceEntityProvider', () => { - const logMock = jest.fn(); - - const logger = getVoidLogger(); - logger.child = () => logger; - ['log', ...Object.keys(logger.levels)].forEach(logFunctionName => { - (logger as any)[logFunctionName] = function LogMock() { - logMock(logFunctionName, ...arguments); - }; - }); +const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner() { + return new SchedulerServiceTaskRunnerMock(); + }, +}); - const manualTaskRunner = new ManualTaskRunner(); +describe('AapResourceEntityProvider', () => { + let schedule: SchedulerServiceTaskRunnerMock; beforeEach(() => { jest.clearAllMocks(); - manualTaskRunner.clear(); - }); - - afterEach(() => { - const logs = JSON.stringify(logMock.mock.calls); - // eslint-disable-next-line jest/no-standalone-expect - expect(logs).not.toContain(AUTH_HEADER); - }); - - it('should return an empty array if no providers are configured', () => { - const config = new ConfigReader({}); - - const result = AapResourceEntityProvider.fromConfig(config, { - logger, - }); - - expect(result).toEqual([]); - }); - - it('should not run without a authorization', () => { - const config = new ConfigReader(BASIC_VALID_CONFIG); - - expect(() => - AapResourceEntityProvider.fromConfig(config, { - logger, - }), - ).toThrow( - "Missing required config value at 'catalog.providers.aap.dev.authorization", - ); - }); - - it('should not run without a valid schedule', () => { - const config = new ConfigReader(BASIC_VALID_CONFIG_2); - - expect(() => - AapResourceEntityProvider.fromConfig(config, { - logger, - }), - ).toThrow( - 'No schedule provided neither via code nor config for AapResourceEntityProvider:dev.', - ); - }); - - it('should return a single provider if one is configured', () => { - const config = new ConfigReader(BASIC_VALID_CONFIG_2); - const aap = AapResourceEntityProvider.fromConfig(config, { - logger, - schedule: manualTaskRunner, - }); - - expect(aap).toHaveLength(1); - }); - - it('should return provider name', () => { - const config = new ConfigReader(BASIC_VALID_CONFIG_2); - - const aap = AapResourceEntityProvider.fromConfig(config, { - logger, - schedule: manualTaskRunner, - }); - - expect(aap.map(k => k.getProviderName())).toEqual([ - 'AapResourceEntityProvider:dev', - ]); + schedule = scheduler.createScheduledTaskRunner( + '' as unknown as SchedulerServiceTaskScheduleDefinition, + ) as SchedulerServiceTaskRunnerMock; }); it('should connect', async () => { - const config = new ConfigReader(BASIC_VALID_CONFIG_2); - - const aap = AapResourceEntityProvider.fromConfig(config, { - logger, - schedule: manualTaskRunner, - }); + const aap = AapResourceEntityProvider.fromConfig( + { + config: mockServices.rootConfig({ data: CONFIG }), + logger: mockServices.logger.mock(), + }, + { + schedule, + }, + ); const result = await Promise.all( aap.map(async k => await k.connect(connection)), @@ -199,16 +106,20 @@ describe('AapResourceEntityProvider', () => { }, ]), ); - const config = new ConfigReader(BASIC_VALID_CONFIG_2); - const aap = AapResourceEntityProvider.fromConfig(config, { - logger, - schedule: manualTaskRunner, - }); + const aap = AapResourceEntityProvider.fromConfig( + { + config: mockServices.rootConfig({ data: CONFIG }), + logger: mockServices.logger.mock(), + }, + { + schedule, + }, + ); for await (const k of aap) { await k.connect(connection); - await manualTaskRunner.runAll(); + await schedule.runAll(); } expect(connection.applyMutation).toHaveBeenCalledTimes(1); @@ -218,15 +129,14 @@ describe('AapResourceEntityProvider', () => { }); it('should connect and run should resolves even if one api call fails', async () => { - const error: Error & { config?: any; status?: number } = new Error( - 'Request failed with status code 401', - ); + const error = new Error('Request failed with status code 401') as ErrorLike; error.config = { header: { - authorization: 'Bearer xxxx', // NOSONAR + authorization: AUTH_HEADER, }, }; error.status = 401; + (listJobTemplates as jest.Mock).mockRejectedValue(error); (listWorkflowJobTemplates as jest.Mock).mockReturnValue( Promise.resolve([ @@ -238,29 +148,38 @@ describe('AapResourceEntityProvider', () => { }, ]), ); - const config = new ConfigReader(BASIC_VALID_CONFIG_2); - const aap = AapResourceEntityProvider.fromConfig(config, { - logger, - schedule: manualTaskRunner, + // every aap provider automatically creates a new logger + const aapLogger = mockServices.logger.mock(); + const logger = mockServices.logger.mock({ + child() { + return aapLogger; + }, }); + const aap = AapResourceEntityProvider.fromConfig( + { config: mockServices.rootConfig({ data: CONFIG }), logger }, + { + schedule, + }, + ); + for await (const k of aap) { await k.connect(connection); - await manualTaskRunner.runAll(); + await schedule.runAll(); } + expect(logger.child).toHaveBeenCalledTimes(1); + expect(connection.applyMutation).toHaveBeenCalledTimes(1); expect( (connection.applyMutation as jest.Mock).mock.calls, ).toMatchSnapshot(); - expect(logMock).toHaveBeenCalledWith( - 'info', + expect(aapLogger.info).toHaveBeenCalledWith( 'Discovering ResourceEntities from AAP http://localhost:8080', ); - expect(logMock).toHaveBeenCalledWith( - 'error', + expect(aapLogger.error).toHaveBeenCalledWith( 'Failed to fetch AAP job templates', { name: 'Error', @@ -268,8 +187,7 @@ describe('AapResourceEntityProvider', () => { stack: expect.any(String), }, ); - expect(logMock).toHaveBeenCalledWith( - 'debug', + expect(aapLogger.debug).toHaveBeenCalledWith( 'Discovered ResourceEntity "demoWorkflowJobTemplate"', ); }); diff --git a/plugins/aap-backend/src/providers/AapResourceEntityProvider.ts b/plugins/aap-backend/src/providers/AapResourceEntityProvider.ts index b9811e2d50..dfa2a2fb2f 100644 --- a/plugins/aap-backend/src/providers/AapResourceEntityProvider.ts +++ b/plugins/aap-backend/src/providers/AapResourceEntityProvider.ts @@ -1,13 +1,17 @@ -import { LoggerService } from '@backstage/backend-plugin-api'; -import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; +import type { + LoggerService, + SchedulerService, + SchedulerServiceTaskRunner, +} from '@backstage/backend-plugin-api'; import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, Entity, ResourceEntity, } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { +import type { Config } from '@backstage/config'; +import { InputError, isError, NotFoundError } from '@backstage/errors'; +import type { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; @@ -16,9 +20,9 @@ import { listJobTemplates, listWorkflowJobTemplates, } from '../clients/AapResourceConnector'; -import { JobTemplate } from '../clients/types'; +import type { JobTemplate } from '../clients/types'; import { readAapApiEntityConfigs } from './config'; -import { AapConfig } from './types'; +import type { AapConfig } from './types'; export class AapResourceEntityProvider implements EntityProvider { private readonly env: string; @@ -31,47 +35,47 @@ export class AapResourceEntityProvider implements EntityProvider { private connection?: EntityProviderConnection; static fromConfig( - configRoot: Config, - options: { + deps: { + config: Config; logger: LoggerService; - schedule?: TaskRunner; - scheduler?: PluginTaskScheduler; }, + + options: + | { schedule: SchedulerServiceTaskRunner } + | { scheduler: SchedulerService }, ): AapResourceEntityProvider[] { - const providerConfigs = readAapApiEntityConfigs(configRoot); + const { config, logger } = deps; + + const providerConfigs = readAapApiEntityConfigs(config); return providerConfigs.map(providerConfig => { let taskRunner; - if (options.scheduler && providerConfig.schedule) { + if ('scheduler' in options && providerConfig.schedule) { taskRunner = options.scheduler.createScheduledTaskRunner( providerConfig.schedule, ); - } else if (options.schedule) { + } else if ('schedule' in options) { taskRunner = options.schedule; } else { - throw new Error( - `No schedule provided neither via code nor config for AapResourceEntityProvider:${providerConfig.id}.`, + throw new InputError( + `No schedule provided via config for AapResourceEntityProvider:${providerConfig.id}.`, ); } - return new AapResourceEntityProvider( - providerConfig, - options.logger, - taskRunner, - ); + return new AapResourceEntityProvider(providerConfig, logger, taskRunner); }); } private constructor( config: AapConfig, logger: LoggerService, - taskRunner: TaskRunner, + taskRunner: SchedulerServiceTaskRunner, ) { this.env = config.id; this.baseUrl = config.baseUrl; this.authorization = config.authorization; this.owner = config.owner; - this.system = config.system || ''; + this.system = config.system ?? ''; this.logger = logger.child({ target: this.getProviderName(), }); @@ -79,7 +83,9 @@ export class AapResourceEntityProvider implements EntityProvider { this.scheduleFn = this.createScheduleFn(taskRunner); } - createScheduleFn(taskRunner: TaskRunner): () => Promise { + createScheduleFn( + taskRunner: SchedulerServiceTaskRunner, + ): () => Promise { return async () => { const taskId = `${this.getProviderName()}:run`; return taskRunner.run({ @@ -87,19 +93,21 @@ export class AapResourceEntityProvider implements EntityProvider { fn: async () => { try { await this.run(); - } catch (error: any) { - // Ensure that we don't log any sensitive internal data: - this.logger.error( - `Error while syncing resources from AAP ${this.baseUrl}`, - { - // Default Error properties: - name: error.name, - message: error.message, - stack: error.stack, - // Additional status code if available: - status: error.response?.status, - }, - ); + } catch (error) { + if (isError(error)) { + // Ensure that we don't log any sensitive internal data: + this.logger.error( + `Error while syncing resources from AAP ${this.baseUrl}`, + { + // Default Error properties: + name: error.name, + message: error.message, + stack: error.stack, + // Additional status code if available: + status: (error.response as { status?: string })?.status, + }, + ); + } } }, }); @@ -117,7 +125,7 @@ export class AapResourceEntityProvider implements EntityProvider { async run(): Promise { if (!this.connection) { - throw new Error('Not initialized'); + throw new NotFoundError('Not initialized'); } this.logger.info(`Discovering ResourceEntities from AAP ${this.baseUrl}`); diff --git a/plugins/aap-backend/src/providers/config.ts b/plugins/aap-backend/src/providers/config.ts index a88607cf0d..20ef194523 100644 --- a/plugins/aap-backend/src/providers/config.ts +++ b/plugins/aap-backend/src/providers/config.ts @@ -1,7 +1,7 @@ -import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks'; -import { Config } from '@backstage/config'; +import { readSchedulerServiceTaskScheduleDefinitionFromConfig } from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; -import { AapConfig } from './types'; +import type { AapConfig } from './types'; export function readAapApiEntityConfigs(config: Config): AapConfig[] { const providerConfigs = config.getOptionalConfig('catalog.providers.aap'); @@ -17,10 +17,12 @@ function readAapApiEntityConfig(id: string, config: Config): AapConfig { const baseUrl = config.getString('baseUrl'); const authorization = config.getString('authorization'); const system = config.getOptionalString('system'); - const owner = config.getOptionalString('owner') || 'unknown'; + const owner = config.getOptionalString('owner') ?? 'unknown'; const schedule = config.has('schedule') - ? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule')) + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + config.getConfig('schedule'), + ) : undefined; return { diff --git a/plugins/aap-backend/src/providers/types.ts b/plugins/aap-backend/src/providers/types.ts index 303466727f..4969f744c6 100644 --- a/plugins/aap-backend/src/providers/types.ts +++ b/plugins/aap-backend/src/providers/types.ts @@ -1,4 +1,4 @@ -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import type { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; export type AapConfig = { id: string; @@ -6,5 +6,5 @@ export type AapConfig = { authorization: string; owner: string; system?: string; - schedule?: TaskScheduleDefinition; + schedule?: SchedulerServiceTaskScheduleDefinition; }; diff --git a/plugins/aap-backend/src/run.ts b/plugins/aap-backend/src/run.ts deleted file mode 100644 index 8498f0e5e8..0000000000 --- a/plugins/aap-backend/src/run.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getRootLogger } from '@backstage/backend-common'; - -const logger = getRootLogger(); - -process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); -}); diff --git a/yarn.lock b/yarn.lock index bf31b1824c..919664c3ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2932,7 +2932,7 @@ yn "^4.0.0" zod "^3.22.4" -"@backstage/backend-defaults@^0.4.0", "@backstage/backend-defaults@^0.4.1": +"@backstage/backend-defaults@0.4.1", "@backstage/backend-defaults@^0.4.0", "@backstage/backend-defaults@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@backstage/backend-defaults/-/backend-defaults-0.4.1.tgz#072f5bbd2bb8a8c4998f6baba7e1134d7171a8dc" integrity sha512-dLuFjJCPsWDJQzdauNQMdPjinV2YB+k6Jx2JSx04l3SCspjdmBRnZf/jwIrPcQyzgQrCQhupHeprYq7wJSXgbA==