diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index 3dc7deae7c..120de5152e 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -24,6 +24,7 @@ import { } from './entity'; import { ComponentEntityV1alpha1Policy, + GroupEntityV1alpha1Policy, LocationEntityV1alpha1Policy, TemplateEntityV1alpha1Policy, } from './kinds'; @@ -74,6 +75,7 @@ export class EntityPolicies implements EntityPolicy { ]), EntityPolicies.anyOf([ new ComponentEntityV1alpha1Policy(), + new GroupEntityV1alpha1Policy(), new LocationEntityV1alpha1Policy(), new TemplateEntityV1alpha1Policy(), ]), diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts new file mode 100644 index 0000000000..c0a98685e9 --- /dev/null +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 '../types'; +import { + GroupEntityV1alpha1, + GroupEntityV1alpha1Policy, +} from './GroupEntityV1alpha1'; + +describe('GroupV1alpha1Policy', () => { + let entity: GroupEntityV1alpha1; + let policy: EntityPolicy; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'doe-squad', + title: 'Doe Squad', + description: 'A squad for John and Jane', + }, + spec: { + type: 'squad', + owner: 'me', + parent: ['group a'], + ancestors: ['group-a', 'global-synergies', 'acme-corp'], + children: [], + grandchildren: [], + }, + }; + policy = new GroupEntityV1alpha1Policy(); + }); + + it('happy path: accepts valid data', async () => { + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('silently accepts v1beta1 as well', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta1'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/); + }); + + it('rejects unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(policy.enforce(entity)).rejects.toThrow(/kind/); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects missing owner', async () => { + delete (entity as any).spec.owner; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects wrong owner', async () => { + (entity as any).spec.owner = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects empty owner', async () => { + (entity as any).spec.owner = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('accepts missing parent', async () => { + delete (entity as any).spec.parent; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('accepts missing ancestors', async () => { + delete (entity as any).spec.ancestors; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('accepts missing children', async () => { + delete (entity as any).spec.children; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('accepts missing grandchildren', async () => { + delete (entity as any).spec.grandchildren; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); +}); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts new file mode 100644 index 0000000000..f7902dbd86 --- /dev/null +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 * as yup from 'yup'; +import type { Entity } from '../entity/Entity'; +import type { EntityPolicy } from '../types'; + +const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; +const KIND = 'Group' as const; + +export interface GroupEntityV1alpha1 extends Entity { + apiVersion: typeof API_VERSION[number]; + kind: typeof KIND; + spec: { + type: string; + owner: string; + parent?: string[]; + ancestors?: string[]; + children?: string[]; + grandchildren?: string[]; + }; +} + +export class GroupEntityV1alpha1Policy implements EntityPolicy { + private schema: yup.Schema; + + constructor() { + this.schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + type: yup.string().required().min(1), + owner: yup.string().required().min(1), + parent: yup.array(yup.string()).notRequired(), + ancestors: yup.array(yup.string()).notRequired(), + children: yup.array(yup.string()).notRequired(), + grandchildren: yup.array(yup.string()).notRequired(), + }) + .required(), + }); + } + + async enforce(envelope: Entity): Promise { + return await this.schema.validate(envelope, { strict: true }); + } +} diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index b5fad184d4..4526e99da4 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -19,6 +19,11 @@ export type { ComponentEntityV1alpha1 as ComponentEntity, ComponentEntityV1alpha1, } from './ComponentEntityV1alpha1'; +export { GroupEntityV1alpha1Policy } from './GroupEntityV1alpha1'; +export type { + GroupEntityV1alpha1 as GroupEntity, + GroupEntityV1alpha1, +} from './GroupEntityV1alpha1'; export { LocationEntityV1alpha1Policy } from './LocationEntityV1alpha1'; export type { LocationEntityV1alpha1 as LocationEntity,