From 8cd6f68b6f15e6ed50e8f18d398f83076552b275 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 6 Aug 2020 02:43:35 +0800 Subject: [PATCH 1/3] Define a simple Group Entity --- packages/catalog-model/src/EntityPolicies.ts | 2 + .../src/kinds/GroupEntityV1alpha1.test.ts | 116 ++++++++++++++++++ .../src/kinds/GroupEntityV1alpha1.ts | 60 +++++++++ packages/catalog-model/src/kinds/index.ts | 5 + 4 files changed, 183 insertions(+) create mode 100644 packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts create mode 100644 packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts 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, From c9cd3e1354a5593244050e1472074f79f261a21f Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Tue, 11 Aug 2020 16:25:38 +0800 Subject: [PATCH 2/3] Update GroupEntity spec definition * Make parent a single string instead of an array * Rename certain spec parameters * Make children, ancestors & descendents non-optional --- .../src/kinds/GroupEntityV1alpha1.test.ts | 50 ++++++++++--------- .../src/kinds/GroupEntityV1alpha1.ts | 18 +++---- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index c0a98685e9..e7594772c1 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -35,11 +35,10 @@ describe('GroupV1alpha1Policy', () => { }, spec: { type: 'squad', - owner: 'me', - parent: ['group a'], + parent: 'group-a', ancestors: ['group-a', 'global-synergies', 'acme-corp'], - children: [], - grandchildren: [], + children: ['child-a', 'child-b'], + descendents: ['desc-a', 'desc-b'], }, }; policy = new GroupEntityV1alpha1Policy(); @@ -79,38 +78,43 @@ describe('GroupV1alpha1Policy', () => { 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 () => { + it('rejects empty parent', async () => { + (entity as any).spec.parent = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/parent/); + }); + + it('rejects missing ancestors', async () => { delete (entity as any).spec.ancestors; + await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/); + }); + + it('accepts empty ancestors', async () => { + (entity as any).spec.ancestors = ['']; await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('accepts missing children', async () => { + it('rejects missing children', async () => { delete (entity as any).spec.children; + await expect(policy.enforce(entity)).rejects.toThrow(/children/); + }); + + it('accepts empty children', async () => { + (entity as any).spec.children = ['']; await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('accepts missing grandchildren', async () => { - delete (entity as any).spec.grandchildren; + it('rejects missing descendents', async () => { + delete (entity as any).spec.descendents; + await expect(policy.enforce(entity)).rejects.toThrow(/descendents/); + }); + + it('accepts empty descendents', async () => { + (entity as any).spec.descendents = ['']; 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 index f7902dbd86..b3ab406729 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -26,11 +26,10 @@ export interface GroupEntityV1alpha1 extends Entity { kind: typeof KIND; spec: { type: string; - owner: string; - parent?: string[]; - ancestors?: string[]; - children?: string[]; - grandchildren?: string[]; + parent?: string; + ancestors: string[]; + children: string[]; + descendents: string[]; }; } @@ -44,11 +43,10 @@ export class GroupEntityV1alpha1Policy implements EntityPolicy { 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(), + parent: yup.string().notRequired().min(1), + ancestors: yup.array(yup.string()).required(), + children: yup.array(yup.string()).required(), + descendents: yup.array(yup.string()).required(), }) .required(), }); From c898200065c209977e486c30381511a4b3454277 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Wed, 12 Aug 2020 19:23:43 +0800 Subject: [PATCH 3/3] Fix typo --- .../src/kinds/GroupEntityV1alpha1.test.ts | 12 ++++++------ .../catalog-model/src/kinds/GroupEntityV1alpha1.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index e7594772c1..94dcc2f14f 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -38,7 +38,7 @@ describe('GroupV1alpha1Policy', () => { parent: 'group-a', ancestors: ['group-a', 'global-synergies', 'acme-corp'], children: ['child-a', 'child-b'], - descendents: ['desc-a', 'desc-b'], + descendants: ['desc-a', 'desc-b'], }, }; policy = new GroupEntityV1alpha1Policy(); @@ -108,13 +108,13 @@ describe('GroupV1alpha1Policy', () => { await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('rejects missing descendents', async () => { - delete (entity as any).spec.descendents; - await expect(policy.enforce(entity)).rejects.toThrow(/descendents/); + it('rejects missing descendants', async () => { + delete (entity as any).spec.descendants; + await expect(policy.enforce(entity)).rejects.toThrow(/descendants/); }); - it('accepts empty descendents', async () => { - (entity as any).spec.descendents = ['']; + it('accepts empty descendants', async () => { + (entity as any).spec.descendants = ['']; 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 index b3ab406729..c1d39d5b44 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -29,7 +29,7 @@ export interface GroupEntityV1alpha1 extends Entity { parent?: string; ancestors: string[]; children: string[]; - descendents: string[]; + descendants: string[]; }; } @@ -46,7 +46,7 @@ export class GroupEntityV1alpha1Policy implements EntityPolicy { parent: yup.string().notRequired().min(1), ancestors: yup.array(yup.string()).required(), children: yup.array(yup.string()).required(), - descendents: yup.array(yup.string()).required(), + descendants: yup.array(yup.string()).required(), }) .required(), });