Merge pull request #2893 from spotify/freben/validator-any

chore(catalog-model): use unknown instead of any for validators
This commit is contained in:
Fredrik Adelöw
2020-10-15 09:48:15 +02:00
committed by GitHub
21 changed files with 947 additions and 279 deletions
@@ -57,7 +57,7 @@ export function buildPgDatabaseConfig(
* Gets the postgres connection config
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explictly control connection string parsing
* @param parseConnectionString Flag to explicitly control connection string parsing
*/
export function getPgConnectionConfig(
dbConfig: Config,
+8 -24
View File
@@ -14,37 +14,21 @@
* limitations under the License.
*/
import { useHotCleanup } from '@backstage/backend-common';
import {
CatalogBuilder,
createRouter,
DatabaseEntitiesCatalog,
DatabaseLocationsCatalog,
DatabaseManager,
HigherOrderOperations,
LocationReaders,
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
config,
reader,
database,
}: PluginEnvironment) {
const locationReader = new LocationReaders({ logger, reader, config });
const db = await DatabaseManager.createDatabase(await database.getClient(), {
logger,
});
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
const higherOrderOperation = new HigherOrderOperations(
export default async function createPlugin(env: PluginEnvironment) {
const builder = new CatalogBuilder(env);
const {
entitiesCatalog,
locationsCatalog,
locationReader,
logger,
);
higherOrderOperation,
} = await builder.build();
useHotCleanup(
module,
@@ -55,6 +39,6 @@ export default async function createPlugin({
entitiesCatalog,
locationsCatalog,
higherOrderOperation,
logger,
logger: env.logger,
});
}
@@ -0,0 +1,110 @@
import { Entity } from './entity';
/*
* 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 { EntityPolicies } from './EntityPolicies';
import { EntityPolicy } from './types';
describe('EntityPolicies', () => {
const p1: jest.Mocked<EntityPolicy> = { enforce: jest.fn() };
const p2: jest.Mocked<EntityPolicy> = { enforce: jest.fn() };
const entity1: Entity = {
apiVersion: 'a1',
kind: 'k1',
metadata: { name: 'n1' },
};
const entity2: Entity = {
apiVersion: 'a2',
kind: 'k2',
metadata: { name: 'n2' },
};
afterEach(() => jest.resetAllMocks());
describe('allOf', () => {
it('resolves when no policies', async () => {
const policy = EntityPolicies.allOf([]);
await expect(policy.enforce(entity1)).resolves.toBe(entity1);
});
it('resolves when all resolve', async () => {
p1.enforce.mockResolvedValue(entity1);
p2.enforce.mockResolvedValue(entity1);
const policy = EntityPolicies.allOf([p1, p2]);
await expect(policy.enforce(entity1)).resolves.toBe(entity1);
});
it('rejects when any rejects', async () => {
p1.enforce.mockResolvedValue(entity1);
p2.enforce.mockRejectedValue(new Error('a'));
const policy = EntityPolicies.allOf([p1, p2]);
await expect(policy.enforce(entity1)).rejects.toThrow('a');
});
it('rejects when any ignores', async () => {
p1.enforce.mockResolvedValue(entity1);
p2.enforce.mockResolvedValue(undefined);
const policy = EntityPolicies.allOf([p1, p2]);
await expect(policy.enforce(entity1)).rejects.toThrow(
/did not return a result/,
);
});
it('passes through transforms properly', async () => {
p1.enforce.mockResolvedValue(entity2);
p2.enforce.mockResolvedValue(entity2);
const policy = EntityPolicies.allOf([p1, p2]);
await expect(policy.enforce(entity1)).resolves.toBe(entity2);
expect(p1.enforce).toBeCalledWith(entity1);
expect(p2.enforce).toBeCalledWith(entity2);
});
});
describe('oneOf', () => {
it('rejects when no policies', async () => {
const policy = EntityPolicies.oneOf([]);
await expect(policy.enforce(entity1)).rejects.toThrow(/did not match/);
});
it('resolves when one resolves', async () => {
p1.enforce.mockResolvedValue(undefined);
p2.enforce.mockResolvedValue(entity1);
const policy = EntityPolicies.oneOf([p1, p2]);
await expect(policy.enforce(entity1)).resolves.toBe(entity1);
});
it('rejects when one rejects first', async () => {
p1.enforce.mockRejectedValue(new Error('a'));
p2.enforce.mockResolvedValue(entity1);
const policy = EntityPolicies.oneOf([p1, p2]);
await expect(policy.enforce(entity1)).rejects.toThrow('a');
});
it('resolves first resolution when several resolve', async () => {
p1.enforce.mockResolvedValue(entity1);
p2.enforce.mockResolvedValue(entity2);
const policy = EntityPolicies.oneOf([p1, p2]);
await expect(policy.enforce(entity1)).resolves.toBe(entity1);
});
it('rejects when all ignore', async () => {
p1.enforce.mockResolvedValue(undefined);
p2.enforce.mockResolvedValue(undefined);
const policy = EntityPolicies.oneOf([p1, p2]);
await expect(policy.enforce(entity1)).rejects.toThrow(/did not match/);
});
});
});
+7 -53
View File
@@ -14,22 +14,7 @@
* limitations under the License.
*/
import {
DefaultNamespaceEntityPolicy,
Entity,
FieldFormatEntityPolicy,
NoForeignRootFieldsEntityPolicy,
ReservedFieldsEntityPolicy,
SchemaValidEntityPolicy,
} from './entity';
import {
apiEntityV1alpha1Policy,
componentEntityV1alpha1Policy,
groupEntityV1alpha1Policy,
locationEntityV1alpha1Policy,
templateEntityV1alpha1Policy,
userEntityV1alpha1Policy,
} from './kinds';
import { Entity } from './entity';
import { EntityPolicy } from './types';
// Helper that requires that all of a set of policies can be successfully
@@ -68,42 +53,11 @@ class AnyEntityPolicy implements EntityPolicy {
}
}
export class EntityPolicies implements EntityPolicy {
private readonly policy: EntityPolicy;
static defaultPolicies(): EntityPolicy {
return EntityPolicies.allOf([
EntityPolicies.allOf([
new SchemaValidEntityPolicy(),
new DefaultNamespaceEntityPolicy(),
new NoForeignRootFieldsEntityPolicy(),
new FieldFormatEntityPolicy(),
new ReservedFieldsEntityPolicy(),
]),
EntityPolicies.anyOf([
componentEntityV1alpha1Policy,
groupEntityV1alpha1Policy,
userEntityV1alpha1Policy,
locationEntityV1alpha1Policy,
templateEntityV1alpha1Policy,
apiEntityV1alpha1Policy,
]),
]);
}
static allOf(policies: EntityPolicy[]): EntityPolicy {
export const EntityPolicies = {
allOf(policies: EntityPolicy[]) {
return new AllEntityPolicies(policies);
}
static anyOf(policies: EntityPolicy[]): EntityPolicy {
},
oneOf(policies: EntityPolicy[]) {
return new AnyEntityPolicy(policies);
}
constructor(policy: EntityPolicy = EntityPolicies.defaultPolicies()) {
this.policy = policy;
}
enforce(entity: Entity): Promise<Entity | undefined> {
return this.policy.enforce(entity);
}
}
},
};
@@ -113,7 +113,7 @@ describe('CommonValidatorFunctions', () => {
[{}, true],
[{ a: 1 }, true],
[{ a: undefined }, false],
] as [any, boolean][])(`isJsonSafe %p ? %p`, (value, result) => {
] as [unknown, boolean][])(`isJsonSafe %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isJsonSafe(value)).toBe(result);
});
@@ -31,7 +31,7 @@ export class CommonValidatorFunctions {
* @param isValidSuffix Checks that the part after the separator (or the entire value if there is no separator) is valid
*/
static isValidPrefixAndOrSuffix(
value: any,
value: unknown,
separator: string,
isValidPrefix: (value: string) => boolean,
isValidSuffix: (value: string) => boolean,
@@ -55,7 +55,7 @@ export class CommonValidatorFunctions {
*
* @param value The value to check
*/
static isJsonSafe(value: any): boolean {
static isJsonSafe(value: unknown): boolean {
try {
return lodash.isEqual(value, JSON.parse(JSON.stringify(value)));
} catch {
@@ -69,7 +69,7 @@ export class CommonValidatorFunctions {
* @param value The value to check
* @see https://tools.ietf.org/html/rfc1123
*/
static isValidDnsSubdomain(value: any): boolean {
static isValidDnsSubdomain(value: unknown): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
@@ -84,7 +84,7 @@ export class CommonValidatorFunctions {
* @param value The value to check
* @see https://tools.ietf.org/html/rfc1123
*/
static isValidDnsLabel(value: any): boolean {
static isValidDnsLabel(value: unknown): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
@@ -25,7 +25,7 @@ import { CommonValidatorFunctions } from './CommonValidatorFunctions';
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
*/
export class KubernetesValidatorFunctions {
static isValidApiVersion(value: any): boolean {
static isValidApiVersion(value: unknown): boolean {
return CommonValidatorFunctions.isValidPrefixAndOrSuffix(
value,
'/',
@@ -34,7 +34,7 @@ export class KubernetesValidatorFunctions {
);
}
static isValidKind(value: any): boolean {
static isValidKind(value: unknown): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
@@ -43,7 +43,7 @@ export class KubernetesValidatorFunctions {
);
}
static isValidObjectName(value: any): boolean {
static isValidObjectName(value: unknown): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
@@ -52,11 +52,11 @@ export class KubernetesValidatorFunctions {
);
}
static isValidNamespace(value: any): boolean {
static isValidNamespace(value: unknown): boolean {
return CommonValidatorFunctions.isValidDnsLabel(value);
}
static isValidLabelKey(value: any): boolean {
static isValidLabelKey(value: unknown): boolean {
return CommonValidatorFunctions.isValidPrefixAndOrSuffix(
value,
'/',
@@ -65,13 +65,13 @@ export class KubernetesValidatorFunctions {
);
}
static isValidLabelValue(value: any): boolean {
static isValidLabelValue(value: unknown): boolean {
return (
value === '' || KubernetesValidatorFunctions.isValidObjectName(value)
);
}
static isValidAnnotationKey(value: any): boolean {
static isValidAnnotationKey(value: unknown): boolean {
return CommonValidatorFunctions.isValidPrefixAndOrSuffix(
value,
'/',
@@ -80,7 +80,7 @@ export class KubernetesValidatorFunctions {
);
}
static isValidAnnotationValue(value: any): boolean {
static isValidAnnotationValue(value: unknown): boolean {
return typeof value === 'string';
}
}
@@ -15,13 +15,13 @@
*/
export type Validators = {
isValidApiVersion(value: any): boolean;
isValidKind(value: any): boolean;
isValidEntityName(value: any): boolean;
isValidNamespace(value: any): boolean;
isValidLabelKey(value: any): boolean;
isValidLabelValue(value: any): boolean;
isValidAnnotationKey(value: any): boolean;
isValidAnnotationValue(value: any): boolean;
isValidTag(value: any): boolean;
isValidApiVersion(value: unknown): boolean;
isValidKind(value: unknown): boolean;
isValidEntityName(value: unknown): boolean;
isValidNamespace(value: unknown): boolean;
isValidLabelKey(value: unknown): boolean;
isValidLabelValue(value: unknown): boolean;
isValidAnnotationKey(value: unknown): boolean;
isValidAnnotationValue(value: unknown): boolean;
isValidTag(value: unknown): boolean;
};
@@ -1,44 +1,28 @@
import { useHotCleanup } from '@backstage/backend-common';
import {
CatalogBuilder,
createRouter,
DatabaseEntitiesCatalog,
DatabaseLocationsCatalog,
DatabaseManager,
HigherOrderOperations,
LocationReaders,
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
config,
reader,
database,
}: PluginEnvironment) {
const locationReader = new LocationReaders({ logger, reader, config });
const db = await DatabaseManager.createDatabase(await database.getClient(),
{ logger },
);
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
const higherOrderOperation = new HigherOrderOperations(
export default async function createPlugin(env: PluginEnvironment) {
const builder = new CatalogBuilder(env);
const {
entitiesCatalog,
locationsCatalog,
locationReader,
logger,
);
higherOrderOperation,
} = await builder.build();
useHotCleanup(
module,
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000),
);
return await createRouter({
entitiesCatalog,
locationsCatalog,
higherOrderOperation,
logger,
logger: env.logger,
});
}