Merge pull request #22568 from sennyeya/catalog-entity-policies
feat(catalog): add support for entity policies in the new backend.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
'@backstage/plugin-catalog-node': minor
|
||||
---
|
||||
|
||||
Adds support for supplying field validators to the new backend's catalog plugin. If you're using entity policies, you should use the new `transformLegacyPolicyToProcessor` function to install them as processors instead.
|
||||
|
||||
```ts
|
||||
import {
|
||||
catalogProcessingExtensionPoint,
|
||||
catalogModelExtensionPoint,
|
||||
} from '@backstage/plugin-catalog-node/alpha';
|
||||
import {myPolicy} from './my-policy';
|
||||
|
||||
export const catalogModulePolicyProvider = createBackendModule({
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'internal-policy-provider',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
modelExtensions: catalogModelExtensionPoint,
|
||||
processingExtensions: catalogProcessingExtensionPoint,
|
||||
},
|
||||
async init({ modelExtensions, processingExtensions }) {
|
||||
modelExtensions.setFieldValidators({
|
||||
...
|
||||
});
|
||||
processingExtensions.addProcessors(transformLegacyPolicyToProcessor(myPolicy))
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
Generated
+5
@@ -450,6 +450,11 @@ export const processingResult: Readonly<{
|
||||
// @public @deprecated (undocumented)
|
||||
export type ScmLocationAnalyzer = ScmLocationAnalyzer_2;
|
||||
|
||||
// @public
|
||||
export function transformLegacyPolicyToProcessor(
|
||||
policy: EntityPolicy,
|
||||
): CatalogProcessor_2;
|
||||
|
||||
// @public (undocumented)
|
||||
export class UrlReaderProcessor implements CatalogProcessor_2 {
|
||||
constructor(options: { reader: UrlReader; logger: Logger });
|
||||
|
||||
@@ -24,3 +24,4 @@ export { PlaceholderProcessor } from './PlaceholderProcessor';
|
||||
export type { PlaceholderProcessorOptions } from './PlaceholderProcessor';
|
||||
export { UrlReaderProcessor } from './UrlReaderProcessor';
|
||||
export { parseEntityYaml } from '../util/parse';
|
||||
export { transformLegacyPolicyToProcessor } from './transformLegacyPolicyToProcessor';
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Entity, EntityPolicy } from '@backstage/catalog-model';
|
||||
import { transformLegacyPolicyToProcessor } from './transformLegacyPolicyToProcessor';
|
||||
import { clone } from 'lodash';
|
||||
|
||||
describe('transformLegacyPolicyToProcessor', () => {
|
||||
const entityToProcess: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
},
|
||||
};
|
||||
it('modifies the entity if the policy modifies the entity', async () => {
|
||||
const policy: EntityPolicy = {
|
||||
async enforce(entity) {
|
||||
entity.kind = 'Group';
|
||||
return entity;
|
||||
},
|
||||
};
|
||||
const processor = transformLegacyPolicyToProcessor(policy);
|
||||
const clonedEntity = clone(entityToProcess);
|
||||
const entity = await processor.preProcessEntity?.(
|
||||
clonedEntity,
|
||||
{} as any,
|
||||
jest.fn(),
|
||||
{} as any,
|
||||
{} as any,
|
||||
);
|
||||
expect(entity).toBeTruthy();
|
||||
expect(entity?.kind).toBe('Group');
|
||||
expect(entity?.apiVersion).toBe('backstage.io/v1alpha');
|
||||
expect(entity?.metadata.name).toBe('test');
|
||||
});
|
||||
|
||||
it('does not modify the entity if the policy returns undefined', async () => {
|
||||
const policy: EntityPolicy = {
|
||||
async enforce() {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
const processor = transformLegacyPolicyToProcessor(policy);
|
||||
const clonedEntity = clone(entityToProcess);
|
||||
const entity = await processor.preProcessEntity?.(
|
||||
clonedEntity,
|
||||
{} as any,
|
||||
jest.fn(),
|
||||
{} as any,
|
||||
{} as any,
|
||||
);
|
||||
expect(entity).toBeTruthy();
|
||||
expect(entity?.kind).toBe('Component');
|
||||
expect(entity?.apiVersion).toBe('backstage.io/v1alpha');
|
||||
expect(entity?.metadata.name).toBe('test');
|
||||
});
|
||||
|
||||
it('bubbles up processor error', async () => {
|
||||
const policy: EntityPolicy = {
|
||||
async enforce() {
|
||||
throw new TypeError('Invalid value for metadata.name');
|
||||
},
|
||||
};
|
||||
const processor = transformLegacyPolicyToProcessor(policy);
|
||||
const clonedEntity = clone(entityToProcess);
|
||||
await expect(
|
||||
processor.preProcessEntity?.(
|
||||
clonedEntity,
|
||||
{} as any,
|
||||
jest.fn(),
|
||||
{} as any,
|
||||
{} as any,
|
||||
),
|
||||
).rejects.toThrow(/Invalid value for metadata.name/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2024 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 { EntityPolicy } from '@backstage/catalog-model';
|
||||
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
|
||||
|
||||
/**
|
||||
* Transform a given entity policy to an entity processor.
|
||||
* @param policy - The policy to transform
|
||||
* @returns A new entity processor that uses the entity policy.
|
||||
* @public
|
||||
*/
|
||||
export function transformLegacyPolicyToProcessor(
|
||||
policy: EntityPolicy,
|
||||
): CatalogProcessor {
|
||||
return {
|
||||
getProcessorName() {
|
||||
return policy.constructor.name;
|
||||
},
|
||||
async preProcessEntity(entity) {
|
||||
// If enforcing the policy fails, throw the policy error.
|
||||
const result = await policy.enforce(entity);
|
||||
if (!result) {
|
||||
return entity;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
createBackendPlugin,
|
||||
coreServices,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, Validators } from '@backstage/catalog-model';
|
||||
import { CatalogBuilder, CatalogPermissionRuleInput } from './CatalogBuilder';
|
||||
import {
|
||||
CatalogAnalysisExtensionPoint,
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
catalogProcessingExtensionPoint,
|
||||
CatalogPermissionExtensionPoint,
|
||||
catalogPermissionExtensionPoint,
|
||||
CatalogModelExtensionPoint,
|
||||
catalogModelExtensionPoint,
|
||||
} from '@backstage/plugin-catalog-node/alpha';
|
||||
import {
|
||||
CatalogProcessor,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
ScmLocationAnalyzer,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
class CatalogProcessingExtensionPointImpl
|
||||
implements CatalogProcessingExtensionPoint
|
||||
@@ -124,6 +127,18 @@ class CatalogPermissionExtensionPointImpl
|
||||
}
|
||||
}
|
||||
|
||||
class CatalogModelExtensionPointImpl implements CatalogModelExtensionPoint {
|
||||
#fieldValidators: Partial<Validators> = {};
|
||||
|
||||
setFieldValidators(validators: Partial<Validators>): void {
|
||||
merge(this.#fieldValidators, validators);
|
||||
}
|
||||
|
||||
get fieldValidators() {
|
||||
return this.#fieldValidators;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog plugin
|
||||
* @alpha
|
||||
@@ -150,6 +165,9 @@ export const catalogPlugin = createBackendPlugin({
|
||||
permissionExtensions,
|
||||
);
|
||||
|
||||
const modelExtensions = new CatalogModelExtensionPointImpl();
|
||||
env.registerExtensionPoint(catalogModelExtensionPoint, modelExtensions);
|
||||
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
@@ -192,6 +210,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
);
|
||||
builder.addLocationAnalyzers(...analysisExtensions.locationAnalyzers);
|
||||
builder.addPermissionRules(...permissionExtensions.permissionRules);
|
||||
builder.setFieldFormatValidators(modelExtensions.fieldValidators);
|
||||
|
||||
const { processingEngine, router } = await builder.build();
|
||||
|
||||
|
||||
Generated
+9
@@ -14,6 +14,7 @@ import { PermissionRuleParams } from '@backstage/plugin-permission-common';
|
||||
import { PlaceholderResolver } from '@backstage/plugin-catalog-node';
|
||||
import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-node';
|
||||
import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
import { Validators } from '@backstage/catalog-model';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface CatalogAnalysisExtensionPoint {
|
||||
@@ -24,6 +25,14 @@ export interface CatalogAnalysisExtensionPoint {
|
||||
// @alpha (undocumented)
|
||||
export const catalogAnalysisExtensionPoint: ExtensionPoint<CatalogAnalysisExtensionPoint>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface CatalogModelExtensionPoint {
|
||||
setFieldValidators(validators: Partial<Validators>): void;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const catalogModelExtensionPoint: ExtensionPoint<CatalogModelExtensionPoint>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface CatalogPermissionExtensionPoint {
|
||||
// (undocumented)
|
||||
|
||||
@@ -22,3 +22,5 @@ export { catalogAnalysisExtensionPoint } from './extensions';
|
||||
export type { CatalogPermissionRuleInput } from './extensions';
|
||||
export type { CatalogPermissionExtensionPoint } from './extensions';
|
||||
export { catalogPermissionExtensionPoint } from './extensions';
|
||||
export type { CatalogModelExtensionPoint } from './extensions';
|
||||
export { catalogModelExtensionPoint } from './extensions';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { createExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, Validators } from '@backstage/catalog-model';
|
||||
import {
|
||||
CatalogProcessor,
|
||||
EntitiesSearchFilter,
|
||||
@@ -45,6 +45,18 @@ export interface CatalogProcessingExtensionPoint {
|
||||
): void;
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export interface CatalogModelExtensionPoint {
|
||||
/**
|
||||
* Sets the validator function to use for one or more special fields of an
|
||||
* entity. This is useful if the default rules for formatting of fields are
|
||||
* not sufficient.
|
||||
*
|
||||
* @param validators - The (subset of) validators to set
|
||||
*/
|
||||
setFieldValidators(validators: Partial<Validators>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
@@ -68,6 +80,12 @@ export const catalogAnalysisExtensionPoint =
|
||||
id: 'catalog.analysis',
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export const catalogModelExtensionPoint =
|
||||
createExtensionPoint<CatalogModelExtensionPoint>({
|
||||
id: 'catalog.model',
|
||||
});
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user