Skip to content

Commit 25adbdd

Browse files
authored
Merge pull request #22568 from sennyeya/catalog-entity-policies
feat(catalog): add support for entity policies in the new backend.
2 parents b1789a2 + 5182f5b commit 25adbdd

File tree

9 files changed

+221
-2
lines changed

9 files changed

+221
-2
lines changed

.changeset/thirty-bags-try.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@backstage/plugin-catalog-backend': minor
3+
'@backstage/plugin-catalog-node': minor
4+
---
5+
6+
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.
7+
8+
```ts
9+
import {
10+
catalogProcessingExtensionPoint,
11+
catalogModelExtensionPoint,
12+
} from '@backstage/plugin-catalog-node/alpha';
13+
import {myPolicy} from './my-policy';
14+
15+
export const catalogModulePolicyProvider = createBackendModule({
16+
pluginId: 'catalog',
17+
moduleId: 'internal-policy-provider',
18+
register(reg) {
19+
reg.registerInit({
20+
deps: {
21+
modelExtensions: catalogModelExtensionPoint,
22+
processingExtensions: catalogProcessingExtensionPoint,
23+
},
24+
async init({ modelExtensions, processingExtensions }) {
25+
modelExtensions.setFieldValidators({
26+
...
27+
});
28+
processingExtensions.addProcessors(transformLegacyPolicyToProcessor(myPolicy))
29+
},
30+
});
31+
},
32+
});
33+
```

