cli: add template for catalog provider

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-10-17 13:45:27 +02:00
parent 43b9f8fd90
commit fc7cbfced9
18 changed files with 353 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The templates executed with the `yarn new` command now supports templating filenames.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Added a template for the `yarn new` command to create an catalog entity provider. To add this template to an explicit list in the root `package.json`, use `@backstage/cli/templates/catalog-provider-module`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Added the new `@backstage/cli/templates/catalog-provider-module` template to the explicit template configuration for the `next-app` template.
@@ -979,6 +979,7 @@ When creating a new Backstage app with `create-app` and using the `--next` flag
"@backstage/cli/templates/plugin-common-library",
"@backstage/cli/templates/web-library",
"@backstage/cli/templates/node-library",
"@backstage/cli/templates/catalog-provider-module",
"@backstage/cli/templates/scaffolder-backend-module"
]
}
+1
View File
@@ -90,6 +90,7 @@ When defining the `templates` array it will override the default set of template
"@backstage/cli/templates/plugin-common-library",
"@backstage/cli/templates/web-library",
"@backstage/cli/templates/node-library",
"@backstage/cli/templates/catalog-provider-module",
"@backstage/cli/templates/scaffolder-backend-module"
]
}
@@ -23,5 +23,6 @@ export const defaultTemplates = [
'@backstage/cli/templates/plugin-common-library',
'@backstage/cli/templates/web-library',
'@backstage/cli/templates/node-library',
'@backstage/cli/templates/catalog-provider-module',
'@backstage/cli/templates/scaffolder-backend-module',
];
@@ -22,6 +22,7 @@ import { PortableTemplate, PortableTemplateInput } from '../types';
import { ForwardedError, InputError } from '@backstage/errors';
import { isMonoRepo as getIsMonoRepo } from '@backstage/cli-node';
import { PortableTemplater } from './PortableTemplater';
import { isChildPath } from '@backstage/cli-common';
export async function writeTemplateContents(
template: PortableTemplate,
@@ -63,7 +64,12 @@ export async function writeTemplateContents(
}
for (const file of template.files) {
const destPath = resolvePath(targetDir, file.path);
const destPath = resolvePath(targetDir, templater.template(file.path));
if (!isChildPath(targetDir, destPath)) {
throw new Error(
`Path ${destPath} is outside of target directory ${targetDir}`,
);
}
await fs.ensureDir(dirname(destPath));
let content =
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,5 @@
# {{packageName}}
The {{fullModuleId}} module for [@backstage/plugin-catalog-backend](https://www.npmjs.com/package/@backstage/plugin-catalog-backend).
_This plugin was created through the Backstage CLI_
@@ -0,0 +1,34 @@
import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
export interface Config {
catalog?: {
providers?: {
/**
* {{providerClass}} configuration.
*/
{{providerVar}}?:
| {
/**
* The target that this provider should consume.
*/
target: string;
/**
* Overrides the schedule at which this provider runs.
*/
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
}
| {
[name: string]: {
/**
* The target that this provider should consume.
*/
target: string;
/**
* Overrides the schedule at which this provider runs.
*/
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
};
};
};
};
}
@@ -0,0 +1,36 @@
{
"name": "{{packageName}}",
"description": "The {{fullModuleId}} module for @backstage/plugin-catalog-backend",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"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": "{{versionQuery '@backstage/backend-plugin-api'}}",
"@backstage/plugin-catalog-node": "{{versionQuery '@backstage/plugin-catalog-node'}}"
},
"devDependencies": {
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}"
},
"files": [
"dist"
]
}
@@ -0,0 +1,9 @@
name: catalog-provider-module
role: backend-plugin-module
description: An Entity Provider module for the Software Catalog
values:
pluginId: catalog
fullModuleId: '{{ moduleId }}-provider'
moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}'
providerVar: '{{ camelCase moduleId }}Provider'
providerClass: '{{ upperFirst ( camelCase moduleId ) }}Provider'
@@ -0,0 +1,8 @@
/***/
/**
* The {{fullModuleId}} module for @backstage/plugin-catalog-backend
*
* @packageDocumentation
*/
export { {{moduleVar}} as default } from './module';
@@ -0,0 +1,29 @@
import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { {{providerClass}} } from './provider/{{providerClass}}';
export const {{moduleVar}} = createBackendModule({
moduleId: '{{fullModuleId}}',
pluginId: '{{pluginId}}',
register({ registerInit }) {
registerInit({
deps: {
logger: coreServices.logger,
config: coreServices.rootConfig,
scheduler: coreServices.scheduler,
processing: catalogProcessingExtensionPoint,
},
async init({ logger, scheduler, config, processing }) {
processing.addEntityProvider(
{{providerClass}}.fromConfig(config, {
logger,
scheduler,
}),
);
}
});
},
})
@@ -0,0 +1,78 @@
import {
readSchedulerServiceTaskScheduleDefinitionFromConfig,
SchedulerServiceTaskScheduleDefinition,
} from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
const DEFAULT_PROVIDER_ID = 'default';
const DEFAULT_SCHEDULE: SchedulerServiceTaskScheduleDefinition = {
frequency: {
minutes: 30,
},
timeout: {
minutes: 3,
},
}
export type {{providerClass}}ProviderConfig = {
id: string;
target: string;
schedule: SchedulerServiceTaskScheduleDefinition;
}
/**
* Parses all configured providers.
*
* @param config - The root of the provider config hierarchy
*
* @public
*/
export function readProviderConfigs(
config: Config,
): {{providerClass}}ProviderConfig[] {
const providersConfig = config.getOptionalConfig(
'catalog.providers.{{providerVar}}',
);
if (!providersConfig) {
return [];
}
if ((providersConfig).has('target')) {
// simple/single config variant
return [readProviderConfig(DEFAULT_PROVIDER_ID, providersConfig)];
}
return providersConfig.keys().map(id => {
const providerConfig = providersConfig.getConfig(id);
return readProviderConfig(id, providerConfig);
});
}
/**
* Parses a single configured provider by id.
*
* @param id - the id of the provider to parse
* @param config - The root of the provider config hierarchy
*
* @public
*/
export function readProviderConfig(
id: string,
config: Config,
): {{providerClass}}ProviderConfig {
const target = config.getString('target');
const schedule = config.has('schedule')
? readSchedulerServiceTaskScheduleDefinitionFromConfig(
config.getConfig('schedule'),
)
: DEFAULT_SCHEDULE;
return {
id,
target,
schedule,
};
}
@@ -0,0 +1,18 @@
import { {{providerClass}} } from './{{providerClass}}';
import { mockServices } from '@backstage/backend-test-utils';
describe('{{providerClass}}', () => {
it('should read entities from the target', async () => {
const logger = mockServices.logger.mock();
const provider = new {{providerClass}}({
id: 'test',
target: 'https://example.com',
logger: mockServices.logger.mock(),
taskRunner: { run: jest.fn() },
});
const entities = await provider.read({ logger });
expect(entities).toEqual([]);
});
})
@@ -0,0 +1,109 @@
import { Config } from '@backstage/config';
import {
DeferredEntity,
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import * as uuid from 'uuid';
import { readProviderConfigs } from './readProviderConfigs';
import {
LoggerService,
SchedulerService,
SchedulerServiceTaskRunner,
} from '@backstage/backend-plugin-api';
export type {{providerClass}}Options = {
/**
* The logger to use.
*/
logger: LoggerService;
/**
* Scheduler used to schedule refreshes based on
* the schedule config.
*/
scheduler: SchedulerService;
};
export class {{providerClass}} implements EntityProvider {
static fromConfig(
configRoot: Config,
options: {{providerClass}}Options,
): {{providerClass}}[] {
return readProviderConfigs(configRoot).map(providerConfig => {
return new {{providerClass}}({
id: providerConfig.id,
target: providerConfig.target,
logger: options.logger,
taskRunner: options.scheduler.createScheduledTaskRunner(
providerConfig.schedule,
),
});
});
}
readonly #id: string;
readonly #target: string;
readonly #logger: LoggerService;
readonly #taskRunner: SchedulerServiceTaskRunner;
constructor(options: {
id: string;
target: string;
logger: LoggerService;
taskRunner: SchedulerServiceTaskRunner;
}) {
this.#id = options.id;
this.#target = options.target;
this.#logger = options.logger;
this.#taskRunner = options.taskRunner;
}
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.getProviderName} */
getProviderName() {
return `{{providerClass}}:${this.#id}`;
}
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.connect} */
async connect(connection: EntityProviderConnection) {
const id = `${this.getProviderName()}:refresh`;
// Schedule a refresh task to be run periodically
await this.#taskRunner.run({
id,
fn: async () => {
const logger = this.#logger.child({
taskId: id,
taskInstanceId: uuid.v4(),
});
try {
const entities = await this.read({ logger });
logger.info(`Read ${entities.length} entities`);
await connection.applyMutation({
type: 'full',
entities,
});
} catch (error) {
logger.error(`Refresh failed`, error);
}
},
});
}
/**
* Reads entities to be added to the catalog.
*/
async read(options: { logger: LoggerService }): Promise<DeferredEntity[]> {
const { logger } = options;
logger.info(`Reading entities from ${this.#target}`);
// TODO: Implement entity reading logic from the target
const entities: DeferredEntity[] = [];
return entities;
}
}
@@ -38,6 +38,7 @@
"@backstage/cli/templates/plugin-common-library",
"@backstage/cli/templates/web-library",
"@backstage/cli/templates/node-library",
"@backstage/cli/templates/catalog-provider-module",
"@backstage/cli/templates/scaffolder-backend-module"
]
}