Add a DefaultParentEntityPolicy.

It's useful to have an entity policy to set a parent for unparented
groups. This can be used to build a groups hierarchy with a single
root parent group without having to make changes to every group
entity in the catalog.

Signed-off-by: James Peach <jpeach@cloudflare.com>
This commit is contained in:
James Peach
2022-06-23 10:39:12 +10:00
parent f9e2ec2551
commit 4cc81372f8
5 changed files with 152 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-model': patch
---
Introduced DefaultParentEntityPolicy to set a default group entity parent.
+7
View File
@@ -108,6 +108,13 @@ export class DefaultNamespaceEntityPolicy implements EntityPolicy {
enforce(entity: Entity): Promise<Entity>;
}
// @public
export class DefaultParentEntityPolicy implements EntityPolicy {
constructor(parent: string);
// (undocumented)
enforce(entity: Entity): Promise<Entity>;
}
// @public
interface DomainEntityV1alpha1 extends Entity {
// (undocumented)
@@ -0,0 +1,71 @@
/*
* Copyright 2022 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 { UserEntity, GroupEntity } from '../../kinds';
import { DefaultParentEntityPolicy } from './DefaultParentEntityPolicy';
describe('DefaultParentEntityPolicy', () => {
it('should ignore non-group entities', async () => {
const p = new DefaultParentEntityPolicy('name');
const u: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: 'n' },
spec: { profile: {}, memberOf: ['c'] },
};
const result = await p.enforce(u);
expect(result).toEqual({
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: 'n' },
spec: { profile: {}, memberOf: ['c'] },
});
});
it('should parent group entities', async () => {
const p = new DefaultParentEntityPolicy('name');
const g: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'n' },
spec: { type: 'foo', children: [] },
};
const result = await p.enforce(g);
expect(result).toEqual({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'n' },
spec: { type: 'foo', parent: 'group:default/name', children: [] },
});
});
it('should not replace existing parents', async () => {
const p = new DefaultParentEntityPolicy('namespace/name');
const g: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'n' },
spec: { type: 'foo', parent: 'group:something/else', children: [] },
};
const result = await p.enforce(g);
expect(result).toEqual({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'n' },
spec: { type: 'foo', parent: 'group:something/else', children: [] },
});
});
});
@@ -0,0 +1,68 @@
/*
* Copyright 2022 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 } from '../Entity';
import { GroupEntity } from '../../kinds';
import { EntityPolicy } from './types';
import { DEFAULT_NAMESPACE } from '../constants';
import { parseEntityRef, stringifyEntityRef } from '../ref';
/**
* DefaultParentPolicy is an EntityPolicy that updates group entities
* with a parent of last resort. This ensures that, while we preserve
* any existing group hierarchies, we can guarantee that there is a
* single global root of the group hierarchy.
*
* @public
*/
export class DefaultParentEntityPolicy implements EntityPolicy {
private readonly parentRef: string;
constructor(parentEntityRef: string) {
const { kind, namespace, name } = parseEntityRef(parentEntityRef, {
defaultKind: 'Group',
defaultNamespace: DEFAULT_NAMESPACE,
});
if (kind.toLocaleUpperCase('en-US') !== 'GROUP') {
throw new TypeError('group parent must be a group');
}
this.parentRef = stringifyEntityRef({
kind: kind,
namespace: namespace,
name: name,
});
}
async enforce(entity: Entity): Promise<Entity> {
if (entity.kind !== 'Group') {
return entity;
}
const group = entity as GroupEntity;
if (group.spec.parent) {
return group;
}
// Avoid making the parent entity it's own parent.
if (stringifyEntityRef(group) !== this.parentRef) {
group.spec.parent = this.parentRef;
}
return group;
}
}
@@ -15,6 +15,7 @@
*/
export { DefaultNamespaceEntityPolicy } from './DefaultNamespaceEntityPolicy';
export { DefaultParentEntityPolicy } from './DefaultParentEntityPolicy';
export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy';
export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy';
export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy';