feat(catalog-backend-module-gitea) add new Gitea provider module
Signed-off-by: Vinnie McGuinness <vinnie.mcguinness@intive.com> feat(catalog-backend-module-gitea) add new Gitea provider module Signed-off-by: Vinnie McGuinness <vinnie.mcguinness@intive.com> adding tests
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-gitea': minor
|
||||
---
|
||||
|
||||
add new gitea provider module
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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<void>;
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
configRoot: Config,
|
||||
options: {
|
||||
logger: LoggerService;
|
||||
schedule?: SchedulerServiceTaskRunner;
|
||||
scheduler?: SchedulerService;
|
||||
},
|
||||
): GiteaEntityProvider[];
|
||||
// (undocumented)
|
||||
getProviderName(): string;
|
||||
// (undocumented)
|
||||
refresh(logger: LoggerService): Promise<void>;
|
||||
}
|
||||
```
|
||||
@@ -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';
|
||||
@@ -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<GiteaEntityProvider> | 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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<void>;
|
||||
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<void> {
|
||||
this.connection = connection;
|
||||
await this.scheduleFn();
|
||||
}
|
||||
|
||||
private async parseGiteaJsonResponse(
|
||||
response: Response,
|
||||
): Promise<GiteaProjectQueryResult> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<LocationSpec> {
|
||||
return {
|
||||
type: 'url',
|
||||
target: `${repo}/src/branch/${this.config.branch}/${this.config.catalogPath}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/');
|
||||
});
|
||||
});
|
||||
@@ -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/`;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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<string, GiteaProjectInfo>;
|
||||
|
||||
export type GiteaProviderConfig = {
|
||||
host: string;
|
||||
id: string;
|
||||
branch?: string;
|
||||
catalogPath?: string;
|
||||
organization?: string;
|
||||
schedule?: SchedulerServiceTaskScheduleDefinition;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user