From e4dabc605c9426763567421bff87bd501fdca487 Mon Sep 17 00:00:00 2001 From: Vinnie McGuinness Date: Thu, 8 May 2025 12:17:26 +0100 Subject: [PATCH 1/2] feat(catalog-backend-module-gitea) add new Gitea provider module Signed-off-by: Vinnie McGuinness feat(catalog-backend-module-gitea) add new Gitea provider module Signed-off-by: Vinnie McGuinness adding tests --- .changeset/orange-banks-deny.md | 5 + .github/CODEOWNERS | 1 + docs/integrations/gitea/discovery.md | 63 +++ .../catalog-backend-module-gitea/.eslintrc.js | 1 + .../catalog-backend-module-gitea/README.md | 8 + .../catalog-info.yaml | 10 + .../catalog-backend-module-gitea/package.json | 51 ++ .../report.api.md | 36 ++ .../catalog-backend-module-gitea/src/index.ts | 24 + .../src/module.test.ts | 86 +++ .../src/module.ts | 47 ++ .../src/providers/GiteaEntityProvider.test.ts | 532 ++++++++++++++++++ .../src/providers/GiteaEntityProvider.ts | 266 +++++++++ .../src/providers/config.test.ts | 125 ++++ .../src/providers/config.ts | 58 ++ .../src/providers/core.test.ts | 42 ++ .../src/providers/core.ts | 26 + .../src/providers/index.ts | 17 + .../src/providers/types.ts | 36 ++ yarn.lock | 18 + 20 files changed, 1452 insertions(+) create mode 100644 .changeset/orange-banks-deny.md create mode 100644 docs/integrations/gitea/discovery.md create mode 100644 plugins/catalog-backend-module-gitea/.eslintrc.js create mode 100644 plugins/catalog-backend-module-gitea/README.md create mode 100644 plugins/catalog-backend-module-gitea/catalog-info.yaml create mode 100644 plugins/catalog-backend-module-gitea/package.json create mode 100644 plugins/catalog-backend-module-gitea/report.api.md create mode 100644 plugins/catalog-backend-module-gitea/src/index.ts create mode 100644 plugins/catalog-backend-module-gitea/src/module.test.ts create mode 100644 plugins/catalog-backend-module-gitea/src/module.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/config.test.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/config.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/core.test.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/core.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/index.ts create mode 100644 plugins/catalog-backend-module-gitea/src/providers/types.ts diff --git a/.changeset/orange-banks-deny.md b/.changeset/orange-banks-deny.md new file mode 100644 index 0000000000..3719206865 --- /dev/null +++ b/.changeset/orange-banks-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitea': minor +--- + +add new gitea provider module diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 41f409a651..f5c6f49617 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -51,6 +51,7 @@ yarn.lock @backstage/maintainers @backst /plugins/catalog-backend-module-aws @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann /plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann /plugins/catalog-backend-module-backstage-openapi @backstage/maintainers @backstage/reviewers @backstage/openapi-tooling-maintainers +/plugins/catalog-backend-module-gitea @backstage/maintainers @backstage/reviewers @vinnie-jog-on /plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann /plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers /plugins/catalog-graph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @backstage/sda-se-reviewers diff --git a/docs/integrations/gitea/discovery.md b/docs/integrations/gitea/discovery.md new file mode 100644 index 0000000000..c01ea52edf --- /dev/null +++ b/docs/integrations/gitea/discovery.md @@ -0,0 +1,63 @@ +--- +id: discovery +title: Gitea Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from Gitea repositories +--- + +:::info +This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). +::: + +The Gitea integration has a special entity provider for discovering catalog entities +from Gitea repositories. The provider uses the "List Projects" API in Gitea to get +a list of repositories and will automatically ingest all `catalog-info.yaml` files +stored in the root of the matching projects. + +## Installation + +As this provider is not one of the default providers, you will first need to install +the Gitea provider plugin: + +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitea +``` + +Then update your backend by adding the following line: + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-catalog-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-catalog-backend-module-gitea')); +/* highlight-add-end */ +``` + +## Configuration + +To use the discovery processor, you'll need a Gitea integration +[set up](locations.md). Then you can add any number of providers. + +```yaml +# app-config.yaml +catalog: + providers: + gitea: + yourProviderId: # identifies your dataset / provider independent of config changes + organization: 'your-company' # string + host: gitea-your-company.com + branch: 'main' # Optional, defaults to 'main' + catalogPath: 'catalog-info.yaml' # Optional, defaults to catalog-info.yaml + schedule: + # supports cron, ISO duration, "human duration" as used in code + frequency: { minutes: 30 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } +``` + +The provider configuration consists of the following parts: + +- **`organization`**: Name of your organization account/workspace. If you want to add multiple organizations, you need to add one provider config each. +- **`host`**: the host of the Gitea integration to use. +- **`branch`** _(optional)_: the branch where we will look for catalog entities (defaults to 'main'). +- **`catalogPath`**: path relative to the root of the repository where the Backstage manifests are stored. diff --git a/plugins/catalog-backend-module-gitea/.eslintrc.js b/plugins/catalog-backend-module-gitea/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-gitea/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-gitea/README.md b/plugins/catalog-backend-module-gitea/README.md new file mode 100644 index 0000000000..6b93c9c90a --- /dev/null +++ b/plugins/catalog-backend-module-gitea/README.md @@ -0,0 +1,8 @@ +# Catalog Backend Module for Gitea + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at Gitea integrations. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/gitea/discovery.md) +for details on how to install and configure the plugin. diff --git a/plugins/catalog-backend-module-gitea/catalog-info.yaml b/plugins/catalog-backend-module-gitea/catalog-info.yaml new file mode 100644 index 0000000000..eb580d570b --- /dev/null +++ b/plugins/catalog-backend-module-gitea/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-gitea + title: '@backstage/plugin-catalog-backend-module-gitea' + description: The gitea backend module for the catalog plugin provide by intive.com +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json new file mode 100644 index 0000000000..fbba66a039 --- /dev/null +++ b/plugins/catalog-backend-module-gitea/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-gitea", + "version": "0.1.0", + "license": "Apache-2.0", + "private": true, + "description": "The gitea backend module for the catalog plugin.", + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-gitea" + }, + "backstage": { + "role": "backend-plugin-module", + "pluginId": "catalog", + "pluginPackage": "@backstage/plugin-catalog-backend" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/integration": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", + "p-limit": "^3.0.2", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@backstage/backend-common": "^0.25.0", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-backend-module-gitea/report.api.md b/plugins/catalog-backend-module-gitea/report.api.md new file mode 100644 index 0000000000..1aae80271a --- /dev/null +++ b/plugins/catalog-backend-module-gitea/report.api.md @@ -0,0 +1,36 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-gitea" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-node'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { SchedulerService } from '@backstage/backend-plugin-api'; +import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +const catalogModuleGitea: BackendFeature; +export default catalogModuleGitea; + +// @public (undocumented) +export class GiteaEntityProvider implements EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + configRoot: Config, + options: { + logger: LoggerService; + schedule?: SchedulerServiceTaskRunner; + scheduler?: SchedulerService; + }, + ): GiteaEntityProvider[]; + // (undocumented) + getProviderName(): string; + // (undocumented) + refresh(logger: LoggerService): Promise; +} +``` diff --git a/plugins/catalog-backend-module-gitea/src/index.ts b/plugins/catalog-backend-module-gitea/src/index.ts new file mode 100644 index 0000000000..b92ced7fcf --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2025 The Backstage 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. + */ + +/** + * The gitea backend module for the catalog plugin. + * + * @packageDocumentation + */ + +export { catalogModuleGitea as default } from './module'; +export { GiteaEntityProvider } from './providers/GiteaEntityProvider'; diff --git a/plugins/catalog-backend-module-gitea/src/module.test.ts b/plugins/catalog-backend-module-gitea/src/module.test.ts new file mode 100644 index 0000000000..0ce2d4319a --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/module.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2022 The Backstage 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 { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; +import { catalogModuleGitea } from './module'; +import { GiteaEntityProvider } from './providers/GiteaEntityProvider'; + +describe('catalogModuleGitea', () => { + it('should register provider at the catalog extension point', async () => { + let addedProviders: Array | undefined; + let usedSchedule: SchedulerServiceTaskScheduleDefinition | undefined; + + const extensionPoint = { + addEntityProvider: (providers: any) => { + addedProviders = providers; + }, + }; + const runner = jest.fn(); + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { + usedSchedule = schedule; + return { run: runner }; + }, + }); + + const config = { + catalog: { + providers: { + gitea: { + test: { + host: 'g.com', + organization: 'org', + branch: 'main', + schedule: { + frequency: 'P1M', + timeout: 'PT3M', + }, + }, + }, + }, + }, + integrations: { + gitea: [ + { + host: 'g.com', + baseUrl: 'https://g.com/gitea', + gitilesBaseUrl: 'https:/g.com/gitiles', + }, + ], + }, + }; + + await startTestBackend({ + extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + features: [ + catalogModuleGitea, + mockServices.rootConfig.factory({ data: config }), + mockServices.logger.factory(), + scheduler.factory, + ], + }); + + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); + expect(addedProviders?.length).toEqual(1); + expect(addedProviders?.pop()?.getProviderName()).toEqual( + 'gitea-provider:test', + ); + expect(runner).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/catalog-backend-module-gitea/src/module.ts b/plugins/catalog-backend-module-gitea/src/module.ts new file mode 100644 index 0000000000..91e019c6fb --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/module.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Backstage 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 { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; + +import { GiteaEntityProvider } from './providers/GiteaEntityProvider'; +/** + * @public + */ +export const catalogModuleGitea = createBackendModule({ + pluginId: 'catalog', + moduleId: 'gitea', + register(reg) { + reg.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + config: coreServices.rootConfig, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + }, + + async init({ catalog, config, logger, scheduler }) { + const providers = GiteaEntityProvider.fromConfig(config, { + logger, + scheduler, + }); + catalog.addEntityProvider(providers); + }, + }); + }, +}); diff --git a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts new file mode 100644 index 0000000000..c17b3bc20e --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts @@ -0,0 +1,532 @@ +/* + * Copyright 2022 The Backstage 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 { getVoidLogger } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { ConfigReader } from '@backstage/config'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { GiteaEntityProvider } from './GiteaEntityProvider'; +import * as uuid from 'uuid'; +import { readGiteaConfigs } from './config'; +import { getGiteaApiUrl } from './core'; +import { GiteaIntegration } from '@backstage/integration'; + +jest.mock('./config'); +jest.mock('./core'); +jest.mock('uuid'); + +describe('GiteaEntityProvider', () => { + const logger = getVoidLogger() as unknown as LoggerService; + const mockScheduler = { + createScheduledTaskRunner: jest.fn(), + triggerTask: jest.fn(), + scheduleTask: jest.fn(), + getScheduledTasks: jest.fn(), + }; + const mockTaskRunner = { + run: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + (readGiteaConfigs as jest.Mock).mockReturnValue([ + { + id: 'test-provider', + host: 'gitea.example.com', + organization: 'test-org', + catalogPath: 'catalog-info.yaml', + branch: 'main', + schedule: { + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }, + }, + ]); + (getGiteaApiUrl as jest.Mock).mockReturnValue( + 'https://gitea.example.com/api/v1/', + ); + mockScheduler.createScheduledTaskRunner.mockReturnValue(mockTaskRunner); + (uuid.v4 as jest.Mock).mockReturnValue('test-uuid'); + + // Mock global fetch + global.fetch = jest.fn(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('fromConfig', () => { + it('should create providers from config', () => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'gitea.example.com', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitea: { + 'test-provider': { + host: 'gitea.example.com', + organization: 'test-org', + }, + }, + }, + }, + }); + + const providers = GiteaEntityProvider.fromConfig(config, { + logger, + scheduler: mockScheduler, + }); + + expect(providers).toHaveLength(1); + expect(providers[0].getProviderName()).toBe( + 'gitea-provider:test-provider', + ); + }); + + it('should throw if no schedule or scheduler is provided', () => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'gitea.example.com', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitea: { + 'test-provider': { + host: 'gitea.example.com', + organization: 'test-org', + }, + }, + }, + }, + }); + + expect(() => + GiteaEntityProvider.fromConfig(config, { + logger, + } as any), + ).toThrow('Either schedule or scheduler must be provided.'); + }); + + it('should throw if no matching integration is found', () => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'wrong-host.example.com', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitea: { + 'test-provider': { + host: 'gitea.example.com', + organization: 'test-org', + }, + }, + }, + }, + }); + + expect(() => + GiteaEntityProvider.fromConfig(config, { + logger, + scheduler: mockScheduler, + }), + ).toThrow( + 'No Gitea integration found that matches host gitea.example.com', + ); + }); + + it('should throw if no schedule is provided in code or config', () => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'gitea.example.com', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitea: { + 'test-provider': { + host: 'gitea.example.com', + organization: 'test-org', + schedule: undefined, + }, + }, + }, + }, + }); + + (readGiteaConfigs as jest.Mock).mockReturnValue([ + { + id: 'test-provider', + host: 'gitea.example.com', + organization: 'test-org', + catalogPath: 'catalog-info.yaml', + branch: 'main', + schedule: undefined, + }, + ]); + + expect(() => + GiteaEntityProvider.fromConfig(config, { + logger, + scheduler: mockScheduler, + }), + ).toThrow( + 'No schedule provided neither via code nor config for Gitea-provider:test-provider.', + ); + }); + }); + + describe('getProviderName', () => { + it('should return the correct provider name', () => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'gitea.example.com', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitea: { + 'test-provider': { + host: 'gitea.example.com', + organization: 'test-org', + }, + }, + }, + }, + }); + + const providers = GiteaEntityProvider.fromConfig(config, { + logger, + scheduler: mockScheduler, + }); + + expect(providers[0].getProviderName()).toBe( + 'gitea-provider:test-provider', + ); + }); + }); + + describe('connect', () => { + let provider: GiteaEntityProvider; + const mockConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + beforeEach(() => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'gitea.example.com', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitea: { + 'test-provider': { + host: 'gitea.example.com', + organization: 'test-org', + }, + }, + }, + }, + }); + + provider = GiteaEntityProvider.fromConfig(config, { + logger, + scheduler: mockScheduler, + })[0]; + }); + + it('should connect and schedule a refresh', async () => { + await provider.connect(mockConnection); + + expect(mockTaskRunner.run).toHaveBeenCalledWith({ + id: 'gitea-provider:test-provider:refresh', + fn: expect.any(Function), + }); + }); + + it('should run the scheduled refresh function', async () => { + await provider.connect(mockConnection); + + // Extract and call the scheduled function + const runArgument = mockTaskRunner.run.mock.calls[0][0]; + const refreshFn = runArgument.fn; + + // Mock the refresh behavior + (global.fetch as jest.Mock).mockImplementation(async (url: string) => { + if (url.includes('/orgs/test-org/repos')) { + if (url.includes('page=1')) { + return { + ok: true, + json: async () => [ + { + name: 'repo1', + html_url: 'https://gitea.example.com/test-org/repo1', + empty: false, + }, + { + name: 'repo2', + html_url: 'https://gitea.example.com/test-org/repo2', + empty: false, + }, + ], + }; + } + return { + ok: true, + json: async () => [], + }; + } else if (url.includes('/contents/catalog-info.yaml')) { + if (url.includes('repo1')) { + return { ok: true }; + } + } + return { ok: false }; + }); + + const abortController = new AbortController(); + await refreshFn(abortController.signal); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + locationKey: 'gitea-provider:test-provider', + entity: expect.objectContaining({ + metadata: expect.objectContaining({ + annotations: expect.objectContaining({ + 'backstage.io/managed-by-location': + 'url:https://gitea.example.com/test-org/repo1/src/branch/main/catalog-info.yaml', + }), + }), + }), + }, + ], + }); + }); + }); + + describe('refresh', () => { + let provider: GiteaEntityProvider; + let integration: GiteaIntegration; + const mockConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + beforeEach(() => { + // Need to access the provider internals for refresh tests + integration = { + config: { + host: 'gitea.example.com', + token: 'test-token', + }, + } as unknown as GiteaIntegration; + + // Direct instantiation for testing the refresh method + provider = new (GiteaEntityProvider as any)( + { + id: 'test-provider', + host: 'gitea.example.com', + organization: 'test-org', + catalogPath: 'catalog-info.yaml', + branch: 'main', + }, + integration, + logger, + mockTaskRunner, + ); + }); + + it('should throw if not connected', async () => { + await expect(provider.refresh(logger)).rejects.toThrow( + 'Gitea discovery connection not initialized', + ); + }); + + it('should handle API errors gracefully', async () => { + await provider.connect(mockConnection); + + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 500, + }); + + await expect(provider.refresh(logger)).rejects.toThrow( + 'Failed to list Gitea projects for organization test-org', + ); + }); + + it('should handle empty results', async () => { + await provider.connect(mockConnection); + + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => [], + }); + + await provider.refresh(logger); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [], + }); + }); + + it('should handle pagination and filter repos with catalog files', async () => { + await provider.connect(mockConnection); + + // Page 1 response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => [ + { + name: 'repo1', + html_url: 'https://gitea.example.com/test-org/repo1', + empty: false, + }, + { + name: 'repo2', + html_url: 'https://gitea.example.com/test-org/repo2', + empty: false, + }, + { + name: 'empty-repo', + html_url: 'https://gitea.example.com/test-org/empty-repo', + empty: true, + }, + ], + }); + + // Page 2 response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => [ + { + name: 'repo3', + html_url: 'https://gitea.example.com/test-org/repo3', + empty: false, + }, + ], + }); + + // Empty page 3 to terminate pagination + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => [], + }); + + // Catalog check for repo1 - has catalog file + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + }); + + // Catalog check for repo2 - no catalog file + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + }); + + // Catalog check for repo3 - has catalog file + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + }); + + await provider.refresh(logger); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + expect.objectContaining({ + locationKey: 'gitea-provider:test-provider', + entity: expect.anything(), + }), + expect.objectContaining({ + locationKey: 'gitea-provider:test-provider', + entity: expect.anything(), + }), + ], + }); + + // Verify we made requests for all repos + expect(global.fetch).toHaveBeenCalledTimes(6); + }); + + it('should handle errors when applying mutations', async () => { + await provider.connect(mockConnection); + + // Mock repository data + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => [ + { + name: 'repo1', + html_url: 'https://gitea.example.com/test-org/repo1', + empty: false, + }, + ], + }); + + // Empty page 2 + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => [], + }); + + // Catalog check - has catalog file + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + }); + + // Mock mutation failure + (mockConnection.applyMutation as jest.Mock).mockRejectedValueOnce( + new Error('Mutation failed'), + ); + + // Should not throw but log the error + await provider.refresh(logger); + + expect(mockConnection.applyMutation).toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts new file mode 100644 index 0000000000..7097edf2ca --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts @@ -0,0 +1,266 @@ +/* + * Copyright 2022 The Backstage 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 * as uuid from 'uuid'; +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import { + EntityProvider, + EntityProviderConnection, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { + GiteaIntegration, + getGiteaRequestOptions, + ScmIntegrations, +} from '@backstage/integration'; +import { getGiteaApiUrl } from './core'; + +import { readGiteaConfigs } from './config'; +import { GiteaProjectQueryResult, GiteaProviderConfig } from './types'; +import { + LoggerService, + SchedulerService, + SchedulerServiceTaskRunner, +} from '@backstage/backend-plugin-api'; + +/** @public */ +export class GiteaEntityProvider implements EntityProvider { + private readonly config: GiteaProviderConfig; + private readonly integration: GiteaIntegration; + private readonly logger: LoggerService; + private readonly scheduleFn: () => Promise; + private connection?: EntityProviderConnection; + + static fromConfig( + configRoot: Config, + options: { + logger: LoggerService; + schedule?: SchedulerServiceTaskRunner; + scheduler?: SchedulerService; + }, + ): GiteaEntityProvider[] { + if (!options.schedule && !options.scheduler) { + throw new Error('Either schedule or scheduler must be provided.'); + } + + const providerConfigs = readGiteaConfigs(configRoot); + const integrations = ScmIntegrations.fromConfig(configRoot).gitea; + const providers: GiteaEntityProvider[] = []; + + providerConfigs.forEach(providerConfig => { + const integration = integrations.byHost(providerConfig.host); + if (!integration) { + throw new InputError( + `No Gitea integration found that matches host ${providerConfig.host}`, + ); + } + + if (!options.schedule && !providerConfig.schedule) { + throw new Error( + `No schedule provided neither via code nor config for Gitea-provider:${providerConfig.id}.`, + ); + } + + const taskRunner = + options.schedule ?? + options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!); + + providers.push( + new GiteaEntityProvider( + providerConfig, + integration, + options.logger, + taskRunner, + ), + ); + }); + + return providers; + } + + private constructor( + config: GiteaProviderConfig, + integration: GiteaIntegration, + logger: LoggerService, + taskRunner: SchedulerServiceTaskRunner, + ) { + this.config = config; + this.integration = integration; + this.logger = logger.child({ + target: this.getProviderName(), + }); + this.scheduleFn = this.createScheduleFn(taskRunner); + } + + getProviderName(): string { + return `gitea-provider:${this.config.id}`; + } + + async connect(connection: EntityProviderConnection): Promise { + this.connection = connection; + await this.scheduleFn(); + } + + private async parseGiteaJsonResponse( + response: Response, + ): Promise { + if (!response.ok) { + throw new Error( + `Failed to fetch Gitea repositories, status: ${response.status}`, + ); + } + try { + return await response.json(); + } catch (error) { + throw new Error(`Failed to parse Gitea API response: ${error}`); + } + } + + private createScheduleFn( + taskRunner: SchedulerServiceTaskRunner, + ): () => Promise { + return async () => { + const taskId = `${this.getProviderName()}:refresh`; + return taskRunner.run({ + id: taskId, + fn: async () => { + const logger = this.logger.child({ + class: GiteaEntityProvider.prototype.constructor.name, + taskId, + taskInstanceId: uuid.v4(), + }); + + try { + await this.refresh(logger); + } catch (error) { + logger.error( + `${this.getProviderName()} refresh failed, ${error}`, + error as Error, + ); + } + }, + }); + }; + } + + async refresh(logger: LoggerService): Promise { + if (!this.connection) { + throw new Error('Gitea discovery connection not initialized'); + } + let allRepos: any[] = []; + + try { + // Step 1: Fetch all repos for an organization + const OrgRepoApiUrl = `${getGiteaApiUrl(this.integration.config)}orgs/${ + this.config.organization + }/repos`; + let page = 1; + let hasMoreData = true; + + while (hasMoreData) { + const url = `${OrgRepoApiUrl}?page=${page}`; + const response = await fetch(url, { + method: 'GET', + ...getGiteaRequestOptions(this.integration.config), + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch page ${page} of repos, status: ${response.status}`, + ); + } + + const projectsPage = (await this.parseGiteaJsonResponse( + response, + )) as GiteaProjectQueryResult; + + if (!Array.isArray(projectsPage) || projectsPage.length === 0) { + hasMoreData = false; + } else { + allRepos = allRepos.concat(projectsPage); + page++; + } + } + } catch (e) { + throw new Error( + `Failed to list Gitea projects for organization ${this.config.organization}, ${e}`, + ); + } + + // Step 2: Filter for repos that have catalog-info.yaml at root + const { default: pLimit } = await import('p-limit'); + const limit = pLimit(5); + const validRepos: string[] = []; + + await Promise.all( + allRepos.map(repo => + limit(async () => { + if (repo.empty) { + logger.warn(`Repo ${repo.html_url} is empty, skipping`); + return; + } + const contentsApiUrl = `${getGiteaApiUrl( + this.integration.config, + )}repos/${this.config.organization}/${repo.name}/contents/${ + this.config.catalogPath + }`; + + const res = await fetch(contentsApiUrl, { + method: 'GET', + ...getGiteaRequestOptions(this.integration.config), + }); + if (res.ok) { + validRepos.push(repo.html_url); + } else { + logger.warn( + `Repo ${repo.html_url} does not contain a catalog-info.yaml file`, + ); + return; + } + }), + ), + ); + // Step 3: create location specs for each valid repo + const locations = await Promise.all( + validRepos.map(repo => limit(() => this.createLocationSpec(repo))), + ); + // Step 4: Apply the locations to the catalog + try { + const providerName: string = this.getProviderName(); + const entities = locations.map(location => ({ + locationKey: providerName, + entity: locationSpecToLocationEntity({ location }), + })); + await this.connection.applyMutation({ + type: 'full', + entities: entities, + }); + } catch (e) { + logger.error(`Failed to apply mutation: ${e}`); + } + logger.info( + `Found ${locations.length} locations from ${allRepos.length} repos`, + ); + } + + private async createLocationSpec(repo: string): Promise { + return { + type: 'url', + target: `${repo}/src/branch/${this.config.branch}/${this.config.catalogPath}`, + }; + } +} diff --git a/plugins/catalog-backend-module-gitea/src/providers/config.test.ts b/plugins/catalog-backend-module-gitea/src/providers/config.test.ts new file mode 100644 index 0000000000..5efece4e5a --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/config.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright 2022 The Backstage 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 { readGiteaConfigs } from './config'; +import { ConfigReader } from '@backstage/config'; +import { readSchedulerServiceTaskScheduleDefinitionFromConfig } from '@backstage/backend-plugin-api'; + +jest.mock('@backstage/backend-plugin-api', () => ({ + ...jest.requireActual('@backstage/backend-plugin-api'), + readSchedulerServiceTaskScheduleDefinitionFromConfig: jest.fn(), +})); +jest.mock('p-limit'); + +describe('readGiteaConfigs', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('returns empty array when no gitea provider config is present', () => { + const config = new ConfigReader({}); + + const result = readGiteaConfigs(config); + + expect(result).toEqual([]); + }); + + it('parses multiple gitea provider configurations correctly', () => { + ( + readSchedulerServiceTaskScheduleDefinitionFromConfig as jest.Mock + ).mockReturnValue({ + frequency: { minutes: 5 }, + timeout: { minutes: 3 }, + }); + + const config = new ConfigReader({ + catalog: { + providers: { + gitea: { + myProvider: { + host: 'gitea.example.com', + organization: 'example-org', + branch: 'dev', + catalogPath: 'custom-path.yaml', + schedule: { + frequency: { minutes: 5 }, + timeout: { minutes: 3 }, + }, + }, + }, + }, + }, + }); + + const result = readGiteaConfigs(config); + + expect(result).toEqual([ + { + id: 'myProvider', + host: 'gitea.example.com', + organization: 'example-org', + branch: 'dev', + catalogPath: 'custom-path.yaml', + schedule: { + frequency: { minutes: 5 }, + timeout: { minutes: 3 }, + }, + }, + ]); + }); + + it('applies defaults for optional fields', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitea: { + default: { + host: 'gitea.com', + organization: 'default-org', + }, + }, + }, + }, + }); + + const result = readGiteaConfigs(config); + + expect(result[0]).toMatchObject({ + id: 'default', + host: 'gitea.com', + organization: 'default-org', + branch: 'main', + catalogPath: 'catalog-info.yaml', + schedule: undefined, + }); + }); + + it('throws if required fields are missing', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitea: { + bad: {}, + }, + }, + }, + }); + + expect(() => readGiteaConfigs(config)).toThrow( + /Missing required config value/, + ); + }); +}); diff --git a/plugins/catalog-backend-module-gitea/src/providers/config.ts b/plugins/catalog-backend-module-gitea/src/providers/config.ts new file mode 100644 index 0000000000..5863e76bf0 --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/config.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2022 The Backstage 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 { readSchedulerServiceTaskScheduleDefinitionFromConfig } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { GiteaProviderConfig } from './types'; + +function readGiteaConfig(id: string, config: Config): GiteaProviderConfig { + const branch = config.getOptionalString('branch') ?? 'main'; + const catalogPath = + config.getOptionalString('catalogPath') ?? 'catalog-info.yaml'; + const host = config.getString('host'); + const organization = config.getString('organization'); + + const schedule = config.has('schedule') + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + config.getConfig('schedule'), + ) + : undefined; + + return { + branch, + catalogPath, + host, + id, + schedule, + organization, + }; +} + +export function readGiteaConfigs(config: Config): GiteaProviderConfig[] { + const configs: GiteaProviderConfig[] = []; + + const providerConfigs = config.getOptionalConfig('catalog.providers.gitea'); + + if (!providerConfigs) { + return configs; + } + + for (const id of providerConfigs.keys()) { + configs.push(readGiteaConfig(id, providerConfigs.getConfig(id))); + } + + return configs; +} diff --git a/plugins/catalog-backend-module-gitea/src/providers/core.test.ts b/plugins/catalog-backend-module-gitea/src/providers/core.test.ts new file mode 100644 index 0000000000..883dab7edc --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/core.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage 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 { getGiteaApiUrl } from './core'; +import { GiteaIntegrationConfig } from '@backstage/integration'; + +describe('getGiteaApiUrl', () => { + it('should return the correct Gitea API base URL', () => { + const config: GiteaIntegrationConfig = { + host: 'gitea.example.com', + baseUrl: 'https://gitea.example.com', + }; + + const result = getGiteaApiUrl(config); + + expect(result).toBe('https://gitea.example.com/api/v1/'); + }); + + it('should handle trailing slash in baseUrl correctly', () => { + const config: GiteaIntegrationConfig = { + host: 'gitea.example.com', + baseUrl: 'https://gitea.example.com/', + }; + + const result = getGiteaApiUrl(config); + + expect(result).toBe('https://gitea.example.com//api/v1/'); + }); +}); diff --git a/plugins/catalog-backend-module-gitea/src/providers/core.ts b/plugins/catalog-backend-module-gitea/src/providers/core.ts new file mode 100644 index 0000000000..c994f3a714 --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/core.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2022 The Backstage 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 { GiteaIntegrationConfig } from '@backstage/integration'; +/** + * Return the url to query available repos using the Gitea API. + * + * @param config - A Gitea provider config. + * @public + */ +export function getGiteaApiUrl(config: GiteaIntegrationConfig) { + return `${config.baseUrl}/api/v1/`; +} diff --git a/plugins/catalog-backend-module-gitea/src/providers/index.ts b/plugins/catalog-backend-module-gitea/src/providers/index.ts new file mode 100644 index 0000000000..9914fda397 --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage 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 { getGiteaApiUrl } from './core'; diff --git a/plugins/catalog-backend-module-gitea/src/providers/types.ts b/plugins/catalog-backend-module-gitea/src/providers/types.ts new file mode 100644 index 0000000000..0bbd09f9e7 --- /dev/null +++ b/plugins/catalog-backend-module-gitea/src/providers/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2022 The Backstage 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 { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; + +export type GiteaProjectInfo = { + id: string; + name: string; + parent?: string; + state?: string; + branch?: string; +}; + +export type GiteaProjectQueryResult = Record; + +export type GiteaProviderConfig = { + host: string; + id: string; + branch?: string; + catalogPath?: string; + organization?: string; + schedule?: SchedulerServiceTaskScheduleDefinition; +}; diff --git a/yarn.lock b/yarn.lock index f700a3af06..6fa02a3a52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5942,6 +5942,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-catalog-backend-module-gitea@workspace:plugins/catalog-backend-module-gitea": + version: 0.0.0-use.local + resolution: "@backstage/plugin-catalog-backend-module-gitea@workspace:plugins/catalog-backend-module-gitea" + dependencies: + "@backstage/backend-common": "npm:^0.25.0" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/integration": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" + p-limit: "npm:^3.0.2" + uuid: "npm:^11.0.0" + languageName: unknown + linkType: soft + "@backstage/plugin-catalog-backend-module-github-org@workspace:plugins/catalog-backend-module-github-org": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-github-org@workspace:plugins/catalog-backend-module-github-org" From 704d94ba03ca9d35ad453379bc7edfe5262d1925 Mon Sep 17 00:00:00 2001 From: Vinnie McGuinness Date: Mon, 12 May 2025 11:18:20 +0100 Subject: [PATCH 2/2] Resloving initial feadback comments Signed-off-by: Vinnie McGuinness --- .github/CODEOWNERS | 2 +- docs/integrations/gitea/discovery.md | 5 -- .../catalog-backend-module-gitea/config.d.ts | 58 ++++++++++++++++ .../catalog-backend-module-gitea/package.json | 4 +- .../src/providers/GiteaEntityProvider.test.ts | 66 +------------------ .../src/providers/GiteaEntityProvider.ts | 8 +-- yarn.lock | 1 - 7 files changed, 67 insertions(+), 77 deletions(-) create mode 100644 plugins/catalog-backend-module-gitea/config.d.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f5c6f49617..16d55c29dd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -51,7 +51,7 @@ yarn.lock @backstage/maintainers @backst /plugins/catalog-backend-module-aws @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann /plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann /plugins/catalog-backend-module-backstage-openapi @backstage/maintainers @backstage/reviewers @backstage/openapi-tooling-maintainers -/plugins/catalog-backend-module-gitea @backstage/maintainers @backstage/reviewers @vinnie-jog-on +/plugins/catalog-backend-module-gitea @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers /plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann /plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers /plugins/catalog-graph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @backstage/sda-se-reviewers diff --git a/docs/integrations/gitea/discovery.md b/docs/integrations/gitea/discovery.md index c01ea52edf..6cae169b58 100644 --- a/docs/integrations/gitea/discovery.md +++ b/docs/integrations/gitea/discovery.md @@ -2,14 +2,9 @@ id: discovery title: Gitea Discovery sidebar_label: Discovery -# prettier-ignore description: Automatically discovering catalog entities from Gitea repositories --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). -::: - The Gitea integration has a special entity provider for discovering catalog entities from Gitea repositories. The provider uses the "List Projects" API in Gitea to get a list of repositories and will automatically ingest all `catalog-info.yaml` files diff --git a/plugins/catalog-backend-module-gitea/config.d.ts b/plugins/catalog-backend-module-gitea/config.d.ts new file mode 100644 index 0000000000..bbb1a0ee4f --- /dev/null +++ b/plugins/catalog-backend-module-gitea/config.d.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2022 The Backstage 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 { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; + +export interface Config { + catalog?: { + /** + * List of provider-specific options and attributes + */ + providers?: { + /** + * GiteaEntityProvider configuration + * + * Maps provider id with configuration. + */ + gitea?: { + [name: string]: { + /** + * (Required) The host of the Gitea integration to use. + */ + host: string; + /** + * (Required) Name of your organization account/workspace. + */ + organization: string; + /** + * (Optional) Branch. + * The branch where the provider will try to find entities. Uses the default to "main". + */ + branch?: string; + /** + * (Optional) Path where the catalog YAML manifest file is expected in the repository. + * Defaults to "catalog-info.yaml". + */ + catalogPath?: string; + /** + * (Optional) TaskScheduleDefinition for the discovery. + */ + schedule?: SchedulerServiceTaskScheduleDefinition; + }; + }; + }; + }; +} diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json index fbba66a039..b35f0b6b77 100644 --- a/plugins/catalog-backend-module-gitea/package.json +++ b/plugins/catalog-backend-module-gitea/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gitea", - "version": "0.1.0", + "version": "0.0.0", "license": "Apache-2.0", - "private": true, "description": "The gitea backend module for the catalog plugin.", "main": "src/index.ts", "types": "src/index.ts", @@ -41,7 +40,6 @@ "uuid": "^11.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.25.0", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^" }, diff --git a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts index c17b3bc20e..d6a31899f1 100644 --- a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { GiteaEntityProvider } from './GiteaEntityProvider'; @@ -28,7 +27,8 @@ jest.mock('./core'); jest.mock('uuid'); describe('GiteaEntityProvider', () => { - const logger = getVoidLogger() as unknown as LoggerService; + const logger = mockServices.logger.mock(); + const mockScheduler = { createScheduledTaskRunner: jest.fn(), triggerTask: jest.fn(), @@ -285,66 +285,6 @@ describe('GiteaEntityProvider', () => { fn: expect.any(Function), }); }); - - it('should run the scheduled refresh function', async () => { - await provider.connect(mockConnection); - - // Extract and call the scheduled function - const runArgument = mockTaskRunner.run.mock.calls[0][0]; - const refreshFn = runArgument.fn; - - // Mock the refresh behavior - (global.fetch as jest.Mock).mockImplementation(async (url: string) => { - if (url.includes('/orgs/test-org/repos')) { - if (url.includes('page=1')) { - return { - ok: true, - json: async () => [ - { - name: 'repo1', - html_url: 'https://gitea.example.com/test-org/repo1', - empty: false, - }, - { - name: 'repo2', - html_url: 'https://gitea.example.com/test-org/repo2', - empty: false, - }, - ], - }; - } - return { - ok: true, - json: async () => [], - }; - } else if (url.includes('/contents/catalog-info.yaml')) { - if (url.includes('repo1')) { - return { ok: true }; - } - } - return { ok: false }; - }); - - const abortController = new AbortController(); - await refreshFn(abortController.signal); - - expect(mockConnection.applyMutation).toHaveBeenCalledWith({ - type: 'full', - entities: [ - { - locationKey: 'gitea-provider:test-provider', - entity: expect.objectContaining({ - metadata: expect.objectContaining({ - annotations: expect.objectContaining({ - 'backstage.io/managed-by-location': - 'url:https://gitea.example.com/test-org/repo1/src/branch/main/catalog-info.yaml', - }), - }), - }), - }, - ], - }); - }); }); describe('refresh', () => { diff --git a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts index 7097edf2ca..2f9475af26 100644 --- a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts +++ b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.ts @@ -164,7 +164,7 @@ export class GiteaEntityProvider implements EntityProvider { let allRepos: any[] = []; try { - // Step 1: Fetch all repos for an organization + // Fetch all repos for an organization const OrgRepoApiUrl = `${getGiteaApiUrl(this.integration.config)}orgs/${ this.config.organization }/repos`; @@ -201,7 +201,7 @@ export class GiteaEntityProvider implements EntityProvider { ); } - // Step 2: Filter for repos that have catalog-info.yaml at root + // Filter for repos that have catalog-info.yaml at root const { default: pLimit } = await import('p-limit'); const limit = pLimit(5); const validRepos: string[] = []; @@ -234,11 +234,11 @@ export class GiteaEntityProvider implements EntityProvider { }), ), ); - // Step 3: create location specs for each valid repo + // Create location specs for each valid repo const locations = await Promise.all( validRepos.map(repo => limit(() => this.createLocationSpec(repo))), ); - // Step 4: Apply the locations to the catalog + // Apply the locations to the catalog try { const providerName: string = this.getProviderName(); const entities = locations.map(location => ({ diff --git a/yarn.lock b/yarn.lock index 6fa02a3a52..8ed49c2325 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5946,7 +5946,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-gitea@workspace:plugins/catalog-backend-module-gitea" dependencies: - "@backstage/backend-common": "npm:^0.25.0" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^"