plugins/catalog-backend/api-report.md

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/catalog-backend/src/modules/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export { PlaceholderProcessor } from './PlaceholderProcessor';
2424
export type { PlaceholderProcessorOptions } from './PlaceholderProcessor';
2525
export { UrlReaderProcessor } from './UrlReaderProcessor';
2626
export { parseEntityYaml } from '../util/parse';
27+
export { transformLegacyPolicyToProcessor } from './transformLegacyPolicyToProcessor';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2024 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Entity, EntityPolicy } from '@backstage/catalog-model';
18+
import { transformLegacyPolicyToProcessor } from './transformLegacyPolicyToProcessor';
19+
import { clone } from 'lodash';
20+
21+
describe('transformLegacyPolicyToProcessor', () => {
22+
const entityToProcess: Entity = {
23+
apiVersion: 'backstage.io/v1alpha',
24+
kind: 'Component',
25+
metadata: {
26+
name: 'test',
27+
},
28+
};
29+
it('modifies the entity if the policy modifies the entity', async () => {
30+
const policy: EntityPolicy = {
31+
async enforce(entity) {
32+
entity.kind = 'Group';
33+
return entity;
34+
},
35+
};
36+
const processor = transformLegacyPolicyToProcessor(policy);
37+
const clonedEntity = clone(entityToProcess);
38+
const entity = await processor.preProcessEntity?.(
39+
clonedEntity,
40+
{} as any,
41+
jest.fn(),
42+
{} as any,
43+
{} as any,
44+
);
45+
expect(entity).toBeTruthy();
46+
expect(entity?.kind).toBe('Group');
47+
expect(entity?.apiVersion).toBe('backstage.io/v1alpha');
48+
expect(entity?.metadata.name).toBe('test');
49+
});
50+
51+
it('does not modify the entity if the policy returns undefined', async () => {
52+
const policy: EntityPolicy = {
53+
async enforce() {
54+
return undefined;
55+
},
56+
};
57+
const processor = transformLegacyPolicyToProcessor(policy);
58+
const clonedEntity = clone(entityToProcess);
59+
const entity = await processor.preProcessEntity?.(
60+
clonedEntity,
61+
{} as any,
62+
jest.fn(),
63+
{} as any,
64+
{} as any,
65+
);
66+
expect(entity).toBeTruthy();
67+
expect(entity?.kind).toBe('Component');
68+
expect(entity?.apiVersion).toBe('backstage.io/v1alpha');
69+
expect(entity?.metadata.name).toBe('test');
70+
});
71+
72+
it('bubbles up processor error', async () => {
73+
const policy: EntityPolicy = {
74+
async enforce() {
75+
throw new TypeError('Invalid value for metadata.name');
76+
},
77+
};
78+
const processor = transformLegacyPolicyToProcessor(policy);
79+
const clonedEntity = clone(entityToProcess);
80+
await expect(
81+
processor.preProcessEntity?.(
82+
clonedEntity,
83+
{} as any,
84+
jest.fn(),
85+
{} as any,
86+
{} as any,
87+
),
88+
).rejects.toThrow(/Invalid value for metadata.name/);
89+
});
90+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2024 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { EntityPolicy } from '@backstage/catalog-model';
18+
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
19+
20+
/**
21+
* Transform a given entity policy to an entity processor.
22+
* @param policy - The policy to transform
23+
* @returns A new entity processor that uses the entity policy.
24+
* @public
25+
*/
26+
export function transformLegacyPolicyToProcessor(
27+
policy: EntityPolicy,
28+
): CatalogProcessor {
29+
return {
30+
getProcessorName() {
31+
return policy.constructor.name;
32+
},
33+
async preProcessEntity(entity) {
34+
// If enforcing the policy fails, throw the policy error.
35+
const result = await policy.enforce(entity);
36+
if (!result) {
37+
return entity;
38+
}
39+
return result;
40+
},
41+
};
42+
}

plugins/catalog-backend/src/service/CatalogPlugin.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
createBackendPlugin,
1818
coreServices,
1919
} from '@backstage/backend-plugin-api';
20-
import { Entity } from '@backstage/catalog-model';
20+
import { Entity, Validators } from '@backstage/catalog-model';
2121
import { CatalogBuilder, CatalogPermissionRuleInput } from './CatalogBuilder';
2222
import {
2323
CatalogAnalysisExtensionPoint,
@@ -26,6 +26,8 @@ import {
2626
catalogProcessingExtensionPoint,
2727
CatalogPermissionExtensionPoint,
2828
catalogPermissionExtensionPoint,
29+
CatalogModelExtensionPoint,
30+
catalogModelExtensionPoint,
2931
} from '@backstage/plugin-catalog-node/alpha';
3032
import {
3133
CatalogProcessor,
@@ -34,6 +36,7 @@ import {
3436
ScmLocationAnalyzer,
3537
} from '@backstage/plugin-catalog-node';
3638
import { loggerToWinstonLogger } from '@backstage/backend-common';
39+
import { merge } from 'lodash';
3740

3841
class CatalogProcessingExtensionPointImpl
3942
implements CatalogProcessingExtensionPoint
@@ -124,6 +127,18 @@ class CatalogPermissionExtensionPointImpl
124127
}
125128
}
126129

130+
class CatalogModelExtensionPointImpl implements CatalogModelExtensionPoint {
131+
#fieldValidators: Partial<Validators> = {};
132+
133+
setFieldValidators(validators: Partial<Validators>): void {
134+
merge(this.#fieldValidators, validators);
135+
}
136+
137+
get fieldValidators() {
138+
return this.#fieldValidators;
139+
}
140+
}
141+
127142
/**
128143
* Catalog plugin
129144
* @alpha
@@ -150,6 +165,9 @@ export const catalogPlugin = createBackendPlugin({
150165
permissionExtensions,
151166
);
152167

168+
const modelExtensions = new CatalogModelExtensionPointImpl();
169+
env.registerExtensionPoint(catalogModelExtensionPoint, modelExtensions);
170+
153171
env.registerInit({
154172
deps: {
155173
logger: coreServices.logger,
@@ -192,6 +210,7 @@ export const catalogPlugin = createBackendPlugin({
192210
);
193211
builder.addLocationAnalyzers(...analysisExtensions.locationAnalyzers);
194212
builder.addPermissionRules(...permissionExtensions.permissionRules);
213+
builder.setFieldFormatValidators(modelExtensions.fieldValidators);
195214

196215
const { processingEngine, router } = await builder.build();
197216

plugins/catalog-node/api-report-alpha.md

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/catalog-node/src/alpha.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ export { catalogAnalysisExtensionPoint } from './extensions';
2222
export type { CatalogPermissionRuleInput } from './extensions';
2323
export type { CatalogPermissionExtensionPoint } from './extensions';
2424
export { catalogPermissionExtensionPoint } from './extensions';
25+
export type { CatalogModelExtensionPoint } from './extensions';
26+
export { catalogModelExtensionPoint } from './extensions';

plugins/catalog-node/src/extensions.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { createExtensionPoint } from '@backstage/backend-plugin-api';
18-
import { Entity } from '@backstage/catalog-model';
18+
import { Entity, Validators } from '@backstage/catalog-model';
1919
import {
2020
CatalogProcessor,
2121
EntitiesSearchFilter,
@@ -45,6 +45,18 @@ export interface CatalogProcessingExtensionPoint {
4545
): void;
4646
}
4747

48+
/** @alpha */
49+
export interface CatalogModelExtensionPoint {
50+
/**
51+
* Sets the validator function to use for one or more special fields of an
52+
* entity. This is useful if the default rules for formatting of fields are
53+
* not sufficient.
54+
*
55+
* @param validators - The (subset of) validators to set
56+
*/
57+
setFieldValidators(validators: Partial<Validators>): void;
58+
}
59+
4860
/**
4961
* @alpha
5062
*/
@@ -68,6 +80,12 @@ export const catalogAnalysisExtensionPoint =
6880
id: 'catalog.analysis',
6981
});
7082

83+
/** @alpha */
84+
export const catalogModelExtensionPoint =
85+
createExtensionPoint<CatalogModelExtensionPoint>({
86+
id: 'catalog.model',
87+
});
88+
7189
/**
7290
* @alpha
7391
*/

0 commit comments

Comments
 (0)