Merge remote-tracking branch 'upstream/master' into ndudnik/use-catalog-backend

This commit is contained in:
Nikita Nek Dudnik
2020-05-29 10:13:18 +02:00
95 changed files with 2138 additions and 1988 deletions
@@ -15,3 +15,4 @@
*/
require('jest-fetch-mock').enableMocks();
export {};
+5 -3
View File
@@ -10,7 +10,7 @@
},
"scripts": {
"build": "tsc",
"start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess nodemon",
"start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"nodemon -r esm\\\"",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"clean": "backstage-cli clean",
@@ -18,13 +18,15 @@
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.6",
"@backstage/catalog-model": "^0.1.1-alpha.6",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.6",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.6",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.6",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.6",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.6",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.6",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.6",
"compression": "^1.7.4",
"cors": "^2.8.5",
"esm": "^3.2.25",
"express": "^4.17.1",
"helmet": "^3.22.0",
"knex": "^0.21.1",
+4 -1
View File
@@ -14,8 +14,11 @@
* limitations under the License.
*/
import { PluginEnvironment } from './types';
describe('test', () => {
it('unbreaks the test runner', () => {
expect(true).toBeTruthy();
const unbreaker = {} as PluginEnvironment;
expect(unbreaker).toBeTruthy();
});
});
+10 -4
View File
@@ -21,22 +21,28 @@ import {
DatabaseManager,
DescriptorParsers,
LocationReaders,
IngestionModels,
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { EntityPolicies } from '@backstage/catalog-model';
export default async function ({ logger, database }: PluginEnvironment) {
const reader = LocationReaders.create();
const parser = DescriptorParsers.create();
const policy = new EntityPolicies();
const ingestion = new IngestionModels(
new LocationReaders(),
new DescriptorParsers(),
new EntityPolicies(),
);
const db = await DatabaseManager.createDatabase(database, logger);
runPeriodically(
() => DatabaseManager.refreshLocations(db, reader, parser, logger),
() => DatabaseManager.refreshLocations(db, ingestion, policy, logger),
10000,
);
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db, reader);
const locationsCatalog = new DatabaseLocationsCatalog(db, ingestion);
return await createRouter({ entitiesCatalog, locationsCatalog, logger });
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+12
View File
@@ -0,0 +1,12 @@
# Catalog Model
Contains the core model types and validators/policies used by the Backstage catalog functionality.
This package will be imported both by the frontend and backend parts of the catalog,
as well as by others that want to consume catalog data.
## Links
- (Default frontend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog]
- (Default backend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog-backend]
- (The Backstage homepage)[https://backstage.io]
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.6",
"main": "dist/index.esm.js",
"main:src": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "backstage-cli plugin:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"lodash": "^4.17.15",
"yup": "^0.28.5"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.6",
"@types/jest": "^25.2.2",
"@types/lodash": "^4.14.151",
"@types/yup": "^0.28.2",
"yaml": "^1.9.2"
},
"files": [
"dist/**/*.{js,d.ts}"
]
}
@@ -0,0 +1,88 @@
/*
* 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 {
Entity,
FieldFormatEntityPolicy,
NoForeignRootFieldsEntityPolicy,
ReservedFieldsEntityPolicy,
SchemaValidEntityPolicy,
} from './entity';
import { ComponentV1beta1Policy } from './kinds';
import { EntityPolicy } from './types';
// Helper that requires that all of a set of policies can be successfully
// applied
class AllEntityPolicies implements EntityPolicy {
constructor(private readonly policies: EntityPolicy[]) {}
async enforce(entity: Entity): Promise<Entity> {
let result = entity;
for (const policy of this.policies) {
result = await policy.enforce(entity);
}
return result;
}
}
// Helper that requires that at least one of a set of policies can be
// successfully applied
class AnyEntityPolicy implements EntityPolicy {
constructor(private readonly policies: EntityPolicy[]) {}
async enforce(entity: Entity): Promise<Entity> {
for (const policy of this.policies) {
try {
return await policy.enforce(entity);
} catch {
continue;
}
}
throw new Error(`The entity did not match any known policy`);
}
}
export class EntityPolicies implements EntityPolicy {
private readonly policy: EntityPolicy;
static defaultPolicies(): EntityPolicy {
return EntityPolicies.allOf([
EntityPolicies.allOf([
new SchemaValidEntityPolicy(),
new NoForeignRootFieldsEntityPolicy(),
new FieldFormatEntityPolicy(),
new ReservedFieldsEntityPolicy(),
]),
EntityPolicies.anyOf([new ComponentV1beta1Policy()]),
]);
}
static allOf(policies: EntityPolicy[]): EntityPolicy {
return new AllEntityPolicies(policies);
}
static anyOf(policies: EntityPolicy[]): EntityPolicy {
return new AnyEntityPolicy(policies);
}
constructor(policy: EntityPolicy = EntityPolicies.defaultPolicies()) {
this.policy = policy;
}
enforce(entity: Entity): Promise<Entity> {
return this.policy.enforce(entity);
}
}
+108
View File
@@ -0,0 +1,108 @@
/*
* 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.
*/
/**
* The format envelope that's common to all versions/kinds of entity.
*
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/
*/
export type Entity = {
/**
* The version of specification format for this particular entity that
* this is written against.
*/
apiVersion: string;
/**
* The high level entity type being described.
*/
kind: string;
/**
* Metadata related to the entity.
*/
metadata: EntityMeta;
/**
* The specification data describing the entity itself.
*/
spec?: object;
};
/**
* Metadata fields common to all versions/kinds of entity.
*
* @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/
*/
export type EntityMeta = {
/**
* A globally unique ID for the entity.
*
* This field can not be set by the user at creation time, and the server
* will reject an attempt to do so. The field will be populated in read
* operations. The field can (optionally) be specified when performing
* update or delete operations, but the server is free to reject requests
* that do so in such a way that it breaks semantics.
*/
uid?: string;
/**
* An opaque string that changes for each update operation to any part of
* the entity, including metadata.
*
* This field can not be set by the user at creation time, and the server
* will reject an attempt to do so. The field will be populated in read
* operations. The field can (optionally) be specified when performing
* update or delete operations, and the server will then reject the
* operation if it does not match the current stored value.
*/
etag?: string;
/**
* A positive nonzero number that indicates the current generation of data
* for this entity; the value is incremented each time the spec changes.
*
* This field can not be set by the user at creation time, and the server
* will reject an attempt to do so. The field will be populated in read
* operations.
*/
generation?: number;
/**
* The name of the entity.
*
* Must be uniqe within the catalog at any given point in time, for any
* given namespace + kind pair.
*/
name: string;
/**
* The namespace that the entity belongs to.
*/
namespace?: string;
/**
* Key/value pairs of identifying information attached to the entity.
*/
labels?: Record<string, string>;
/**
* Key/value pairs of non-identifying auxiliary information attached to the
* entity.
*/
annotations?: Record<string, string>;
};
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export * from './icons';
export * from './types';
export type { Entity, EntityMeta } from './Entity';
export * from './policies';
@@ -0,0 +1,105 @@
/*
* 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 yaml from 'yaml';
import { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy';
describe('FieldFormatEntityPolicy', () => {
let data: any;
let policy: FieldFormatEntityPolicy;
beforeEach(() => {
data = yaml.parse(`
apiVersion: backstage.io/v1beta1
kind: Component
metadata:
uid: e01199ab-08cc-44c2-8e19-5c29ded82521
etag: lsndfkjsndfkjnsdfkjnsd==
generation: 13
name: my-component-yay
namespace: the-namespace
labels:
backstage.io/custom: ValueStuff
annotations:
example.com/bindings: are-secret
spec:
custom: stuff
`);
policy = new FieldFormatEntityPolicy();
});
it('works for the happy path', async () => {
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad apiVersion', async () => {
data.apiVersion = 7;
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
data.apiVersion = 'a#b';
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
});
it('rejects bad kind', async () => {
data.kind = 7;
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
data.kind = 'a#b';
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
});
it('handles missing metadata gracefully', async () => {
delete data.medatata;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('handles missing spec gracefully', async () => {
delete data.spec;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad name', async () => {
data.metadata.name = 7;
await expect(policy.enforce(data)).rejects.toThrow(/name.*7/);
data.metadata.name = 'a'.repeat(1000);
await expect(policy.enforce(data)).rejects.toThrow(/name.*aaaa/);
});
it('rejects bad namespace', async () => {
data.metadata.namespace = 7;
await expect(policy.enforce(data)).rejects.toThrow(/namespace.*7/);
data.metadata.namespace = 'a'.repeat(1000);
await expect(policy.enforce(data)).rejects.toThrow(/namespace.*aaaa/);
});
it('rejects bad label key', async () => {
data.metadata.labels['a#b'] = 'value';
await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i);
});
it('rejects bad label value', async () => {
data.metadata.labels.a = 'a#b';
await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i);
});
it('rejects bad annotation key', async () => {
data.metadata.annotations['a#b'] = 'value';
await expect(policy.enforce(data)).rejects.toThrow(/annotation.*a#b/i);
});
it('rejects bad annotation value', async () => {
data.metadata.annotations.a = 7;
await expect(policy.enforce(data)).rejects.toThrow(/annotation.*7/i);
});
});
@@ -0,0 +1,88 @@
/*
* 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 { makeValidator, Validators } from '../../validation';
import { Entity } from '../Entity';
/**
* Ensures that the format of individual fields of the entity envelope
* is valid.
*
* This does not take into account machine generated fields such as uid, etag
* and generation.
*/
export class FieldFormatEntityPolicy implements EntityPolicy {
private readonly validators: Validators;
constructor(validators: Validators = makeValidator()) {
this.validators = validators;
}
async enforce(entity: Entity): Promise<Entity> {
function require(
field: string,
value: any,
validator: (value: any) => boolean,
) {
if (value === undefined || value === null) {
throw new Error(`${field} must have a value`);
}
let isValid: boolean;
try {
isValid = validator(value);
} catch (e) {
throw new Error(`${field} could not be validated, ${e}`);
}
if (!isValid) {
throw new Error(`${field} "${value}" is not valid`);
}
}
function optional(
field: string,
value: any,
validator: (value: any) => boolean,
) {
return value === undefined || require(field, value, validator);
}
require('apiVersion', entity.apiVersion, this.validators.isValidApiVersion);
require('kind', entity.kind, this.validators.isValidKind);
require('metadata.name', entity.metadata.name, this.validators
.isValidEntityName);
optional(
'metadata.namespace',
entity.metadata.namespace,
this.validators.isValidNamespace,
);
for (const [k, v] of Object.entries(entity.metadata.labels ?? [])) {
require(`labels.${k}`, k, this.validators.isValidLabelKey);
require(`labels.${k}`, v, this.validators.isValidLabelValue);
}
for (const [k, v] of Object.entries(entity.metadata.annotations ?? [])) {
require(`annotations.${k}`, k, this.validators.isValidAnnotationKey);
require(`annotations.${k}`, v, this.validators.isValidAnnotationValue);
}
return entity;
}
}
@@ -0,0 +1,52 @@
/*
* 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 yaml from 'yaml';
import { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy';
describe('NoForeignRootFieldsEntityPolicy', () => {
let data: any;
let policy: NoForeignRootFieldsEntityPolicy;
beforeEach(() => {
data = yaml.parse(`
apiVersion: backstage.io/v1beta1
kind: Component
metadata:
uid: e01199ab-08cc-44c2-8e19-5c29ded82521
etag: lsndfkjsndfkjnsdfkjnsd==
generation: 13
name: my-component-yay
namespace: the-namespace
labels:
backstage.io/custom: ValueStuff
annotations:
example.com/bindings: are-secret
spec:
custom: stuff
`);
policy = new NoForeignRootFieldsEntityPolicy();
});
it('works for the happy path', async () => {
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects unknown root fields', async () => {
data.spec2 = {};
await expect(policy.enforce(data)).rejects.toThrow(/spec2/i);
});
});
@@ -0,0 +1,40 @@
/*
* 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 { Entity } from '../Entity';
const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec'];
/**
* Ensures that there are no foreign root fields in the entity.
*/
export class NoForeignRootFieldsEntityPolicy implements EntityPolicy {
private readonly knownFields: string[];
constructor(knownFields: string[] = defaultKnownFields) {
this.knownFields = knownFields;
}
async enforce(entity: Entity): Promise<Entity> {
for (const field of Object.keys(entity)) {
if (!this.knownFields.includes(field)) {
throw new Error(`Unknown field ${field}`);
}
}
return entity;
}
}
@@ -0,0 +1,64 @@
/*
* 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 yaml from 'yaml';
import { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy';
describe('ReservedFieldsEntityPolicy', () => {
let data: any;
let policy: ReservedFieldsEntityPolicy;
beforeEach(() => {
data = yaml.parse(`
apiVersion: backstage.io/v1beta1
kind: Component
metadata:
uid: e01199ab-08cc-44c2-8e19-5c29ded82521
etag: lsndfkjsndfkjnsdfkjnsd==
generation: 13
name: my-component-yay
namespace: the-namespace
labels:
backstage.io/custom: ValueStuff
annotations:
example.com/bindings: are-secret
spec:
custom: stuff
`);
policy = new ReservedFieldsEntityPolicy();
});
it('works for the happy path', async () => {
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects reserved keys in the spec root', async () => {
data.spec.apiVersion = 'a/b';
await expect(policy.enforce(data)).rejects.toThrow(/spec.*apiVersion/i);
});
it('rejects reserved keys in labels', async () => {
data.metadata.labels.apiVersion = 'a';
await expect(policy.enforce(data)).rejects.toThrow(/label.*apiVersion/i);
});
it('rejects reserved keys in annotations', async () => {
data.metadata.annotations.apiVersion = 'a';
await expect(policy.enforce(data)).rejects.toThrow(
/annotation.*apiVersion/i,
);
});
});
@@ -0,0 +1,66 @@
/*
* 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 { Entity } from '../Entity';
const DEFAULT_RESERVED_ENTITY_FIELDS = [
'apiVersion',
'kind',
'uid',
'etag',
'generation',
'name',
'namespace',
'labels',
'annotations',
'spec',
];
/**
* Ensures that fields are not given certain reserved names.
*/
export class ReservedFieldsEntityPolicy implements EntityPolicy {
private readonly reservedFields: string[];
constructor(fields?: string[]) {
this.reservedFields = [
...(fields ?? []),
...DEFAULT_RESERVED_ENTITY_FIELDS,
];
}
async enforce(entity: Entity): Promise<Entity> {
for (const field of this.reservedFields) {
if (entity.spec?.hasOwnProperty(field)) {
throw new Error(
`The spec may not contain the field ${field}, because it has reserved meaning`,
);
}
if (entity.metadata.labels?.hasOwnProperty(field)) {
throw new Error(
`A label may not have the field ${field}, because it has reserved meaning`,
);
}
if (entity.metadata.annotations?.hasOwnProperty(field)) {
throw new Error(
`An annotation may not have the field ${field}, because it has reserved meaning`,
);
}
}
return entity;
}
}
@@ -0,0 +1,176 @@
/*
* 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 yaml from 'yaml';
import { Entity } from '../Entity';
import { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy';
describe('SchemaValidEntityPolicy', () => {
let data: any;
let policy: SchemaValidEntityPolicy;
beforeEach(() => {
data = yaml.parse(`
apiVersion: backstage.io/v1beta1
kind: Component
metadata:
uid: e01199ab-08cc-44c2-8e19-5c29ded82521
etag: lsndfkjsndfkjnsdfkjnsd==
generation: 13
name: my-component-yay
namespace: the-namespace
labels:
backstage.io/custom: ValueStuff
annotations:
example.com/bindings: are-secret
spec:
custom: stuff
`);
policy = new SchemaValidEntityPolicy();
});
it('works for the happy path', async () => {
await expect(policy.enforce(data)).resolves.toBe(data);
});
//
// apiVersion and kind
//
it('rejects wrong root type', async () => {
await expect(policy.enforce((7 as unknown) as Entity)).rejects.toThrow(
/object/,
);
});
it('rejects missing apiVersion', async () => {
delete data.apiVersion;
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
});
it('rejects bad apiVersion type', async () => {
data.apiVersion = 7;
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
});
it('rejects missing kind', async () => {
delete data.kind;
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
});
it('rejects bad kind type', async () => {
data.kind = 7;
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
});
//
// metadata
//
it('rejects missing metadata', async () => {
delete data.metadata;
await expect(policy.enforce(data)).rejects.toThrow(/metadata/);
});
it('rejects bad metadata type', async () => {
data.metadata = 7;
await expect(policy.enforce(data)).rejects.toThrow(/metadata/);
});
it('accepts missing uid', async () => {
delete data.metadata.uid;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad uid type', async () => {
data.metadata.uid = 7;
await expect(policy.enforce(data)).rejects.toThrow(/uid/);
});
it('accepts missing etag', async () => {
delete data.metadata.etag;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad etag type', async () => {
data.metadata.etag = 7;
await expect(policy.enforce(data)).rejects.toThrow(/etag/);
});
it('accepts missing generation', async () => {
delete data.metadata.generation;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad generation type', async () => {
data.metadata.generation = 'a';
await expect(policy.enforce(data)).rejects.toThrow(/generation/);
});
it('rejects missing name', async () => {
delete data.metadata.name;
await expect(policy.enforce(data)).rejects.toThrow(/name/);
});
it('rejects bad name type', async () => {
data.metadata.name = 7;
await expect(policy.enforce(data)).rejects.toThrow(/name/);
});
it('accepts missing namespace', async () => {
delete data.metadata.namespace;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad namespace type', async () => {
data.metadata.namespace = 7;
await expect(policy.enforce(data)).rejects.toThrow(/namespace/);
});
it('accepts missing labels', async () => {
delete data.metadata.labels;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad labels type', async () => {
data.metadata.labels = 7;
await expect(policy.enforce(data)).rejects.toThrow(/labels/);
});
it('accepts missing annotations', async () => {
delete data.metadata.annotations;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad annotations type', async () => {
data.metadata.annotations = 7;
await expect(policy.enforce(data)).rejects.toThrow(/annotations/);
});
//
// spec
//
it('accepts missing spec', async () => {
delete data.spec;
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects non-object spec', async () => {
data.spec = 7;
await expect(policy.enforce(data)).rejects.toThrow(/spec/);
});
});
@@ -0,0 +1,80 @@
/*
* 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 { EntityPolicy } from '../../types';
import { Entity } from '../Entity';
const DEFAULT_ENTITY_SCHEMA = yup.object({
apiVersion: yup.string().required(),
kind: yup.string().required(),
metadata: yup
.object({
uid: yup
.string()
.notRequired()
.test(
'metadata.uid',
'The uid must not be empty',
value => value === undefined || value.length > 0,
),
etag: yup
.string()
.notRequired()
.test(
'metadata.etag',
'The etag must not be empty',
value => value === undefined || value.length > 0,
),
generation: yup
.number()
.notRequired()
.test(
'metadata.generation',
'The generation must be an integer greater than zero',
value => value === undefined || (value === (value | 0) && value > 0),
),
name: yup.string().required(),
namespace: yup.string().notRequired(),
labels: yup.object<Record<string, string>>().notRequired(),
annotations: yup.object<Record<string, string>>().notRequired(),
})
.required(),
spec: yup.object({}).notRequired(),
});
/**
* Ensures that the entity spec is valid according to a schema.
*
* This should be the first policy in the list, to ensure that other downstream
* policies can work with a structure that is at least valid in therms of the
* typescript type.
*/
export class SchemaValidEntityPolicy implements EntityPolicy {
private readonly schema: yup.Schema<Entity>;
constructor(schema: yup.Schema<Entity> = DEFAULT_ENTITY_SCHEMA) {
this.schema = schema;
}
async enforce(entity: Entity): Promise<Entity> {
try {
return await this.schema.validate(entity, { strict: true });
} catch (e) {
throw new Error(`Malformed envelope, ${e}`);
}
}
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy';
export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy';
export { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy';
export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy';
@@ -14,8 +14,8 @@
* limitations under the License.
*/
describe('dummy', () => {
it('dummy', () => {
expect(1).toBe(1);
});
});
export * from './entity';
export { EntityPolicies } from './EntityPolicies';
export * from './kinds';
export type { EntityPolicy } from './types';
export * from './validation';
@@ -0,0 +1,63 @@
/*
* 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, EntityMeta } from '../entity/Entity';
import type { EntityPolicy } from '../types';
const API_VERSION = 'backstage.io/v1beta1';
const KIND = 'Component';
export interface ComponentV1beta1 extends Entity {
apiVersion: typeof API_VERSION;
kind: typeof KIND;
metadata: EntityMeta & {
name: string;
};
spec: {
type: string;
};
}
export class ComponentV1beta1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<ComponentV1beta1>>({
metadata: yup
.object({
name: yup.string().required(),
})
.required(),
spec: yup
.object({
type: yup.string().required(),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity> {
if (
envelope.apiVersion !== 'backstage.io/v1beta1' ||
envelope.kind !== 'Component'
) {
throw new Error('Unsupported apiVersion / kind');
}
return await this.schema.validate(envelope, { strict: true });
}
}
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { ComponentType } from 'react';
import { SvgIconProps } from '@material-ui/core';
export type IconComponent = ComponentType<SvgIconProps>;
export type SystemIconKey = 'user' | 'group';
export type SystemIcons = { [key in SystemIconKey]: IconComponent };
import type { ComponentV1beta1 } from './ComponentV1beta1';
export { ComponentV1beta1Policy } from './ComponentV1beta1';
export { ComponentV1beta1 as Component };
export { ComponentV1beta1 };
+32
View File
@@ -0,0 +1,32 @@
/*
* 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 type { Entity } from './entity/Entity';
/**
* A policy for validation or mutation to be applied to entities as they are
* entering the system.
*/
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
* @throws An error if the entity should be rejected
*/
enforce(entity: Entity): Promise<Entity>;
};
@@ -0,0 +1,178 @@
/*
* 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 { CommonValidatorFunctions } from './CommonValidatorFunctions';
describe('CommonValidatorFunctions', () => {
describe('isValidPrefixAndOrSuffix', () => {
it('only accepts strings', () => {
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
null,
'/',
() => true,
() => true,
),
).toBe(false);
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
7,
'/',
() => true,
() => true,
),
).toBe(false);
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
() => 'hello',
'/',
() => true,
() => true,
),
).toBe(false);
});
it('only accepts one or two parts', () => {
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
'a',
'/',
() => true,
() => true,
),
).toBe(true);
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
'a/b',
'/',
() => true,
() => true,
),
).toBe(true);
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
'a/b/c',
'/',
() => true,
() => true,
),
).toBe(false);
});
it('checks the prefix and suffix', () => {
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
'a/b',
'/',
() => true,
() => true,
),
).toBe(true);
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
'a/b',
'/',
() => false,
() => true,
),
).toBe(false);
expect(
CommonValidatorFunctions.isValidPrefixAndOrSuffix(
'a/b',
'/',
() => true,
() => false,
),
).toBe(false);
});
});
it.each([
[null, true],
[undefined, false],
[1, true],
['a', true],
[() => 'a', false],
[Symbol('a'), false],
[[], true],
[[1], true],
[[undefined], false],
[{}, true],
[{ a: 1 }, true],
[{ a: undefined }, false],
] as [any, boolean][])(`isJsonSafe %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isJsonSafe(value)).toBe(result);
});
it.each([
[null, false],
[7, false],
['', false],
['a', true],
['a-b', true],
['-a-b', false],
['a-b-', false],
['a--b', false],
['a_b', false],
['adam.bertil.caesar', true],
['adam.ber-til.caesar', true],
['adam.-bertil.caesar', false],
['adam.bertil-.caesar', false],
['adam/bertil.caesar', false],
[`a.${'b'.repeat(63)}.c`, true],
[`a.${'b'.repeat(64)}.c`, false],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(61)}`,
true,
],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(62)}`,
false,
],
])(`isValidDnsSubdomain %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isValidDnsSubdomain(value)).toBe(result);
});
it.each([
[null, false],
[7, false],
['', false],
['a', true],
['a-b', true],
['-a-b', false],
['a-b-', false],
['a--b', false],
['a_b', false],
[`${'a'.repeat(63)}`, true],
[`${'a'.repeat(64)}`, false],
])(`isValidDnsLabel %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result);
});
it.each([
['', ''],
['a', 'a'],
['a-b', 'ab'],
['-a-b', 'ab'],
['a_b', 'ab'],
[`${'a'.repeat(6000)}`, `${'a'.repeat(6000)}`],
['_:;>!"#€', ''],
])(`normalizeToLowercaseAlphanum %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.normalizeToLowercaseAlphanum(value)).toBe(
result,
);
});
});
@@ -0,0 +1,108 @@
/*
* 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 lodash from 'lodash';
/**
* Contains various helper validation and normalization functions that can be
* composed to form a Validator.
*/
export class CommonValidatorFunctions {
/**
* Checks that the value is on the form <suffix> or <prefix><separator><suffix>, and validates
* those parts separately.
*
* @param value The value to check
* @param separator The separator between parts
* @param isValidPrefix Checks that the part before the separator is valid, if present
* @param isValidSuffix Checks that the part after the separator (or the entire value if there is no separator) is valid
*/
static isValidPrefixAndOrSuffix(
value: any,
separator: string,
isValidPrefix: (value: string) => boolean,
isValidSuffix: (value: string) => boolean,
): boolean {
if (typeof value !== 'string') {
return false;
}
const parts = value.split(separator);
if (parts.length === 1) {
return isValidSuffix(parts[0]);
} else if (parts.length === 2) {
return isValidPrefix(parts[0]) && isValidSuffix(parts[1]);
}
return false;
}
/**
* Checks that the value can be safely transferred as JSON.
*
* @param value The value to check
*/
static isJsonSafe(value: any): boolean {
try {
return lodash.isEqual(value, JSON.parse(JSON.stringify(value)));
} catch {
return false;
}
}
/**
* Checks that the value is a valid DNS subdomain name.
*
* @param value The value to check
* @see https://tools.ietf.org/html/rfc1123
*/
static isValidDnsSubdomain(value: any): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
value.length <= 253 &&
value.split('.').every(CommonValidatorFunctions.isValidDnsLabel)
);
}
/**
* Checks that the value is a valid DNS label.
*
* @param value The value to check
* @see https://tools.ietf.org/html/rfc1123
*/
static isValidDnsLabel(value: any): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
value.length <= 63 &&
/^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value)
);
}
/**
* Normalizes by keeping only a-z, A-Z, and 0-9; and converts to lowercase.
*
* @param value The value to normalize
*/
static normalizeToLowercaseAlphanum(value: string): string {
return value
.split('')
.filter(x => /[a-zA-Z0-9]/.test(x))
.join('')
.toLowerCase();
}
}
@@ -0,0 +1,209 @@
/*
* 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 { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions';
describe('KubernetesValidatorFunctions', () => {
it.each([
[7, false],
[null, false],
['', false],
['a', true],
['AZ09', true],
['a'.repeat(63), true],
['a'.repeat(64), false],
['a-b', false],
['a_b', false],
['a.b', false],
['a/a', true],
['a/aAb5C', true],
['a-b.c/v1', true],
['a--b.c/v1', false],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(
61,
)}/v1`,
true,
],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(
62,
)}/v1`,
false,
],
[`a/${'a'.repeat(63)}`, true],
[`a/${'a'.repeat(64)}`, false],
])(`isValidApiVersion %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidApiVersion(value)).toBe(matches);
});
it.each([
[7, false],
[null, false],
['', false],
['a', true],
['AZ09', true],
['9AZ', false],
['a'.repeat(63), true],
['a'.repeat(64), false],
['a-b', false],
])(`isValidKind %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidKind(value)).toBe(matches);
});
it.each([
[7, false],
[null, false],
['', false],
['a', true],
['AZ09', true],
['a'.repeat(63), true],
['a'.repeat(64), false],
['a/b', false],
['a-b', true],
['-a-b', false],
['a-b-', false],
['a--b', false],
['a_b', true],
['a.b', true],
])(`isValidObjectName %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidObjectName(value)).toBe(matches);
});
it.each([
[7, false],
[null, false],
['', false],
['a', true],
['AZ09', false],
['a'.repeat(63), true],
['a'.repeat(64), false],
['a/b', false],
['a-b', true],
['-a-b', false],
['a-b-', false],
['a--b', false],
['a_b', false],
['a.b', false],
])(`isValidNamespace %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidNamespace(value)).toBe(matches);
});
it.each([
[7, false],
[null, false],
['', false],
['a', true],
['AZ09', true],
['a'.repeat(63), true],
['a'.repeat(64), false],
['a/b', true],
['a-b', true],
['-a-b', false],
['a-b-', false],
['a--b', false],
['a_b', true],
['a.b', true],
['a/a', true],
['a-b.c/a', true],
['a--b.c/a', false],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(
61,
)}/a`,
true,
],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(
62,
)}/a`,
false,
],
[`a/${'a'.repeat(63)}`, true],
[`a/${'a'.repeat(64)}`, false],
])(`isValidLabelKey %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidLabelKey(value)).toBe(matches);
});
it.each([
[7, false],
[null, false],
['', true],
['a', true],
['AZ09', true],
['a'.repeat(63), true],
['a'.repeat(64), false],
['a/b', false],
['a-b', true],
['-a-b', false],
['a-b-', false],
['a--b', false],
['a_b', true],
['a.b', true],
])(`isValidLabelValue %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidLabelValue(value)).toBe(matches);
});
it.each([
[7, false],
[null, false],
['', false],
['a', true],
['AZ09', true],
['a'.repeat(63), true],
['a'.repeat(64), false],
['a/b', true],
['a-b', true],
['-a-b', false],
['a-b-', false],
['a--b', false],
['a_b', true],
['a.b', true],
['a/a', true],
['a-b.c/a', true],
['a--b.c/a', false],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(
61,
)}/a`,
true,
],
[
`${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(
62,
)}/a`,
false,
],
[`a/${'a'.repeat(63)}`, true],
[`a/${'a'.repeat(64)}`, false],
])(`isValidAnnotationKey %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidAnnotationKey(value)).toBe(
matches,
);
});
it.each([
[7, false],
[null, false],
['', true],
['a', true],
['/'.repeat(6000), true],
])(`isValidAnnotationValue %p ? %p`, (value, matches) => {
expect(KubernetesValidatorFunctions.isValidAnnotationValue(value)).toBe(
matches,
);
});
});
@@ -0,0 +1,86 @@
/*
* 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 { CommonValidatorFunctions } from './CommonValidatorFunctions';
/**
* Contains validation functions that match the Kubernetes spec, usable to
* build a catalog that is compatible with those rule sets.
*
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
*/
export class KubernetesValidatorFunctions {
static isValidApiVersion(value: any): boolean {
return CommonValidatorFunctions.isValidPrefixAndOrSuffix(
value,
'/',
CommonValidatorFunctions.isValidDnsSubdomain,
n => n.length >= 1 && n.length <= 63 && /^[a-z0-9A-Z]+$/.test(n),
);
}
static isValidKind(value: any): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
value.length <= 63 &&
/^[a-zA-Z][a-z0-9A-Z]*$/.test(value)
);
}
static isValidObjectName(value: any): boolean {
return (
typeof value === 'string' &&
value.length >= 1 &&
value.length <= 63 &&
/^[a-z0-9A-Z]+([-_.][a-z0-9A-Z]+)*$/.test(value)
);
}
static isValidNamespace(value: any): boolean {
return CommonValidatorFunctions.isValidDnsLabel(value);
}
static isValidLabelKey(value: any): boolean {
return CommonValidatorFunctions.isValidPrefixAndOrSuffix(
value,
'/',
CommonValidatorFunctions.isValidDnsSubdomain,
KubernetesValidatorFunctions.isValidObjectName,
);
}
static isValidLabelValue(value: any): boolean {
return (
value === '' || KubernetesValidatorFunctions.isValidObjectName(value)
);
}
static isValidAnnotationKey(value: any): boolean {
return CommonValidatorFunctions.isValidPrefixAndOrSuffix(
value,
'/',
CommonValidatorFunctions.isValidDnsSubdomain,
KubernetesValidatorFunctions.isValidObjectName,
);
}
static isValidAnnotationValue(value: any): boolean {
return typeof value === 'string';
}
}
@@ -13,16 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext } from 'react';
import { SidebarPinStateContext } from '../layout/Sidebar';
export function useSidebarPinState() {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
return {
isPinned,
toggleSidebarPinState,
};
}
export { CommonValidatorFunctions } from './CommonValidatorFunctions';
export { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions';
export { makeValidator } from './makeValidator';
export type { Validators } from './types';
@@ -0,0 +1,38 @@
/*
* 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 { CommonValidatorFunctions } from './CommonValidatorFunctions';
import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions';
import { Validators } from './types';
const defaultValidators: Validators = {
isValidApiVersion: KubernetesValidatorFunctions.isValidApiVersion,
isValidKind: KubernetesValidatorFunctions.isValidKind,
isValidEntityName: KubernetesValidatorFunctions.isValidObjectName,
isValidNamespace: KubernetesValidatorFunctions.isValidNamespace,
normalizeEntityName: CommonValidatorFunctions.normalizeToLowercaseAlphanum,
isValidLabelKey: KubernetesValidatorFunctions.isValidLabelKey,
isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue,
isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey,
isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue,
};
export function makeValidator(overrides: Partial<Validators> = {}): Validators {
return {
...defaultValidators,
...overrides,
};
}
@@ -0,0 +1,27 @@
/*
* 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.
*/
export type Validators = {
isValidApiVersion(value: any): boolean;
isValidKind(value: any): boolean;
isValidEntityName(value: any): boolean;
isValidNamespace(value: any): boolean;
normalizeEntityName(value: string): string;
isValidLabelKey(value: any): boolean;
isValidLabelValue(value: any): boolean;
isValidAnnotationKey(value: any): boolean;
isValidAnnotationValue(value: any): boolean;
};
@@ -16,33 +16,10 @@
import React from 'react';
import CopyTextButton from '.';
import {
ApiProvider,
errorApiRef,
ApiRegistry,
ErrorApi,
} from '@backstage/core-api';
export default {
title: 'CopyTextButton',
component: CopyTextButton,
decorators: [
(storyFn: () => JSX.Element) => {
// TODO: move this to common storybook config, requires core package to be separate from components
const registry = ApiRegistry.from([
[
errorApiRef,
{
post(error) {
// eslint-disable-next-line no-alert
window.alert(`Component posted error, ${error}`);
},
} as ErrorApi,
],
]);
return <ApiProvider apis={registry} children={storyFn()} />;
},
],
};
export const Default = () => (
-39
View File
@@ -1,39 +0,0 @@
/*
* 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 { SvgIconProps } from '@material-ui/core';
import PeopleIcon from '@material-ui/icons/People';
import PersonIcon from '@material-ui/icons/Person';
import React, { FC } from 'react';
import { useApp } from '@backstage/core-api';
import { IconComponent, SystemIconKey, SystemIcons } from './types';
export const defaultSystemIcons: SystemIcons = {
user: PersonIcon,
group: PeopleIcon,
};
const overridableSystemIcon = (key: SystemIconKey): IconComponent => {
const Component: FC<SvgIconProps> = props => {
const app = useApp();
const Icon = app.getSystemIcon(key);
return <Icon {...props} />;
};
return Component;
};
export const UserIcon = overridableSystemIcon('user');
export const GroupIcon = overridableSystemIcon('group');
+1 -2
View File
@@ -16,7 +16,7 @@
export * from '@backstage/core-api';
export * from './api';
export * from './api-wrappers';
export * from './layout';
export { default as CodeSnippet } from './components/CodeSnippet';
@@ -39,4 +39,3 @@ export { default as TrendLine } from './components/TrendLine';
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
export * from './components/Status';
export { default as WarningPanel } from './components/WarningPanel';
export type { IconComponent } from './icons';
+3 -3
View File
@@ -16,10 +16,10 @@
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import React, { FC, useRef, useState } from 'react';
import React, { FC, useRef, useState, useContext } from 'react';
import { sidebarConfig, SidebarContext } from './config';
import { BackstageTheme } from '@backstage/theme';
import { useSidebarPinState } from '../../hooks/useSidebarPinState';
import { SidebarPinStateContext } from './Page';
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
@@ -76,7 +76,7 @@ export const Sidebar: FC<Props> = ({
const classes = useStyles();
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef<number>();
const { isPinned } = useSidebarPinState();
const { isPinned } = useContext(SidebarPinStateContext);
const handleOpen = () => {
if (isPinned) {
+1 -1
View File
@@ -22,12 +22,12 @@ import {
Typography,
Badge,
} from '@material-ui/core';
import { IconComponent } from '@backstage/core-api';
import SearchIcon from '@material-ui/icons/Search';
import clsx from 'clsx';
import React, { FC, useContext, useState, KeyboardEventHandler } from 'react';
import { NavLink } from 'react-router-dom';
import { sidebarConfig, SidebarContext } from './config';
import { IconComponent } from '../../icons';
const useStyles = makeStyles<Theme>(theme => {
const {
+1 -1
View File
@@ -18,7 +18,7 @@ import { makeStyles } from '@material-ui/core';
import React, { createContext, FC, useEffect, useState } from 'react';
import { sidebarConfig } from './config';
import { BackstageTheme } from '@backstage/theme';
import { LocalStorage } from '../../data/localStorage';
import { LocalStorage } from './localStorage';
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>({
root: {
+16
View File
@@ -0,0 +1,16 @@
import {
ApiRegistry,
alertApiRef,
errorApiRef,
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
} from '@backstage/core';
const builder = ApiRegistry.builder();
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
export const apis = builder.build();
+11 -7
View File
@@ -3,14 +3,18 @@ import { addDecorator, addParameters } from '@storybook/react';
import { lightTheme, darkTheme } from '@backstage/theme';
import { CssBaseline, ThemeProvider } from '@material-ui/core';
import { useDarkMode } from 'storybook-dark-mode';
import { Content } from '@backstage/core';
import { Content, ApiProvider, AlertDisplay } from '@backstage/core';
import { apis } from './apis';
addDecorator((story) => (
<ThemeProvider theme={useDarkMode() ? darkTheme : lightTheme}>
<CssBaseline>
<Content>{story()}</Content>
</CssBaseline>
</ThemeProvider>
addDecorator(story => (
<ApiProvider apis={apis}>
<ThemeProvider theme={useDarkMode() ? darkTheme : lightTheme}>
<CssBaseline>
<AlertDisplay />
<Content>{story()}</Content>
</CssBaseline>
</ThemeProvider>
</ApiProvider>
));
addParameters({