Merge remote-tracking branch 'upstream/master' into ndudnik/use-catalog-backend
This commit is contained in:
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
export {};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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]
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
+4
-12
@@ -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 = () => (
|
||||
|
||||
@@ -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');
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user