Refactor entity validation in catalog-model
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
---
|
||||
'@backstage/catalog-model': minor
|
||||
---
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- The long-deprecated `schemaValidator` export is finally removed.
|
||||
|
||||
Additions:
|
||||
|
||||
- The `EntityEnvelope` type, which is a supertype of `Entity`.
|
||||
- The `entityEnvelopeSchemaValidator` function, which returns a validator for an `EntityEnvelope` or its subtypes, based on a JSON schema.
|
||||
- The `entitySchemaValidator` function, which returns a validator for an `Entity` or its subtypes, based on a JSON schema.
|
||||
- The `entityKindSchemaValidator` function, which returns a specialized validator for custom `Entity` kinds, based on a JSON schema.
|
||||
+96
-94
@@ -1,133 +1,58 @@
|
||||
Apdex
|
||||
Api
|
||||
automations
|
||||
Autoscaling
|
||||
Avro
|
||||
Bigtable
|
||||
Billett
|
||||
Bitrise
|
||||
Blackbox
|
||||
Chai
|
||||
Changesets
|
||||
Chanwit
|
||||
Cloudformation
|
||||
Cobertura
|
||||
Codecov
|
||||
Codehilite
|
||||
Config
|
||||
Datadog
|
||||
Debounce
|
||||
Discoverability
|
||||
Dockerfile
|
||||
dockerfiles
|
||||
Dockerize
|
||||
Docusaurus
|
||||
Env
|
||||
Expedia
|
||||
Fargate
|
||||
Figma
|
||||
Firekube
|
||||
Fiverr
|
||||
GitHub
|
||||
GitLab
|
||||
Grafana
|
||||
GraphQL
|
||||
Hackathons
|
||||
Helidon
|
||||
Heroku
|
||||
hoc
|
||||
Hostname
|
||||
Iain
|
||||
JaCoCo
|
||||
JavaScript
|
||||
Jira
|
||||
Kaewkasi
|
||||
kubernetes
|
||||
Knex
|
||||
Leasot
|
||||
Lerna
|
||||
LocalStack
|
||||
Luxon
|
||||
Minikube
|
||||
Minio
|
||||
Mkdocs
|
||||
Monorepo
|
||||
microservices
|
||||
microservice
|
||||
Namespaces
|
||||
OAuth
|
||||
Okta
|
||||
Onboarding
|
||||
Platformize
|
||||
Podman
|
||||
Preprarer
|
||||
Protobuf
|
||||
Proxying
|
||||
Readme
|
||||
readonly
|
||||
rebase
|
||||
Recharts
|
||||
Redash
|
||||
Repo
|
||||
Rollbar
|
||||
Rollup
|
||||
Routable
|
||||
Scaffolder
|
||||
Serverless
|
||||
Sinon
|
||||
Snyk
|
||||
Splunk
|
||||
Spotifiers
|
||||
Spotify
|
||||
Superfences
|
||||
Talkdesk
|
||||
Telenor
|
||||
Templater
|
||||
Templaters
|
||||
todo
|
||||
Tolerations
|
||||
WWW
|
||||
Weaveworks
|
||||
Webpack
|
||||
XML
|
||||
Zalando
|
||||
Zhou
|
||||
abc
|
||||
accessors
|
||||
Apdex
|
||||
api
|
||||
Api
|
||||
apis
|
||||
args
|
||||
asciidoc
|
||||
async
|
||||
automations
|
||||
autoscaling
|
||||
Autoscaling
|
||||
Avro
|
||||
backrub
|
||||
Bigtable
|
||||
Billett
|
||||
Bitrise
|
||||
Blackbox
|
||||
bool
|
||||
boolean
|
||||
builtins
|
||||
Chai
|
||||
changeset
|
||||
changesets
|
||||
Changesets
|
||||
chanwit
|
||||
Chanwit
|
||||
ci
|
||||
cisphobia
|
||||
cissexist
|
||||
classname
|
||||
cli
|
||||
cloudbuild
|
||||
Cloudformation
|
||||
cncf
|
||||
Cobertura
|
||||
codeblocks
|
||||
Codecov
|
||||
codehilite
|
||||
Codehilite
|
||||
codeowners
|
||||
composability
|
||||
composable
|
||||
config
|
||||
Config
|
||||
configmaps
|
||||
configs
|
||||
const
|
||||
cookiecutter
|
||||
css
|
||||
Datadog
|
||||
dataflow
|
||||
deadnaming
|
||||
debounce
|
||||
Debounce
|
||||
declaratively
|
||||
deps
|
||||
destructured
|
||||
@@ -135,60 +60,98 @@ dev
|
||||
devops
|
||||
devs
|
||||
discoverability
|
||||
Discoverability
|
||||
dls
|
||||
docgen
|
||||
Dockerfile
|
||||
dockerfiles
|
||||
Dockerize
|
||||
dockerode
|
||||
Docusaurus
|
||||
env
|
||||
Env
|
||||
esbuild
|
||||
eslint
|
||||
etag
|
||||
Expedia
|
||||
facto
|
||||
failover
|
||||
Fargate
|
||||
Figma
|
||||
Firekube
|
||||
Fiverr
|
||||
gitbeaker
|
||||
GitHub
|
||||
GitLab
|
||||
Grafana
|
||||
graphql
|
||||
GraphQL
|
||||
graphviz
|
||||
Hackathons
|
||||
haproxy
|
||||
Helidon
|
||||
Heroku
|
||||
hoc
|
||||
horizontalpodautoscalers
|
||||
Hostname
|
||||
hotspots
|
||||
html
|
||||
http
|
||||
https
|
||||
Iain
|
||||
img
|
||||
incentivised
|
||||
inlined
|
||||
inlinehilite
|
||||
interop
|
||||
JaCoCo
|
||||
JavaScript
|
||||
Jira
|
||||
jq
|
||||
js
|
||||
json
|
||||
jsonnet
|
||||
jsx
|
||||
Kaewkasi
|
||||
Knex
|
||||
kubectl
|
||||
kubernetes
|
||||
kubernetes
|
||||
learnings
|
||||
Leasot
|
||||
lerna
|
||||
Lerna
|
||||
LocalStack
|
||||
lockfile
|
||||
lunr
|
||||
Luxon
|
||||
magiclink
|
||||
mailto
|
||||
maintainership
|
||||
makefile
|
||||
md
|
||||
memcache
|
||||
microservice
|
||||
microservices
|
||||
microsite
|
||||
middleware
|
||||
minikube
|
||||
Minikube
|
||||
Minio
|
||||
misconfiguration
|
||||
misconfigured
|
||||
misgendering
|
||||
mkdocs
|
||||
Mkdocs
|
||||
monorepo
|
||||
Monorepo
|
||||
monorepos
|
||||
msw
|
||||
mysql
|
||||
namespace
|
||||
namespaced
|
||||
namespaces
|
||||
Namespaces
|
||||
namespacing
|
||||
neuro
|
||||
newrelic
|
||||
@@ -200,43 +163,67 @@ noop
|
||||
npm
|
||||
nvarchar
|
||||
nvm
|
||||
OAuth
|
||||
octokit
|
||||
oidc
|
||||
Okta
|
||||
onboarding
|
||||
Onboarding
|
||||
pagerduty
|
||||
pageview
|
||||
parallelization
|
||||
plantuml
|
||||
Platformize
|
||||
Podman
|
||||
postgres
|
||||
postpack
|
||||
pre
|
||||
prebaked
|
||||
preconfigured
|
||||
prepack
|
||||
Preprarer
|
||||
productional
|
||||
Protobuf
|
||||
proxying
|
||||
Proxying
|
||||
pygments
|
||||
pymdownx
|
||||
rankdir
|
||||
readme
|
||||
Readme
|
||||
readonly
|
||||
rebase
|
||||
Recharts
|
||||
Redash
|
||||
replicasets
|
||||
repo
|
||||
Repo
|
||||
repos
|
||||
rerender
|
||||
rollbar
|
||||
Rollbar
|
||||
Rollup
|
||||
routable
|
||||
Routable
|
||||
rst
|
||||
rsync
|
||||
ruleset
|
||||
sam
|
||||
scaffolded
|
||||
scaffolder
|
||||
Scaffolder
|
||||
seb
|
||||
semlas
|
||||
semver
|
||||
Serverless
|
||||
Sinon
|
||||
Snyk
|
||||
sourcemaps
|
||||
sparklines
|
||||
Splunk
|
||||
Spotifiers
|
||||
spotify
|
||||
Spotify
|
||||
sqlite
|
||||
squidfunk
|
||||
src
|
||||
@@ -247,17 +234,25 @@ subkey
|
||||
subroutes
|
||||
subtree
|
||||
superfences
|
||||
Superfences
|
||||
superset
|
||||
supertype
|
||||
talkdesk
|
||||
Talkdesk
|
||||
tasklist
|
||||
techdocs
|
||||
Telenor
|
||||
templated
|
||||
templater
|
||||
Templater
|
||||
templaters
|
||||
Templaters
|
||||
theia
|
||||
thumbsup
|
||||
toc
|
||||
todo
|
||||
tolerations
|
||||
Tolerations
|
||||
toolchain
|
||||
toolsets
|
||||
tooltip
|
||||
@@ -273,11 +268,18 @@ untracked
|
||||
upvote
|
||||
url
|
||||
utils
|
||||
validator
|
||||
validators
|
||||
varchar
|
||||
Weaveworks
|
||||
Webpack
|
||||
winston
|
||||
www
|
||||
WWW
|
||||
XML
|
||||
xyz
|
||||
yaml
|
||||
Zalando
|
||||
Zhou
|
||||
zoomable
|
||||
zsh
|
||||
|
||||
@@ -18,9 +18,9 @@ export const analyzeLocationSchema: yup.ObjectSchema<{
|
||||
// @public (undocumented)
|
||||
interface ApiEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND;
|
||||
kind: 'API';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
type: string;
|
||||
@@ -54,9 +54,9 @@ export function compareEntityToRef(entity: Entity, ref: EntityRef | EntityName,
|
||||
// @public (undocumented)
|
||||
interface ComponentEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_2[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_2;
|
||||
kind: 'Component';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
type: string;
|
||||
@@ -87,9 +87,9 @@ export class DefaultNamespaceEntityPolicy implements EntityPolicy {
|
||||
// @public (undocumented)
|
||||
interface DomainEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_3[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_3;
|
||||
kind: 'Domain';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
owner: string;
|
||||
@@ -107,7 +107,7 @@ export const domainEntityV1alpha1Validator: KindValidator;
|
||||
export const EDIT_URL_ANNOTATION = "backstage.io/edit-url";
|
||||
|
||||
// @public
|
||||
export type Entity = {
|
||||
export type Entity = EntityEnvelope & {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: EntityMeta;
|
||||
@@ -122,9 +122,25 @@ export const ENTITY_DEFAULT_NAMESPACE = "default";
|
||||
// @public
|
||||
export const ENTITY_META_GENERATED_FIELDS: readonly ["uid", "etag", "generation"];
|
||||
|
||||
// @public
|
||||
export type EntityEnvelope = {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export function entityEnvelopeSchemaValidator<T extends EntityEnvelope = EntityEnvelope>(schema?: unknown): (data: unknown) => T;
|
||||
|
||||
// @public
|
||||
export function entityHasChanges(previous: Entity, next: Entity): boolean;
|
||||
|
||||
// @public
|
||||
export function entityKindSchemaValidator<T extends Entity>(schema: unknown): (data: unknown) => T | false;
|
||||
|
||||
// @public
|
||||
export type EntityLink = {
|
||||
url: string;
|
||||
@@ -184,6 +200,9 @@ export type EntityRelationSpec = {
|
||||
target: EntityName;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function entitySchemaValidator<T extends Entity = Entity>(schema?: unknown): (data: unknown) => T;
|
||||
|
||||
// @public
|
||||
export class FieldFormatEntityPolicy implements EntityPolicy {
|
||||
constructor(validators?: Validators);
|
||||
@@ -212,9 +231,9 @@ export function getEntitySourceLocation(entity: Entity): {
|
||||
// @public (undocumented)
|
||||
interface GroupEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_4[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_4;
|
||||
kind: 'Group';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
type: string;
|
||||
@@ -279,9 +298,9 @@ export const LOCATION_ANNOTATION = "backstage.io/managed-by-location";
|
||||
// @public (undocumented)
|
||||
interface LocationEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_5[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_5;
|
||||
kind: 'Location';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
type?: string;
|
||||
@@ -405,9 +424,9 @@ export const RELATION_PROVIDES_API = "providesApi";
|
||||
// @public (undocumented)
|
||||
interface ResourceEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_6[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_6;
|
||||
kind: 'Resource';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
type: string;
|
||||
@@ -424,9 +443,6 @@ export { ResourceEntityV1alpha1 }
|
||||
// @public (undocumented)
|
||||
export const resourceEntityV1alpha1Validator: KindValidator;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function schemaValidator(kind: string, apiVersion: readonly string[], schema: yup.Schema<any>): KindValidator;
|
||||
|
||||
// @public
|
||||
export class SchemaValidEntityPolicy implements EntityPolicy {
|
||||
// (undocumented)
|
||||
@@ -459,9 +475,9 @@ export function stringifyLocationReference(ref: {
|
||||
// @public (undocumented)
|
||||
interface SystemEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_7[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_7;
|
||||
kind: 'System';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
owner: string;
|
||||
@@ -479,9 +495,9 @@ export const systemEntityV1alpha1Validator: KindValidator;
|
||||
// @public (undocumented)
|
||||
interface TemplateEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_8[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_8;
|
||||
kind: 'Template';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
type: string;
|
||||
@@ -502,9 +518,9 @@ export const templateEntityV1alpha1Validator: KindValidator;
|
||||
// @public (undocumented)
|
||||
export interface TemplateEntityV1beta2 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_9[number];
|
||||
apiVersion: 'backstage.io/v1beta2';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_9;
|
||||
kind: 'Template';
|
||||
// (undocumented)
|
||||
metadata: EntityMeta & {
|
||||
title?: string;
|
||||
@@ -548,9 +564,9 @@ export type UNSTABLE_EntityStatusLevel = 'info' | 'warning' | 'error';
|
||||
// @public (undocumented)
|
||||
interface UserEntityV1alpha1 extends Entity {
|
||||
// (undocumented)
|
||||
apiVersion: typeof API_VERSION_10[number];
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
// (undocumented)
|
||||
kind: typeof KIND_10;
|
||||
kind: 'User';
|
||||
// (undocumented)
|
||||
spec: {
|
||||
profile?: {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@types/yup": "^0.29.8",
|
||||
"ajv": "^7.0.3",
|
||||
"json-schema": "^0.3.0",
|
||||
"typescript-json-schema": "^0.49.0",
|
||||
"lodash": "^4.17.15",
|
||||
"uuid": "^8.0.0",
|
||||
"yup": "^0.29.3"
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { EntityName } from '../types';
|
||||
import { EntityEnvelope } from './EntityEnvelope';
|
||||
import { UNSTABLE_EntityStatus } from './EntityStatus';
|
||||
|
||||
/**
|
||||
* The format envelope that's common to all versions/kinds of entity.
|
||||
* The parts of the format that's common to all versions/kinds of entity.
|
||||
*
|
||||
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/
|
||||
*/
|
||||
export type Entity = {
|
||||
export type Entity = EntityEnvelope & {
|
||||
/**
|
||||
* The version of specification format for this particular entity that
|
||||
* this is written against.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The envelope skeleton parts of an entity - whatever is necessary to be able
|
||||
* to give it a ref and pass to further validation / policy checking.
|
||||
*
|
||||
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/
|
||||
*/
|
||||
export type EntityEnvelope = {
|
||||
/**
|
||||
* 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: {
|
||||
/**
|
||||
* The name of the entity.
|
||||
*
|
||||
* Must be unique 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;
|
||||
};
|
||||
};
|
||||
@@ -27,6 +27,7 @@ export type {
|
||||
EntityRelation,
|
||||
EntityRelationSpec,
|
||||
} from './Entity';
|
||||
export type { EntityEnvelope } from './EntityEnvelope';
|
||||
export type {
|
||||
UNSTABLE_EntityStatus,
|
||||
UNSTABLE_EntityStatusItem,
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/API.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'API' as const;
|
||||
|
||||
export interface ApiEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'API';
|
||||
spec: {
|
||||
type: string;
|
||||
lifecycle: string;
|
||||
@@ -37,8 +31,5 @@ export interface ApiEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Component.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Component' as const;
|
||||
|
||||
export interface ComponentEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'Component';
|
||||
spec: {
|
||||
type: string;
|
||||
lifecycle: string;
|
||||
@@ -40,8 +34,5 @@ export interface ComponentEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,25 +16,16 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Domain.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Domain' as const;
|
||||
|
||||
export interface DomainEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'Domain';
|
||||
spec: {
|
||||
owner: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Group.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Group' as const;
|
||||
|
||||
export interface GroupEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'Group';
|
||||
spec: {
|
||||
type: string;
|
||||
profile?: {
|
||||
@@ -41,8 +35,5 @@ export interface GroupEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Location.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Location' as const;
|
||||
|
||||
export interface LocationEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'Location';
|
||||
spec: {
|
||||
type?: string;
|
||||
target?: string;
|
||||
@@ -35,8 +29,5 @@ export interface LocationEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Resource.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Resource' as const;
|
||||
|
||||
export interface ResourceEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'Resource';
|
||||
spec: {
|
||||
type: string;
|
||||
owner: string;
|
||||
@@ -36,8 +30,5 @@ export interface ResourceEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/System.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'System' as const;
|
||||
|
||||
export interface SystemEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'System';
|
||||
spec: {
|
||||
owner: string;
|
||||
domain?: string;
|
||||
@@ -34,8 +28,5 @@ export interface SystemEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,18 +16,12 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Template.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import type { JSONSchema } from '../types';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Template' as const;
|
||||
|
||||
export interface TemplateEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'Template';
|
||||
spec: {
|
||||
type: string;
|
||||
templater: string;
|
||||
@@ -38,8 +32,5 @@ export interface TemplateEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const templateEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,20 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import type { Entity, EntityMeta } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Template.v1beta2.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1beta2'] as const;
|
||||
const KIND = 'Template' as const;
|
||||
|
||||
export interface TemplateEntityV1beta2 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1beta2';
|
||||
kind: 'Template';
|
||||
metadata: EntityMeta & {
|
||||
title?: string;
|
||||
};
|
||||
@@ -46,8 +40,5 @@ export interface TemplateEntityV1beta2 extends Entity {
|
||||
}
|
||||
|
||||
export const templateEntityV1beta2Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -16,17 +16,11 @@
|
||||
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/User.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'User' as const;
|
||||
|
||||
export interface UserEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
|
||||
kind: 'User';
|
||||
spec: {
|
||||
profile?: {
|
||||
displayName?: string;
|
||||
@@ -38,8 +32,5 @@ export interface UserEntityV1alpha1 extends Entity {
|
||||
}
|
||||
|
||||
export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { schemaValidator } from './util';
|
||||
export type { KindValidator } from './types';
|
||||
export { apiEntityV1alpha1Validator } from './ApiEntityV1alpha1';
|
||||
export type {
|
||||
ApiEntityV1alpha1 as ApiEntity,
|
||||
@@ -59,6 +57,7 @@ export type {
|
||||
} from './TemplateEntityV1alpha1';
|
||||
export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2';
|
||||
export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2';
|
||||
export type { KindValidator } from './types';
|
||||
export { userEntityV1alpha1Validator } from './UserEntityV1alpha1';
|
||||
export type {
|
||||
UserEntityV1alpha1 as UserEntity,
|
||||
|
||||
@@ -14,66 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Ajv, { AnySchema } from 'ajv';
|
||||
import * as yup from 'yup';
|
||||
import { entityKindSchemaValidator } from '../validation';
|
||||
import { KindValidator } from './types';
|
||||
|
||||
/**
|
||||
* @deprecated We no longer use yup for the catalog model. This utility method will be removed.
|
||||
*/
|
||||
export function schemaValidator(
|
||||
kind: string,
|
||||
apiVersion: readonly string[],
|
||||
schema: yup.Schema<any>,
|
||||
): KindValidator {
|
||||
// TODO(freben): Left here as a compatibility helper. It would be nicer to
|
||||
// just export the inner validator directly. However, all of the already
|
||||
// exported kind validators have the `KindValidator` signature which is
|
||||
// different. So let's postpone that change until a later time.
|
||||
export function ajvCompiledJsonSchemaValidator(schema: unknown): KindValidator {
|
||||
const validator = entityKindSchemaValidator(schema);
|
||||
return {
|
||||
async check(envelope) {
|
||||
if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) {
|
||||
return false;
|
||||
}
|
||||
await schema.validate(envelope, { strict: true });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ajvCompiledJsonSchemaValidator(
|
||||
kind: string,
|
||||
apiVersion: readonly string[],
|
||||
schema: AnySchema,
|
||||
extraSchemas?: AnySchema[],
|
||||
): KindValidator {
|
||||
const ajv = new Ajv({ allowUnionTypes: true });
|
||||
if (extraSchemas) {
|
||||
ajv.addSchema(extraSchemas, undefined, undefined, true);
|
||||
}
|
||||
const validate = ajv.compile(schema);
|
||||
|
||||
return {
|
||||
async check(envelope) {
|
||||
if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = validate(envelope);
|
||||
if (result === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [error] = validate.errors || [];
|
||||
if (!error) {
|
||||
throw new TypeError(`Malformed ${kind}, Unknown error`);
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`Malformed ${kind}, ${error.dataPath || '<root>'} ${error.message}${
|
||||
error.params
|
||||
? ` - ${Object.entries(error.params)
|
||||
.map(([key, val]) => `${key}: ${val}`)
|
||||
.join(', ')}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
async check(data) {
|
||||
return validator(data) === data;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "Entity",
|
||||
"description": "The format envelope that's common to all versions/kinds of entity.",
|
||||
"description": "The parts of the format that's common to all versions/kinds of entity.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "EntityEnvelope",
|
||||
"description": "The envelope skeleton parts of an entity - whatever is necessary to be able to give it a ref and pass to further validation / policy checking.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Component",
|
||||
"metadata": {
|
||||
"name": "LoremService"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"required": ["apiVersion", "kind", "metadata"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"type": "string",
|
||||
"description": "The version of specification format for this particular entity that this is written against.",
|
||||
"minLength": 1,
|
||||
"examples": ["backstage.io/v1alpha1", "my-company.net/v1", "1.0"]
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "The high level entity type being described.",
|
||||
"minLength": 1,
|
||||
"examples": [
|
||||
"API",
|
||||
"Component",
|
||||
"Domain",
|
||||
"Group",
|
||||
"Location",
|
||||
"Resource",
|
||||
"System",
|
||||
"Template",
|
||||
"User"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair.",
|
||||
"examples": ["metadata-proxy"],
|
||||
"minLength": 1
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "The namespace that the entity belongs to.",
|
||||
"default": "default",
|
||||
"examples": ["default", "admin"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2021 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 Ajv, { Schema, ValidateFunction } from 'ajv';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityEnvelopeSchema from '../schema/EntityEnvelope.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
|
||||
// A local cache of compiled schemas, to avoid duplicate work
|
||||
const compiledSchemaCache = new Map<unknown, ValidateFunction<unknown>>();
|
||||
|
||||
// The core schemas that others can depend on
|
||||
const refDependencyCandidates = [
|
||||
entityEnvelopeSchema,
|
||||
entitySchema,
|
||||
entityMetaSchema,
|
||||
commonSchema,
|
||||
];
|
||||
|
||||
export function throwAjvError(
|
||||
errors: ValidateFunction<unknown>['errors'],
|
||||
): never {
|
||||
if (!errors?.length) {
|
||||
throw new TypeError('Unknown error');
|
||||
}
|
||||
|
||||
const error = errors[0];
|
||||
throw new TypeError(
|
||||
`${error.dataPath || '<root>'} ${error.message}${
|
||||
error.params
|
||||
? ` - ${Object.entries(error.params)
|
||||
.map(([key, val]) => `${key}: ${val}`)
|
||||
.join(', ')}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Compiles the given schema, and makes sure to also grab any core dependencies
|
||||
// that it depends on
|
||||
export function compileAjvSchema(
|
||||
schema: Schema,
|
||||
options: { disableCache?: boolean } = {},
|
||||
): ValidateFunction<unknown> {
|
||||
const disableCache = options?.disableCache ?? false;
|
||||
|
||||
if (!disableCache) {
|
||||
const cached = compiledSchemaCache.get(schema);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
const extraSchemas = getExtraSchemas(schema);
|
||||
const ajv = new Ajv({
|
||||
allowUnionTypes: true,
|
||||
allErrors: true,
|
||||
validateSchema: true,
|
||||
});
|
||||
if (extraSchemas.length) {
|
||||
ajv.addSchema(extraSchemas, undefined, undefined, true);
|
||||
}
|
||||
const compiled = ajv.compile(schema);
|
||||
|
||||
if (!disableCache) {
|
||||
compiledSchemaCache.set(schema, compiled);
|
||||
}
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
// Find refs in the given schema and recursively in all known schemas it
|
||||
// targets, collecting that list of schemas as we go
|
||||
function getExtraSchemas(schema: Schema): Schema[] {
|
||||
if (typeof schema !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
if (schema.$id) {
|
||||
seen.add(schema.$id);
|
||||
}
|
||||
|
||||
const selected = new Array<Schema>();
|
||||
|
||||
const todo: Schema[] = [schema];
|
||||
while (todo.length) {
|
||||
const current = todo.pop()!;
|
||||
|
||||
for (const ref of getAllRefs(current)) {
|
||||
if (!seen.has(ref)) {
|
||||
seen.add(ref);
|
||||
|
||||
const match = refDependencyCandidates.find(c => c.$id === ref);
|
||||
if (match) {
|
||||
selected.push(match);
|
||||
todo.push(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
// Naively step through the entire schema looking for "$ref": "x" pairs. The
|
||||
// resulting iterator may contain duplicates. Ignores fragments, i.e. for a ref
|
||||
// of "a#b", it will just yield "a".
|
||||
function* getAllRefs(schema: Schema): Iterable<string> {
|
||||
const todo: any[] = [schema];
|
||||
while (todo.length) {
|
||||
const current = todo.pop()!;
|
||||
if (typeof current === 'object' && current) {
|
||||
for (const [key, value] of Object.entries(current)) {
|
||||
if (key === '$ref' && typeof value === 'string') {
|
||||
yield value.split('#')[0];
|
||||
} else {
|
||||
todo.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2021 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 { entityEnvelopeSchemaValidator } from './entityEnvelopeSchemaValidator';
|
||||
|
||||
describe('entityEnvelopeSchemaValidator', () => {
|
||||
const validator = entityEnvelopeSchemaValidator();
|
||||
let entity: any;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
namespace: 'ns',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('happy path: accepts valid data', () => {
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
//
|
||||
// apiVersion and kind
|
||||
//
|
||||
|
||||
it('rejects wrong root type', () => {
|
||||
expect(() => validator(7)).toThrow(/object/);
|
||||
});
|
||||
|
||||
it('accepts unknown root fields', () => {
|
||||
entity.blah = 7;
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects missing apiVersion', () => {
|
||||
delete entity.apiVersion;
|
||||
expect(() => validator(entity)).toThrow(/apiVersion/);
|
||||
});
|
||||
|
||||
it('rejects bad apiVersion type', () => {
|
||||
entity.apiVersion = 7;
|
||||
expect(() => validator(entity)).toThrow(/apiVersion/);
|
||||
});
|
||||
|
||||
it('rejects empty apiVersion', () => {
|
||||
entity.apiVersion = '';
|
||||
expect(() => validator(entity)).toThrow(/apiVersion/);
|
||||
});
|
||||
|
||||
it('rejects missing kind', () => {
|
||||
delete entity.kind;
|
||||
expect(() => validator(entity)).toThrow(/kind/);
|
||||
});
|
||||
|
||||
it('rejects bad kind type', () => {
|
||||
entity.kind = 7;
|
||||
expect(() => validator(entity)).toThrow(/kind/);
|
||||
});
|
||||
|
||||
it('rejects empty kind', () => {
|
||||
entity.kind = '';
|
||||
expect(() => validator(entity)).toThrow(/kind/);
|
||||
});
|
||||
|
||||
//
|
||||
// metadata
|
||||
//
|
||||
|
||||
it('rejects missing metadata', () => {
|
||||
delete entity.metadata;
|
||||
expect(() => validator(entity)).toThrow(/metadata/);
|
||||
});
|
||||
|
||||
it('rejects bad metadata type', () => {
|
||||
entity.metadata = 7;
|
||||
expect(() => validator(entity)).toThrow(/metadata/);
|
||||
});
|
||||
|
||||
it('rejects missing name', () => {
|
||||
delete entity.metadata.name;
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('rejects empty name', () => {
|
||||
entity.metadata.name = '';
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('rejects bad name type', () => {
|
||||
entity.metadata.name = 7;
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('accepts missing namespace', () => {
|
||||
delete entity.metadata.namespace;
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects empty namespace', () => {
|
||||
entity.metadata.namespace = '';
|
||||
expect(() => validator(entity)).toThrow(/namespace/);
|
||||
});
|
||||
|
||||
it('rejects bad namespace type', () => {
|
||||
entity.metadata.namespace = 7;
|
||||
expect(() => validator(entity)).toThrow(/namespace/);
|
||||
});
|
||||
|
||||
it('accepts unknown metadata fields', () => {
|
||||
entity.metadata.blah = 7;
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Schema } from 'ajv';
|
||||
import { EntityEnvelope } from '../entity/EntityEnvelope';
|
||||
import entityEnvelopeSchema from '../schema/EntityEnvelope.schema.json';
|
||||
import { compileAjvSchema, throwAjvError } from './ajv';
|
||||
|
||||
/**
|
||||
* Creates a validation function that takes some arbitrary data, and either
|
||||
* returns that data cast to an {@link EntityEnvelope} (or the given subtype)
|
||||
* if it matches that schema, or throws a {@link TypeError} describing the
|
||||
* errors.
|
||||
*
|
||||
* Note that this validator is only meant for applying the base schema checks;
|
||||
* it does not take custom policies or additional processor based validation
|
||||
* into account.
|
||||
*
|
||||
* By default, the plain `EntityEnvelope` schema is used. If you pass in your
|
||||
* own, it may contain `$ref` references to the following, which are resolved
|
||||
* automatically for you:
|
||||
*
|
||||
* - EntityEnvelope
|
||||
* - Entity
|
||||
* - EntityMeta
|
||||
* - common#<id>
|
||||
*
|
||||
* @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema
|
||||
*/
|
||||
export function entityEnvelopeSchemaValidator<
|
||||
T extends EntityEnvelope = EntityEnvelope
|
||||
>(schema?: unknown): (data: unknown) => T {
|
||||
const validate = compileAjvSchema(
|
||||
schema ? (schema as Schema) : entityEnvelopeSchema,
|
||||
);
|
||||
|
||||
return data => {
|
||||
const result = validate(data);
|
||||
if (result === true) {
|
||||
return data as T;
|
||||
}
|
||||
|
||||
throw throwAjvError(validate.errors);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2021 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 { entityKindSchemaValidator } from './entityKindSchemaValidator';
|
||||
import componentSchema from '../schema/kinds/Component.v1alpha1.schema.json';
|
||||
|
||||
describe('entityKindSchemaValidator', () => {
|
||||
const validator = entityKindSchemaValidator(componentSchema);
|
||||
let entity: any;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
uid: 'e01199ab-08cc-44c2-8e19-5c29ded82521',
|
||||
etag: 'lsndfkjsndfkjnsdfkjnsd==',
|
||||
generation: 13,
|
||||
name: 'test',
|
||||
namespace: 'ns',
|
||||
labels: {
|
||||
'backstage.io/custom': 'ValueStuff',
|
||||
},
|
||||
annotations: {
|
||||
'example.com/bindings': 'are-secret',
|
||||
},
|
||||
tags: ['java', 'data'],
|
||||
links: [
|
||||
{
|
||||
url: 'https://example.com',
|
||||
title: 'Website',
|
||||
icon: 'website',
|
||||
},
|
||||
],
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'me',
|
||||
},
|
||||
relations: [
|
||||
{ type: 't', target: { kind: 'k', namespace: 'ns', name: 'n' } },
|
||||
],
|
||||
status: {
|
||||
items: [
|
||||
{
|
||||
type: 't',
|
||||
level: 'error',
|
||||
message: 'm',
|
||||
error: { name: 'n', message: 'm', code: '1', stack: 's' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('works in the happy path', () => {
|
||||
expect(validator(entity)).toBe(entity);
|
||||
});
|
||||
|
||||
it('nicely rejects an unknown kind', () => {
|
||||
entity.kind = 'Unknown';
|
||||
expect(validator(entity)).toBe(false);
|
||||
});
|
||||
|
||||
it('nicely rejects an unknown apiVersion', () => {
|
||||
entity.apiVersion = 'backstage.io/v1alpha7';
|
||||
expect(validator(entity)).toBe(false);
|
||||
});
|
||||
|
||||
it('nicely rejects when both kind and apiVersion mismatch', () => {
|
||||
entity.apiVersion = 'backstage.io/v1alpha7';
|
||||
entity.kind = 'Unknown';
|
||||
expect(validator(entity)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects when the kind is actually breaking other rules than enum', () => {
|
||||
entity.kind = 7;
|
||||
expect(() => validator(entity)).toThrow(/kind/);
|
||||
});
|
||||
|
||||
it('rejects when the apiVersion is actually breaking other rules than enum', () => {
|
||||
entity.apiVersion = 7;
|
||||
expect(() => validator(entity)).toThrow(/apiVersion/);
|
||||
});
|
||||
|
||||
it('rejects nicely when there is both a nice mismatch and a fatal error', () => {
|
||||
entity.kind = 'Unknown';
|
||||
entity.metadata = 7;
|
||||
expect(validator(entity)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects on errors in other parts of the schema', () => {
|
||||
entity.spec = 7;
|
||||
expect(() => validator(entity)).toThrow(/spec/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Schema } from 'ajv';
|
||||
import { Entity } from '../entity';
|
||||
import { compileAjvSchema, throwAjvError } from './ajv';
|
||||
|
||||
/**
|
||||
* Creates a validation function that takes some arbitrary data, and either
|
||||
* returns that data cast to a `T` if it matches that schema, or `false` if the
|
||||
* schema apiVersion/kind didn't apply to that data, or throws a
|
||||
* {@link TypeError} describing actual errors.
|
||||
*
|
||||
* This validator is highly specialized, in that it has special treatment of
|
||||
* the `kind` and `apiVersion` root keys. This only works if your schema has
|
||||
* their rule set to `"enum"`:
|
||||
*
|
||||
* <pre>
|
||||
* "apiVersion": {
|
||||
* "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
* },
|
||||
* "kind": {
|
||||
* "enum": ["Group"]
|
||||
* },
|
||||
* </pre>
|
||||
*
|
||||
* In the above example, the created validator will return `false` if and only
|
||||
* if the kind and/or apiVersion mismatch.
|
||||
*
|
||||
* Note that this validator is only meant for applying the base schema checks;
|
||||
* it does not take custom policies or additional processor based validation
|
||||
* into account.
|
||||
*
|
||||
* The given schema may contain `$ref` references to the following, which are
|
||||
* resolved automatically for you:
|
||||
*
|
||||
* - EntityEnvelope
|
||||
* - Entity
|
||||
* - EntityMeta
|
||||
* - common#<id>
|
||||
*
|
||||
* @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema
|
||||
*/
|
||||
export function entityKindSchemaValidator<T extends Entity>(
|
||||
schema: unknown,
|
||||
): (data: unknown) => T | false {
|
||||
const validate = compileAjvSchema(schema as Schema);
|
||||
|
||||
return data => {
|
||||
const result = validate(data);
|
||||
if (result === true) {
|
||||
return data as T;
|
||||
}
|
||||
|
||||
// Only in the case where kind and/or apiVersion have enum mismatches AND
|
||||
// have NO other errors, we call it a soft error.
|
||||
const softCandidates = validate.errors?.filter(e =>
|
||||
['/kind', '/apiVersion'].includes(e.dataPath),
|
||||
);
|
||||
if (
|
||||
softCandidates?.length &&
|
||||
softCandidates.every(e => e.keyword === 'enum')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw throwAjvError(validate.errors);
|
||||
};
|
||||
}
|
||||
+105
-138
@@ -14,16 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Ajv from 'ajv';
|
||||
import { AnyValidateFunction } from 'ajv/dist/core';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { Entity } from './Entity';
|
||||
import { entitySchemaValidator } from './entitySchemaValidator';
|
||||
|
||||
describe('Entity', () => {
|
||||
describe('entitySchemaValidator', () => {
|
||||
const validator = entitySchemaValidator();
|
||||
let entity: any;
|
||||
let validate: AnyValidateFunction;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
@@ -69,38 +64,10 @@ describe('Entity', () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ allowUnionTypes: true });
|
||||
ajv.addSchema([entityMetaSchema, commonSchema], undefined, undefined, true);
|
||||
validate = ajv.compile(entitySchema);
|
||||
});
|
||||
|
||||
function check(data: unknown): () => true {
|
||||
return () => {
|
||||
const result = validate(data);
|
||||
if (result === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [error] = validate.errors || [];
|
||||
if (!error) {
|
||||
throw new TypeError('Unknown error');
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`${error.dataPath || '<root>'} ${error.message}${
|
||||
error.params
|
||||
? ` - ${Object.entries(error.params)
|
||||
.map(([key, val]) => `${key}: ${val}`)
|
||||
.join(', ')}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
it('happy path: accepts valid data', () => {
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
//
|
||||
@@ -108,37 +75,37 @@ describe('Entity', () => {
|
||||
//
|
||||
|
||||
it('rejects wrong root type', () => {
|
||||
expect(check((7 as unknown) as Entity)).toThrow(/object/);
|
||||
expect(() => validator(7)).toThrow(/object/);
|
||||
});
|
||||
|
||||
it('rejects missing apiVersion', () => {
|
||||
delete entity.apiVersion;
|
||||
expect(check(entity)).toThrow(/apiVersion/);
|
||||
expect(() => validator(entity)).toThrow(/apiVersion/);
|
||||
});
|
||||
|
||||
it('rejects bad apiVersion type', () => {
|
||||
entity.apiVersion = 7;
|
||||
expect(check(entity)).toThrow(/apiVersion/);
|
||||
expect(() => validator(entity)).toThrow(/apiVersion/);
|
||||
});
|
||||
|
||||
it('rejects empty apiVersion', () => {
|
||||
entity.apiVersion = '';
|
||||
expect(check(entity)).toThrow(/apiVersion/);
|
||||
expect(() => validator(entity)).toThrow(/apiVersion/);
|
||||
});
|
||||
|
||||
it('rejects missing kind', () => {
|
||||
delete entity.kind;
|
||||
expect(check(entity)).toThrow(/kind/);
|
||||
expect(() => validator(entity)).toThrow(/kind/);
|
||||
});
|
||||
|
||||
it('rejects bad kind type', () => {
|
||||
entity.kind = 7;
|
||||
expect(check(entity)).toThrow(/kind/);
|
||||
expect(() => validator(entity)).toThrow(/kind/);
|
||||
});
|
||||
|
||||
it('rejects empty kind', () => {
|
||||
entity.kind = '';
|
||||
expect(check(entity)).toThrow(/kind/);
|
||||
expect(() => validator(entity)).toThrow(/kind/);
|
||||
});
|
||||
|
||||
//
|
||||
@@ -147,202 +114,202 @@ describe('Entity', () => {
|
||||
|
||||
it('rejects missing metadata', () => {
|
||||
delete entity.metadata;
|
||||
expect(check(entity)).toThrow(/metadata/);
|
||||
expect(() => validator(entity)).toThrow(/metadata/);
|
||||
});
|
||||
|
||||
it('rejects bad metadata type', () => {
|
||||
entity.metadata = 7;
|
||||
expect(check(entity)).toThrow(/metadata/);
|
||||
expect(() => validator(entity)).toThrow(/metadata/);
|
||||
});
|
||||
|
||||
it('accepts missing uid', () => {
|
||||
delete entity.metadata.uid;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad uid type', () => {
|
||||
entity.metadata.uid = 7;
|
||||
expect(check(entity)).toThrow(/uid/);
|
||||
expect(() => validator(entity)).toThrow(/uid/);
|
||||
});
|
||||
|
||||
it('rejects empty uid', () => {
|
||||
entity.metadata.uid = '';
|
||||
expect(check(entity)).toThrow(/uid/);
|
||||
expect(() => validator(entity)).toThrow(/uid/);
|
||||
});
|
||||
|
||||
it('accepts missing etag', () => {
|
||||
delete entity.metadata.etag;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad etag type', () => {
|
||||
entity.metadata.etag = 7;
|
||||
expect(check(entity)).toThrow(/etag/);
|
||||
expect(() => validator(entity)).toThrow(/etag/);
|
||||
});
|
||||
|
||||
it('rejects empty etag', () => {
|
||||
entity.metadata.etag = '';
|
||||
expect(check(entity)).toThrow(/etag/);
|
||||
expect(() => validator(entity)).toThrow(/etag/);
|
||||
});
|
||||
|
||||
it('accepts missing generation', () => {
|
||||
delete entity.metadata.generation;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad generation type', () => {
|
||||
entity.metadata.generation = 'a';
|
||||
expect(check(entity)).toThrow(/generation/);
|
||||
expect(() => validator(entity)).toThrow(/generation/);
|
||||
});
|
||||
|
||||
it('rejects zero generation', () => {
|
||||
entity.metadata.generation = 0;
|
||||
expect(check(entity)).toThrow(/generation/);
|
||||
expect(() => validator(entity)).toThrow(/generation/);
|
||||
});
|
||||
|
||||
it('rejects non-integer generation', () => {
|
||||
entity.metadata.generation = 1.5;
|
||||
expect(check(entity)).toThrow(/generation/);
|
||||
expect(() => validator(entity)).toThrow(/generation/);
|
||||
});
|
||||
|
||||
it('rejects missing name', () => {
|
||||
delete entity.metadata.name;
|
||||
expect(check(entity)).toThrow(/name/);
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('rejects bad name type', () => {
|
||||
entity.metadata.name = 7;
|
||||
expect(check(entity)).toThrow(/name/);
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('accepts missing namespace', () => {
|
||||
delete entity.metadata.namespace;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad namespace type', () => {
|
||||
entity.metadata.namespace = 7;
|
||||
expect(check(entity)).toThrow(/namespace/);
|
||||
expect(() => validator(entity)).toThrow(/namespace/);
|
||||
});
|
||||
|
||||
it('accepts missing description', () => {
|
||||
delete entity.metadata.description;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad description type', () => {
|
||||
entity.metadata.description = 7;
|
||||
expect(check(entity)).toThrow(/description/);
|
||||
expect(() => validator(entity)).toThrow(/description/);
|
||||
});
|
||||
|
||||
it('accepts missing labels', () => {
|
||||
delete entity.metadata.labels;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty labels', () => {
|
||||
entity.metadata.labels = {};
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad labels type', () => {
|
||||
entity.metadata.labels = 7;
|
||||
expect(check(entity)).toThrow(/labels/);
|
||||
expect(() => validator(entity)).toThrow(/labels/);
|
||||
});
|
||||
|
||||
it('accepts missing annotations', () => {
|
||||
delete entity.metadata.annotations;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty annotations object', () => {
|
||||
entity.metadata.annotations = {};
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad annotations type', () => {
|
||||
entity.metadata.annotations = 7;
|
||||
expect(check(entity)).toThrow(/annotations/);
|
||||
expect(() => validator(entity)).toThrow(/annotations/);
|
||||
});
|
||||
|
||||
it('rejects bad tags type', () => {
|
||||
entity.metadata.tags = 7;
|
||||
expect(check(entity)).toThrow(/tags/);
|
||||
expect(() => validator(entity)).toThrow(/tags/);
|
||||
});
|
||||
|
||||
it('accepts empty tags', () => {
|
||||
entity.metadata.tags = [];
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects empty tag', () => {
|
||||
entity.metadata.tags[0] = '';
|
||||
expect(check(entity)).toThrow(/tags/);
|
||||
expect(() => validator(entity)).toThrow(/tags/);
|
||||
});
|
||||
|
||||
it('rejects bad tag type', () => {
|
||||
entity.metadata.tags[0] = 7;
|
||||
expect(check(entity)).toThrow(/tags/);
|
||||
expect(() => validator(entity)).toThrow(/tags/);
|
||||
});
|
||||
|
||||
it('accepts missing links', () => {
|
||||
delete entity.metadata.links;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty links', () => {
|
||||
entity.metadata.links = [];
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects empty links.url', () => {
|
||||
entity.metadata.links[0].url = '';
|
||||
expect(check(entity)).toThrow(/links/);
|
||||
expect(() => validator(entity)).toThrow(/links/);
|
||||
});
|
||||
|
||||
it('rejects missing links.url', () => {
|
||||
delete entity.metadata.links[0].url;
|
||||
expect(check(entity)).toThrow(/links/);
|
||||
expect(() => validator(entity)).toThrow(/links/);
|
||||
});
|
||||
|
||||
it('rejects bad links.url type', () => {
|
||||
entity.metadata.links[0].url = 7;
|
||||
expect(check(entity)).toThrow(/links/);
|
||||
expect(() => validator(entity)).toThrow(/links/);
|
||||
});
|
||||
|
||||
it('rejects empty links.title', () => {
|
||||
entity.metadata.links[0].title = '';
|
||||
expect(check(entity)).toThrow(/links/);
|
||||
expect(() => validator(entity)).toThrow(/links/);
|
||||
});
|
||||
|
||||
it('accepts missing links.title', () => {
|
||||
delete entity.metadata.links[0].title;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad links.title type', () => {
|
||||
entity.metadata.links[0].title = 7;
|
||||
expect(check(entity)).toThrow(/links/);
|
||||
expect(() => validator(entity)).toThrow(/links/);
|
||||
});
|
||||
|
||||
it('rejects empty links.icon', () => {
|
||||
entity.metadata.links[0].icon = '';
|
||||
expect(check(entity)).toThrow(/links/);
|
||||
expect(() => validator(entity)).toThrow(/links/);
|
||||
});
|
||||
|
||||
it('accepts missing links.icon', () => {
|
||||
delete entity.metadata.links[0].icon;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad links.icon type', () => {
|
||||
entity.metadata.links[0].icon = 7;
|
||||
expect(check(entity)).toThrow(/links/);
|
||||
expect(() => validator(entity)).toThrow(/links/);
|
||||
});
|
||||
|
||||
it('accepts unknown metadata field', () => {
|
||||
entity.metadata.unknown = 7;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
//
|
||||
@@ -351,17 +318,17 @@ describe('Entity', () => {
|
||||
|
||||
it('accepts missing spec', () => {
|
||||
delete entity.spec;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects non-object spec', () => {
|
||||
entity.spec = 7;
|
||||
expect(check(entity)).toThrow(/spec/);
|
||||
expect(() => validator(entity)).toThrow(/spec/);
|
||||
});
|
||||
|
||||
it('accepts unknown spec field', () => {
|
||||
entity.spec.unknown = 7;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
//
|
||||
@@ -370,97 +337,97 @@ describe('Entity', () => {
|
||||
|
||||
it('accepts missing relations', () => {
|
||||
delete entity.relations;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty relations', () => {
|
||||
entity.relations = [];
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad relations type', () => {
|
||||
entity.relations = 7;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects missing relations.type', () => {
|
||||
delete entity.relations[0].type;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects empty relations.type', () => {
|
||||
entity.relations[0].type = '';
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects wrong relations.type type', () => {
|
||||
entity.relations[0].type = 7;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects missing relations.target', () => {
|
||||
delete entity.relations[0].target;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects empty relations.target', () => {
|
||||
entity.relations[0].target = '';
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects wrong relations.target type', () => {
|
||||
entity.relations[0].target = 7;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects missing relations.target.kind', () => {
|
||||
delete entity.relations[0].target.kind;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects empty relations.target.kind', () => {
|
||||
entity.relations[0].target.kind = '';
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects wrong relations.target.kind type', () => {
|
||||
entity.relations[0].target.kind = 7;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects missing relations.target.namespace', () => {
|
||||
delete entity.relations[0].target.namespace;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects empty relations.target.namespace', () => {
|
||||
entity.relations[0].target.namespace = '';
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects wrong relations.target.namespace type', () => {
|
||||
entity.relations[0].target.namespace = 7;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects missing relations.target.name', () => {
|
||||
delete entity.relations[0].target.name;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects empty relations.target.name', () => {
|
||||
entity.relations[0].target.name = '';
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects wrong relations.target.name type', () => {
|
||||
entity.relations[0].target.name = 7;
|
||||
expect(check(entity)).toThrow(/relations/);
|
||||
expect(() => validator(entity)).toThrow(/relations/);
|
||||
});
|
||||
|
||||
it('rejects unknown relation field', () => {
|
||||
entity.relations[0].unknown = 7;
|
||||
expect(check(entity)).toThrow(/unknown/);
|
||||
expect(() => validator(entity)).toThrow(/unknown/);
|
||||
});
|
||||
|
||||
//
|
||||
@@ -469,161 +436,161 @@ describe('Entity', () => {
|
||||
|
||||
it('accepts missing status', () => {
|
||||
delete entity.status;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty status', () => {
|
||||
entity.status = {};
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad status type', () => {
|
||||
entity.status = 7;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('accepts missing status.items', () => {
|
||||
delete entity.status.items;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty status.items', () => {
|
||||
entity.status.items = [];
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad status.items type', () => {
|
||||
entity.status.items = 7;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects bad status.items item type', () => {
|
||||
entity.status.items[0] = 7;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects missing status.items.type', () => {
|
||||
delete entity.status.items[0].type;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects empty status.items.type', () => {
|
||||
entity.status.items[0].type = '';
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects bad status.items.type type', () => {
|
||||
entity.status.items[0].type = 7;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects missing status.items.level', () => {
|
||||
delete entity.status.items[0].level;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects empty status.items.level', () => {
|
||||
entity.status.items[0].level = '';
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects bad status.items.level type', () => {
|
||||
entity.status.items[0].level = 7;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects bad status.items.level enum', () => {
|
||||
entity.status.items[0].level = 'unknown';
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('rejects missing status.items.message', () => {
|
||||
delete entity.status.items[0].message;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('accepts empty status.items.message', () => {
|
||||
entity.status.items[0].message = '';
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad status.items.message type', () => {
|
||||
entity.status.items[0].message = 7;
|
||||
expect(check(entity)).toThrow(/status/);
|
||||
expect(() => validator(entity)).toThrow(/status/);
|
||||
});
|
||||
|
||||
it('accepts missing status.items.error', () => {
|
||||
delete entity.status.items[0].error;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects missing status.items.error.name', () => {
|
||||
delete entity.status.items[0].error.name;
|
||||
expect(check(entity)).toThrow(/name/);
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('rejects empty status.items.error.name', () => {
|
||||
entity.status.items[0].error.name = '';
|
||||
expect(check(entity)).toThrow(/name/);
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('rejects bad status.items.error.name type', () => {
|
||||
entity.status.items[0].error.name = 7;
|
||||
expect(check(entity)).toThrow(/name/);
|
||||
expect(() => validator(entity)).toThrow(/name/);
|
||||
});
|
||||
|
||||
it('rejects missing status.items.error.message', () => {
|
||||
delete entity.status.items[0].error.message;
|
||||
expect(check(entity)).toThrow(/message/);
|
||||
expect(() => validator(entity)).toThrow(/message/);
|
||||
});
|
||||
|
||||
it('accepts empty status.items.error.message', () => {
|
||||
entity.status.items[0].error.message = '';
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad status.items.error.message type', () => {
|
||||
entity.status.items[0].error.message = 7;
|
||||
expect(check(entity)).toThrow(/message/);
|
||||
expect(() => validator(entity)).toThrow(/message/);
|
||||
});
|
||||
|
||||
it('accepts missing status.items.error.code', () => {
|
||||
delete entity.status.items[0].error.code;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty status.items.error.code', () => {
|
||||
entity.status.items[0].error.code = '';
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad status.items.error.code type', () => {
|
||||
entity.status.items[0].error.code = 7;
|
||||
expect(check(entity)).toThrow(/code/);
|
||||
expect(() => validator(entity)).toThrow(/code/);
|
||||
});
|
||||
|
||||
it('accepts missing status.items.error.stack', () => {
|
||||
delete entity.status.items[0].error.stack;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts empty status.items.error.stack', () => {
|
||||
entity.status.items[0].error.stack = '';
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects bad status.items.error.stack type', () => {
|
||||
entity.status.items[0].error.stack = 7;
|
||||
expect(check(entity)).toThrow(/stack/);
|
||||
expect(() => validator(entity)).toThrow(/stack/);
|
||||
});
|
||||
|
||||
it('accepts unknown status.items field', () => {
|
||||
entity.status.items[0].unknown = 7;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts unknown status.items.error field', () => {
|
||||
entity.status.items[0].error.unknown = 7;
|
||||
expect(check(entity)).not.toThrow();
|
||||
expect(() => validator(entity)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Schema } from 'ajv';
|
||||
import { Entity } from '../entity/Entity';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import { compileAjvSchema, throwAjvError } from './ajv';
|
||||
|
||||
/**
|
||||
* Creates a validation function that takes some arbitrary data, and either
|
||||
* returns that data cast to an {@link Entity} (or the given subtype) if it
|
||||
* matches that schema, or throws a {@link TypeError} describing the errors.
|
||||
*
|
||||
* Note that this validator is only meant for applying the base schema checks;
|
||||
* it does not take custom policies or additional processor based validation
|
||||
* into account.
|
||||
*
|
||||
* By default, the plain `Entity` schema is used. If you pass in your own, it
|
||||
* may contain `$ref` references to the following, which are resolved
|
||||
* automatically for you:
|
||||
*
|
||||
* - EntityEnvelope
|
||||
* - Entity
|
||||
* - EntityMeta
|
||||
* - common#<id>
|
||||
*
|
||||
* @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema
|
||||
*/
|
||||
export function entitySchemaValidator<T extends Entity = Entity>(
|
||||
schema?: unknown,
|
||||
): (data: unknown) => T {
|
||||
const validate = compileAjvSchema(schema ? (schema as Schema) : entitySchema);
|
||||
|
||||
return data => {
|
||||
const result = validate(data);
|
||||
if (result === true) {
|
||||
return data as T;
|
||||
}
|
||||
|
||||
throw throwAjvError(validate.errors);
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
|
||||
export { CommonValidatorFunctions } from './CommonValidatorFunctions';
|
||||
export { entityEnvelopeSchemaValidator } from './entityEnvelopeSchemaValidator';
|
||||
export { entityKindSchemaValidator } from './entityKindSchemaValidator';
|
||||
export { entitySchemaValidator } from './entitySchemaValidator';
|
||||
export { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions';
|
||||
export { makeValidator } from './makeValidator';
|
||||
export type { Validators } from './types';
|
||||
|
||||
@@ -277,6 +277,9 @@ function createEmitter(logger: Logger, parentEntity: Entity) {
|
||||
return;
|
||||
}
|
||||
if (i.type === 'entity') {
|
||||
// TODO(freben): Perform the most basic validation here
|
||||
// (apiVersion, kind, metadata, metadata.name, metadata.namespace, spec)
|
||||
|
||||
const originLocation = getEntityOriginLocationRef(parentEntity);
|
||||
|
||||
deferredEntities.push({
|
||||
|
||||
Reference in New Issue
Block a user