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:
Fredrik Adelöw
2024-02-17 16:15:55 +01:00
committed by GitHub
9 changed files with 221 additions and 2 deletions
+33
View File
@@ -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))
},
});
},
});
```
+5
View File
@@ -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();
+9
View File
@@ -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)
+2
View File
@@ -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';
+19 -1
View File
@@ -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
*/