Skip to content

Commit

Permalink
Merge pull request #22568 from sennyeya/catalog-entity-policies
Browse files Browse the repository at this point in the history
feat(catalog): add support for entity policies in the new backend.
  • Loading branch information
freben authored Feb 17, 2024
2 parents b1789a2 + 5182f5b commit 25adbdd
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 2 deletions.
33 changes: 33 additions & 0 deletions .changeset/thirty-bags-try.md
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions plugins/catalog-backend/api-report.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions plugins/catalog-backend/src/modules/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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/);
});
});
Original file line number Diff line number Diff line change
@@ -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;
},
};
}
21 changes: 20 additions & 1 deletion plugins/catalog-backend/src/service/CatalogPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +26,8 @@ import {
catalogProcessingExtensionPoint,
CatalogPermissionExtensionPoint,
catalogPermissionExtensionPoint,
CatalogModelExtensionPoint,
catalogModelExtensionPoint,
} from '@backstage/plugin-catalog-node/alpha';
import {
CatalogProcessor,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -150,6 +165,9 @@ export const catalogPlugin = createBackendPlugin({
permissionExtensions,
);

const modelExtensions = new CatalogModelExtensionPointImpl();
env.registerExtensionPoint(catalogModelExtensionPoint, modelExtensions);

env.registerInit({
deps: {
logger: coreServices.logger,
Expand Down Expand Up @@ -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();

Expand Down
9 changes: 9 additions & 0 deletions plugins/catalog-node/api-report-alpha.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions plugins/catalog-node/src/alpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
20 changes: 19 additions & 1 deletion plugins/catalog-node/src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
*/
Expand All @@ -68,6 +80,12 @@ export const catalogAnalysisExtensionPoint =
id: 'catalog.analysis',
});

/** @alpha */
export const catalogModelExtensionPoint =
createExtensionPoint<CatalogModelExtensionPoint>({
id: 'catalog.model',
});

/**
* @alpha
*/
Expand Down

0 comments on commit 25adbdd

Please sign in to comment.