Merge pull request #2669 from spotify/freben/github-org
feat(catalog-backend): implement github org entity ingestion
This commit is contained in:
@@ -45,7 +45,7 @@ export default async function createPlugin({
|
||||
|
||||
useHotCleanup(
|
||||
module,
|
||||
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
|
||||
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000),
|
||||
);
|
||||
|
||||
return await createRouter({
|
||||
|
||||
@@ -23,12 +23,12 @@ import {
|
||||
SchemaValidEntityPolicy,
|
||||
} from './entity';
|
||||
import {
|
||||
ApiEntityV1alpha1Policy,
|
||||
ComponentEntityV1alpha1Policy,
|
||||
GroupEntityV1alpha1Policy,
|
||||
LocationEntityV1alpha1Policy,
|
||||
TemplateEntityV1alpha1Policy,
|
||||
UserEntityV1alpha1Policy,
|
||||
apiEntityV1alpha1Policy,
|
||||
componentEntityV1alpha1Policy,
|
||||
groupEntityV1alpha1Policy,
|
||||
locationEntityV1alpha1Policy,
|
||||
templateEntityV1alpha1Policy,
|
||||
userEntityV1alpha1Policy,
|
||||
} from './kinds';
|
||||
import { EntityPolicy } from './types';
|
||||
|
||||
@@ -40,7 +40,10 @@ class AllEntityPolicies implements EntityPolicy {
|
||||
async enforce(entity: Entity): Promise<Entity> {
|
||||
let result = entity;
|
||||
for (const policy of this.policies) {
|
||||
result = await policy.enforce(entity);
|
||||
const output = await policy.enforce(entity);
|
||||
if (output) {
|
||||
result = output;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -51,12 +54,11 @@ class AllEntityPolicies implements EntityPolicy {
|
||||
class AnyEntityPolicy implements EntityPolicy {
|
||||
constructor(private readonly policies: EntityPolicy[]) {}
|
||||
|
||||
async enforce(entity: Entity): Promise<Entity> {
|
||||
async enforce(entity: Entity): Promise<Entity | undefined> {
|
||||
for (const policy of this.policies) {
|
||||
try {
|
||||
return await policy.enforce(entity);
|
||||
} catch {
|
||||
continue;
|
||||
const output = await policy.enforce(entity);
|
||||
if (output !== null) {
|
||||
return output;
|
||||
}
|
||||
}
|
||||
throw new Error(`The entity did not match any known policy`);
|
||||
@@ -76,12 +78,12 @@ export class EntityPolicies implements EntityPolicy {
|
||||
new ReservedFieldsEntityPolicy(),
|
||||
]),
|
||||
EntityPolicies.anyOf([
|
||||
new ComponentEntityV1alpha1Policy(),
|
||||
new GroupEntityV1alpha1Policy(),
|
||||
new UserEntityV1alpha1Policy(),
|
||||
new LocationEntityV1alpha1Policy(),
|
||||
new TemplateEntityV1alpha1Policy(),
|
||||
new ApiEntityV1alpha1Policy(),
|
||||
componentEntityV1alpha1Policy,
|
||||
groupEntityV1alpha1Policy,
|
||||
userEntityV1alpha1Policy,
|
||||
locationEntityV1alpha1Policy,
|
||||
templateEntityV1alpha1Policy,
|
||||
apiEntityV1alpha1Policy,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -98,7 +100,7 @@ export class EntityPolicies implements EntityPolicy {
|
||||
this.policy = policy;
|
||||
}
|
||||
|
||||
enforce(entity: Entity): Promise<Entity> {
|
||||
enforce(entity: Entity): Promise<Entity | undefined> {
|
||||
return this.policy.enforce(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../types';
|
||||
import {
|
||||
ApiEntityV1alpha1,
|
||||
ApiEntityV1alpha1Policy,
|
||||
apiEntityV1alpha1Policy as policy,
|
||||
} from './ApiEntityV1alpha1';
|
||||
|
||||
describe('ApiV1alpha1Policy', () => {
|
||||
let entity: ApiEntityV1alpha1;
|
||||
let policy: EntityPolicy;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
@@ -49,7 +47,7 @@ paths:
|
||||
'200':
|
||||
description: A paged array of pets
|
||||
content:
|
||||
application/json:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pets"
|
||||
components:
|
||||
@@ -74,7 +72,6 @@ components:
|
||||
`,
|
||||
},
|
||||
};
|
||||
policy = new ApiEntityV1alpha1Policy();
|
||||
});
|
||||
|
||||
it('happy path: accepts valid data', async () => {
|
||||
@@ -86,14 +83,14 @@ components:
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects unknown apiVersion', async () => {
|
||||
it('ignores unknown apiVersion', async () => {
|
||||
(entity as any).apiVersion = 'backstage.io/v1beta0';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects unknown kind', async () => {
|
||||
it('ignores unknown kind', async () => {
|
||||
(entity as any).kind = 'Wizard';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects missing type', async () => {
|
||||
|
||||
@@ -16,11 +16,24 @@
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import type { EntityPolicy } from '../types';
|
||||
import { schemaPolicy } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'API' as const;
|
||||
|
||||
const schema = yup.object<Partial<ApiEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
definition: yup.string().required().min(1),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface ApiEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -32,25 +45,4 @@ export interface ApiEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export class ApiEntityV1alpha1Policy implements EntityPolicy {
|
||||
private schema: yup.Schema<any>;
|
||||
|
||||
constructor() {
|
||||
this.schema = yup.object<Partial<ApiEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
definition: yup.string().required().min(1),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
}
|
||||
|
||||
async enforce(envelope: Entity): Promise<Entity> {
|
||||
return await this.schema.validate(envelope, { strict: true });
|
||||
}
|
||||
}
|
||||
export const apiEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema);
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../types';
|
||||
import {
|
||||
ComponentEntityV1alpha1,
|
||||
ComponentEntityV1alpha1Policy,
|
||||
componentEntityV1alpha1Policy as policy,
|
||||
} from './ComponentEntityV1alpha1';
|
||||
|
||||
describe('ComponentV1alpha1Policy', () => {
|
||||
let entity: ComponentEntityV1alpha1;
|
||||
let policy: EntityPolicy;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
@@ -38,7 +36,6 @@ describe('ComponentV1alpha1Policy', () => {
|
||||
implementsApis: ['api-0'],
|
||||
},
|
||||
};
|
||||
policy = new ComponentEntityV1alpha1Policy();
|
||||
});
|
||||
|
||||
it('happy path: accepts valid data', async () => {
|
||||
@@ -50,14 +47,14 @@ describe('ComponentV1alpha1Policy', () => {
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects unknown apiVersion', async () => {
|
||||
it('ignores unknown apiVersion', async () => {
|
||||
(entity as any).apiVersion = 'backstage.io/v1beta0';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects unknown kind', async () => {
|
||||
it('ignores unknown kind', async () => {
|
||||
(entity as any).kind = 'Wizard';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects missing type', async () => {
|
||||
|
||||
@@ -16,11 +16,24 @@
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import type { EntityPolicy } from '../types';
|
||||
import { schemaPolicy } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Component' as const;
|
||||
|
||||
const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
implementsApis: yup.array(yup.string()).notRequired(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface ComponentEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -32,25 +45,8 @@ export interface ComponentEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export class ComponentEntityV1alpha1Policy implements EntityPolicy {
|
||||
private schema: yup.Schema<any>;
|
||||
|
||||
constructor() {
|
||||
this.schema = yup.object<Partial<ComponentEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
implementsApis: yup.array(yup.string()).notRequired(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
}
|
||||
|
||||
async enforce(envelope: Entity): Promise<Entity> {
|
||||
return await this.schema.validate(envelope, { strict: true });
|
||||
}
|
||||
}
|
||||
export const componentEntityV1alpha1Policy = schemaPolicy(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
);
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../types';
|
||||
import {
|
||||
GroupEntityV1alpha1,
|
||||
GroupEntityV1alpha1Policy,
|
||||
groupEntityV1alpha1Policy as policy,
|
||||
} from './GroupEntityV1alpha1';
|
||||
|
||||
describe('GroupV1alpha1Policy', () => {
|
||||
let entity: GroupEntityV1alpha1;
|
||||
let policy: EntityPolicy;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
@@ -41,7 +39,6 @@ describe('GroupV1alpha1Policy', () => {
|
||||
descendants: ['desc-a', 'desc-b'],
|
||||
},
|
||||
};
|
||||
policy = new GroupEntityV1alpha1Policy();
|
||||
});
|
||||
|
||||
it('happy path: accepts valid data', async () => {
|
||||
@@ -53,14 +50,14 @@ describe('GroupV1alpha1Policy', () => {
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects unknown apiVersion', async () => {
|
||||
it('ignores unknown apiVersion', async () => {
|
||||
(entity as any).apiVersion = 'backstage.io/v1beta0';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects unknown kind', async () => {
|
||||
it('ignores unknown kind', async () => {
|
||||
(entity as any).kind = 'Wizard';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects missing type', async () => {
|
||||
@@ -98,6 +95,11 @@ describe('GroupV1alpha1Policy', () => {
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('accepts no ancestors', async () => {
|
||||
(entity as any).spec.ancestors = [];
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects missing children', async () => {
|
||||
delete (entity as any).spec.children;
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
|
||||
@@ -108,6 +110,11 @@ describe('GroupV1alpha1Policy', () => {
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('accepts no children', async () => {
|
||||
(entity as any).spec.children = [];
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects missing descendants', async () => {
|
||||
delete (entity as any).spec.descendants;
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
|
||||
@@ -117,4 +124,9 @@ describe('GroupV1alpha1Policy', () => {
|
||||
(entity as any).spec.descendants = [''];
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('accepts no descendants', async () => {
|
||||
(entity as any).spec.descendants = [];
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,11 +16,39 @@
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import type { EntityPolicy } from '../types';
|
||||
import { schemaPolicy } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Group' as const;
|
||||
|
||||
const schema = yup.object<Partial<GroupEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
parent: yup.string().notRequired().min(1),
|
||||
// Use these manual tests because yup .required() requires at least
|
||||
// one element and there is no simple workaround -_-
|
||||
ancestors: yup.array(yup.string()).test({
|
||||
name: 'isDefined',
|
||||
message: 'ancestors must be defined',
|
||||
test: v => Boolean(v),
|
||||
}),
|
||||
children: yup.array(yup.string()).test({
|
||||
name: 'isDefined',
|
||||
message: 'children must be defined',
|
||||
test: v => Boolean(v),
|
||||
}),
|
||||
descendants: yup.array(yup.string()).test({
|
||||
name: 'isDefined',
|
||||
message: 'descendants must be defined',
|
||||
test: v => Boolean(v),
|
||||
}),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface GroupEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -33,26 +61,8 @@ export interface GroupEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export class GroupEntityV1alpha1Policy implements EntityPolicy {
|
||||
private schema: yup.Schema<any>;
|
||||
|
||||
constructor() {
|
||||
this.schema = yup.object<Partial<GroupEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
parent: yup.string().notRequired().min(1),
|
||||
ancestors: yup.array(yup.string()).required(),
|
||||
children: yup.array(yup.string()).required(),
|
||||
descendants: yup.array(yup.string()).required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
}
|
||||
|
||||
async enforce(envelope: Entity): Promise<Entity> {
|
||||
return await this.schema.validate(envelope, { strict: true });
|
||||
}
|
||||
}
|
||||
export const groupEntityV1alpha1Policy = schemaPolicy(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
);
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../types';
|
||||
import {
|
||||
LocationEntityV1alpha1,
|
||||
LocationEntityV1alpha1Policy,
|
||||
locationEntityV1alpha1Policy as policy,
|
||||
} from './LocationEntityV1alpha1';
|
||||
|
||||
describe('LocationV1alpha1Policy', () => {
|
||||
let entity: LocationEntityV1alpha1;
|
||||
let policy: EntityPolicy;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
@@ -35,7 +33,6 @@ describe('LocationV1alpha1Policy', () => {
|
||||
type: 'github',
|
||||
},
|
||||
};
|
||||
policy = new LocationEntityV1alpha1Policy();
|
||||
});
|
||||
|
||||
it('happy path: accepts valid data', async () => {
|
||||
@@ -47,14 +44,14 @@ describe('LocationV1alpha1Policy', () => {
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects unknown apiVersion', async () => {
|
||||
it('ignores unknown apiVersion', async () => {
|
||||
(entity as any).apiVersion = 'backstage.io/v1beta0';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects unknown kind', async () => {
|
||||
it('ignores unknown kind', async () => {
|
||||
(entity as any).kind = 'Wizard';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects missing type', async () => {
|
||||
|
||||
@@ -16,11 +16,23 @@
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import type { EntityPolicy } from '../types';
|
||||
import { schemaPolicy } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Location' as const;
|
||||
|
||||
const schema = yup.object<Partial<LocationEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
target: yup.string().notRequired().min(1),
|
||||
targets: yup.array(yup.string()).notRequired(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface LocationEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -31,24 +43,8 @@ export interface LocationEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export class LocationEntityV1alpha1Policy implements EntityPolicy {
|
||||
private schema: yup.Schema<any>;
|
||||
|
||||
constructor() {
|
||||
this.schema = yup.object<Partial<LocationEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
target: yup.string().notRequired().min(1),
|
||||
targets: yup.array(yup.string()).notRequired(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
}
|
||||
|
||||
async enforce(envelope: Entity): Promise<Entity> {
|
||||
return await this.schema.validate(envelope, { strict: true });
|
||||
}
|
||||
}
|
||||
export const locationEntityV1alpha1Policy = schemaPolicy(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
);
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../types';
|
||||
import {
|
||||
TemplateEntityV1alpha1,
|
||||
TemplateEntityV1alpha1Policy,
|
||||
templateEntityV1alpha1Policy as policy,
|
||||
} from './TemplateEntityV1alpha1';
|
||||
|
||||
describe('TemplateEntityV1alpah1', () => {
|
||||
describe('templateEntityV1alpha1', () => {
|
||||
let entity: TemplateEntityV1alpha1;
|
||||
let policy: EntityPolicy;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
@@ -52,7 +50,6 @@ describe('TemplateEntityV1alpah1', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
policy = new TemplateEntityV1alpha1Policy();
|
||||
});
|
||||
|
||||
it('happy path: accepts valid data', async () => {
|
||||
@@ -64,14 +61,14 @@ describe('TemplateEntityV1alpah1', () => {
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects unknown apiVersion', async () => {
|
||||
it('ignores unknown apiVersion', async () => {
|
||||
(entity as any).apiVersion = 'backstage.io/v1beta0';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects unknown kind', async () => {
|
||||
it('ignores unknown kind', async () => {
|
||||
(entity as any).kind = 'Wizard';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects missing type', async () => {
|
||||
|
||||
@@ -16,11 +16,25 @@
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import type { EntityPolicy, JSONSchema } from '../types';
|
||||
import type { JSONSchema } from '../types';
|
||||
import { schemaPolicy } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Template' as const;
|
||||
|
||||
const schema = yup.object<Partial<TemplateEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
path: yup.string(),
|
||||
schema: yup.object().required(),
|
||||
templater: yup.string().required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface TemplateEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -32,25 +46,8 @@ export interface TemplateEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export class TemplateEntityV1alpha1Policy implements EntityPolicy {
|
||||
private schema: yup.Schema<any>;
|
||||
|
||||
constructor() {
|
||||
this.schema = yup.object<Partial<TemplateEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
path: yup.string(),
|
||||
schema: yup.object().required(),
|
||||
templater: yup.string().required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
}
|
||||
|
||||
async enforce(envelope: Entity): Promise<Entity> {
|
||||
return await this.schema.validate(envelope, { strict: true });
|
||||
}
|
||||
}
|
||||
export const templateEntityV1alpha1Policy = schemaPolicy(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
);
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../types';
|
||||
import {
|
||||
UserEntityV1alpha1,
|
||||
UserEntityV1alpha1Policy,
|
||||
userEntityV1alpha1Policy as policy,
|
||||
} from './UserEntityV1alpha1';
|
||||
|
||||
describe('UserV1alpha1Policy', () => {
|
||||
describe('userEntityV1alpha1Policy', () => {
|
||||
let entity: UserEntityV1alpha1;
|
||||
let policy: EntityPolicy;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
@@ -40,7 +38,6 @@ describe('UserV1alpha1Policy', () => {
|
||||
memberOf: ['team-a', 'developers'],
|
||||
},
|
||||
};
|
||||
policy = new UserEntityV1alpha1Policy();
|
||||
});
|
||||
|
||||
it('happy path: accepts valid data', async () => {
|
||||
@@ -54,14 +51,14 @@ describe('UserV1alpha1Policy', () => {
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects unknown apiVersion', async () => {
|
||||
it('ignores unknown apiVersion', async () => {
|
||||
(entity as any).apiVersion = 'backstage.io/v1beta0';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects unknown kind', async () => {
|
||||
it('ignores unknown kind', async () => {
|
||||
(entity as any).kind = 'Wizard';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
|
||||
await expect(policy.enforce(entity)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('spec accepts unknown additional fields', async () => {
|
||||
@@ -147,4 +144,14 @@ describe('UserV1alpha1Policy', () => {
|
||||
(entity as any).spec.memberOf[0] = 7;
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
|
||||
});
|
||||
|
||||
it('accepts empty memberOf', async () => {
|
||||
(entity as any).spec.memberOf = [];
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
|
||||
it('rejects null memberOf', async () => {
|
||||
(entity as any).spec.memberOf = null;
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,11 +16,34 @@
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import type { EntityPolicy } from '../types';
|
||||
import { schemaPolicy } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'User' as const;
|
||||
|
||||
const schema = yup.object<Partial<UserEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
profile: yup
|
||||
.object({
|
||||
displayName: yup.string().min(1).notRequired(),
|
||||
email: yup.string().min(1).notRequired(),
|
||||
picture: yup.string().min(1).notRequired(),
|
||||
})
|
||||
.notRequired(),
|
||||
// Use this manual test because yup .required() requires at least one
|
||||
// element and there is no simple workaround -_-
|
||||
memberOf: yup.array(yup.string()).test({
|
||||
name: 'isDefined',
|
||||
message: 'memberOf must be defined',
|
||||
test: v => Boolean(v),
|
||||
}),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface UserEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -34,29 +57,4 @@ export interface UserEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export class UserEntityV1alpha1Policy implements EntityPolicy {
|
||||
private schema: yup.Schema<any>;
|
||||
|
||||
constructor() {
|
||||
this.schema = yup.object<Partial<UserEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
profile: yup
|
||||
.object({
|
||||
displayName: yup.string().min(1).notRequired(),
|
||||
email: yup.string().min(1).notRequired(),
|
||||
picture: yup.string().min(1).notRequired(),
|
||||
})
|
||||
.notRequired(),
|
||||
memberOf: yup.array(yup.string()).required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
}
|
||||
|
||||
async enforce(envelope: Entity): Promise<Entity> {
|
||||
return await this.schema.validate(envelope, { strict: true });
|
||||
}
|
||||
}
|
||||
export const userEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema);
|
||||
|
||||
@@ -14,32 +14,32 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1';
|
||||
export { apiEntityV1alpha1Policy } from './ApiEntityV1alpha1';
|
||||
export type {
|
||||
ApiEntityV1alpha1 as ApiEntity,
|
||||
ApiEntityV1alpha1,
|
||||
} from './ApiEntityV1alpha1';
|
||||
export { ComponentEntityV1alpha1Policy } from './ComponentEntityV1alpha1';
|
||||
export { componentEntityV1alpha1Policy } from './ComponentEntityV1alpha1';
|
||||
export type {
|
||||
ComponentEntityV1alpha1 as ComponentEntity,
|
||||
ComponentEntityV1alpha1,
|
||||
} from './ComponentEntityV1alpha1';
|
||||
export { GroupEntityV1alpha1Policy } from './GroupEntityV1alpha1';
|
||||
export { groupEntityV1alpha1Policy } from './GroupEntityV1alpha1';
|
||||
export type {
|
||||
GroupEntityV1alpha1 as GroupEntity,
|
||||
GroupEntityV1alpha1,
|
||||
} from './GroupEntityV1alpha1';
|
||||
export { LocationEntityV1alpha1Policy } from './LocationEntityV1alpha1';
|
||||
export { locationEntityV1alpha1Policy } from './LocationEntityV1alpha1';
|
||||
export type {
|
||||
LocationEntityV1alpha1 as LocationEntity,
|
||||
LocationEntityV1alpha1,
|
||||
} from './LocationEntityV1alpha1';
|
||||
export { TemplateEntityV1alpha1Policy } from './TemplateEntityV1alpha1';
|
||||
export { templateEntityV1alpha1Policy } from './TemplateEntityV1alpha1';
|
||||
export type {
|
||||
TemplateEntityV1alpha1 as TemplateEntity,
|
||||
TemplateEntityV1alpha1,
|
||||
} from './TemplateEntityV1alpha1';
|
||||
export { UserEntityV1alpha1Policy } from './UserEntityV1alpha1';
|
||||
export { userEntityV1alpha1Policy } from './UserEntityV1alpha1';
|
||||
export type {
|
||||
UserEntityV1alpha1 as UserEntity,
|
||||
UserEntityV1alpha1,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { Entity } from '../entity';
|
||||
import { EntityPolicy } from '../types';
|
||||
|
||||
export function schemaPolicy(
|
||||
kind: string,
|
||||
apiVersion: readonly string[],
|
||||
schema: yup.Schema<any>,
|
||||
): EntityPolicy {
|
||||
return {
|
||||
async enforce(envelope: Entity): Promise<Entity | undefined> {
|
||||
if (
|
||||
kind !== envelope.kind ||
|
||||
!apiVersion.includes(envelope.apiVersion as any)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return await schema.validate(envelope, { strict: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -27,10 +27,11 @@ export type EntityPolicy = {
|
||||
* Applies validation or mutation on an entity.
|
||||
*
|
||||
* @param entity The entity, as validated/mutated so far in the policy tree
|
||||
* @returns The incoming entity, or a mutated version of the same
|
||||
* @returns The incoming entity, or a mutated version of the same, or
|
||||
* undefined if this processor could not handle the entity
|
||||
* @throws An error if the entity should be rejected
|
||||
*/
|
||||
enforce(entity: Entity): Promise<Entity>;
|
||||
enforce(entity: Entity): Promise<Entity | undefined>;
|
||||
};
|
||||
|
||||
export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue };
|
||||
|
||||
+1
-2
@@ -10,7 +10,7 @@ import { setupServer } from 'msw/node';
|
||||
describe('ExampleComponent', () => {
|
||||
const server = setupServer();
|
||||
// Enable API mocking before tests.
|
||||
beforeAll(() => server.listen())
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
|
||||
// Reset any runtime request handlers we may add during the tests.
|
||||
afterEach(() => server.resetHandlers())
|
||||
@@ -32,4 +32,3 @@ describe('ExampleComponent', () => {
|
||||
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { setupServer } from 'msw/node';
|
||||
describe('ExampleFetchComponent', () => {
|
||||
const server = setupServer();
|
||||
// Enable API mocking before tests.
|
||||
beforeAll(() => server.listen())
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
|
||||
// Reset any runtime request handlers we may add during the tests.
|
||||
afterEach(() => server.resetHandlers())
|
||||
|
||||
@@ -66,7 +66,7 @@ scaffolder:
|
||||
|
||||
catalog:
|
||||
rules:
|
||||
- allow: [Component, API, Group, Template, Location]
|
||||
- allow: [Component, API, Group, User, Template, Location]
|
||||
processors:
|
||||
github:
|
||||
providers:
|
||||
|
||||
Reference in New Issue
Block a user