Merge pull request #5150 from jmgrimes/dependsOn-dependencyOf-relationships

Implemented dependsOn/dependencyOf support in catalog model.
This commit is contained in:
Fredrik Adelöw
2021-04-12 13:40:00 +02:00
committed by GitHub
11 changed files with 251 additions and 6 deletions
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/catalog-model': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog': patch
---
Implemented missing support for the dependsOn/dependencyOf relationships
between `Component` and `Resource` catalog model objects.
Added support for generating the relevant relationships to the
`BuiltinKindsEntityProcessor`, and added simple support for fetching
relationships between `Components` and `Resources` for rendering in the
system diagram. All catalog-model changes backwards compatible.
@@ -519,6 +519,17 @@ consumed by the component, e.g. `artist-api`. This field is optional.
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| [`API`](#kind-api) (default) | Same as this entity, typically `default` | [`consumesApi`, and reverse `apiConsumedBy`](well-known-relations.md#consumesapi-and-apiconsumedby) |
### `spec.dependsOn` [optional]
An array of [entity references](#string-references) to the components and
resources that the component depends on, e.g. `artists-db`. This field is
optional.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
## Kind: Template
The following describes the following entity kind:
@@ -1014,6 +1025,17 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional.
| --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- |
| [`System`](#kind-system) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) |
### `spec.dependsOn` [optional]
An array of [entity references](#string-references) to the components and
resources that the resource depends on, e.g. `artist-lookup`. This field is
optional.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
## Kind: System
Describes the following entity kind:
@@ -36,6 +36,7 @@ describe('ComponentV1alpha1Validator', () => {
subcomponentOf: 'monolith',
providesApis: ['api-0'],
consumesApis: ['api-0'],
dependsOn: ['resource:resource-0', 'component:component-0'],
system: 'system',
},
};
@@ -160,6 +161,26 @@ describe('ComponentV1alpha1Validator', () => {
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing dependsOn', async () => {
delete (entity as any).spec.dependsOn;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty dependsOn', async () => {
(entity as any).spec.dependsOn = [''];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('rejects undefined dependsOn', async () => {
(entity as any).spec.dependsOn = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('accepts no dependsOn', async () => {
(entity as any).spec.dependsOn = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(validator.check(entity)).resolves.toBe(true);
@@ -34,6 +34,7 @@ export interface ComponentEntityV1alpha1 extends Entity {
subcomponentOf?: string;
providesApis?: string[];
consumesApis?: string[];
dependsOn?: string[];
system?: string;
};
}
@@ -32,6 +32,7 @@ describe('ResourceV1alpha1Validator', () => {
spec: {
type: 'database',
owner: 'me',
dependsOn: ['component:component-0', 'resource:resource-0'],
system: 'system',
},
};
@@ -86,6 +87,26 @@ describe('ResourceV1alpha1Validator', () => {
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('accepts missing dependsOn', async () => {
delete (entity as any).spec.dependsOn;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty dependsOn', async () => {
(entity as any).spec.dependsOn = [''];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('rejects undefined dependsOn', async () => {
(entity as any).spec.dependsOn = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('accepts no dependsOn', async () => {
(entity as any).spec.dependsOn = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(validator.check(entity)).resolves.toBe(true);
@@ -30,6 +30,7 @@ export interface ResourceEntityV1alpha1 extends Entity {
spec: {
type: string;
owner: string;
dependsOn?: string[];
system?: string;
};
}
@@ -84,6 +84,14 @@
"type": "string",
"minLength": 1
}
},
"dependsOn": {
"type": "array",
"description": "An array of references to other entities that the component depends on to function.",
"items": {
"type": "string",
"minLength": 1
}
}
}
}
@@ -47,6 +47,14 @@
"examples": ["artist-relations-team", "user:john.johnson"],
"minLength": 1
},
"dependsOn": {
"type": "array",
"description": "An array of references to other entities that the resource depends on to function.",
"items": {
"type": "string",
"minLength": 1
}
},
"system": {
"type": "string",
"description": "An entity reference to the system that the resource belongs to.",
@@ -45,13 +45,14 @@ describe('BuiltinKindsEntityProcessor', () => {
lifecycle: 'l',
providesApis: ['b'],
consumesApis: ['c'],
dependsOn: ['Resource:r', 'Component:d'],
system: 's',
},
};
await processor.postProcessEntity(entity, location, emit);
expect(emit).toBeCalledTimes(10);
expect(emit).toBeCalledTimes(14);
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
@@ -100,6 +101,38 @@ describe('BuiltinKindsEntityProcessor', () => {
target: { kind: 'API', namespace: 'default', name: 'c' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'n' },
type: 'dependsOn',
target: { kind: 'Resource', namespace: 'default', name: 'r' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Resource', namespace: 'default', name: 'r' },
type: 'dependencyOf',
target: { kind: 'Component', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'n' },
type: 'dependsOn',
target: { kind: 'Component', namespace: 'default', name: 'd' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'd' },
type: 'dependencyOf',
target: { kind: 'Component', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
@@ -134,6 +167,29 @@ describe('BuiltinKindsEntityProcessor', () => {
});
});
it('generates an error for component entities with unspecified dependsOn entity reference kinds', async () => {
const entity: ComponentEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'n' },
spec: {
type: 'service',
owner: 'o',
subcomponentOf: 's',
lifecycle: 'l',
providesApis: ['b'],
consumesApis: ['c'],
dependsOn: ['r'],
system: 's',
},
};
await expect(
processor.postProcessEntity(entity, location, emit),
).rejects.toThrowError(
'Entity reference "r" did not specify a kind (e.g. starting with "Component:"), and has no default',
);
});
it('generates relations for api entities', async () => {
const entity: ApiEntity = {
apiVersion: 'backstage.io/v1alpha1',
@@ -193,13 +249,14 @@ describe('BuiltinKindsEntityProcessor', () => {
spec: {
type: 'database',
owner: 'o',
dependsOn: ['Component:c', 'Resource:r'],
system: 's',
},
};
await processor.postProcessEntity(entity, location, emit);
expect(emit).toBeCalledTimes(4);
expect(emit).toBeCalledTimes(8);
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
@@ -216,6 +273,41 @@ describe('BuiltinKindsEntityProcessor', () => {
target: { kind: 'Group', namespace: 'default', name: 'o' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Resource', namespace: 'default', name: 'n' },
type: 'dependsOn',
target: { kind: 'Component', namespace: 'default', name: 'c' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'c' },
type: 'dependencyOf',
target: { kind: 'Resource', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Resource', namespace: 'default', name: 'n' },
type: 'dependsOn',
target: { kind: 'Resource', namespace: 'default', name: 'r' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Resource', namespace: 'default', name: 'r' },
type: 'dependencyOf',
target: { kind: 'Resource', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
@@ -234,6 +326,25 @@ describe('BuiltinKindsEntityProcessor', () => {
});
});
it('generates an error for resource entities with unspecified dependsOn entity reference kinds', async () => {
const entity: ResourceEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Resource',
metadata: { name: 'n' },
spec: {
type: 'database',
owner: 'o',
dependsOn: ['c'],
system: 's',
},
};
await expect(
processor.postProcessEntity(entity, location, emit),
).rejects.toThrowError(
'Entity reference "c" did not specify a kind (e.g. starting with "Component:"), and has no default',
);
});
it('generates relations for system entities', async () => {
const entity: SystemEntity = {
apiVersion: 'backstage.io/v1alpha1',
@@ -32,6 +32,8 @@ import {
RELATION_API_PROVIDED_BY,
RELATION_CHILD_OF,
RELATION_CONSUMES_API,
RELATION_DEPENDENCY_OF,
RELATION_DEPENDS_ON,
RELATION_HAS_MEMBER,
RELATION_HAS_PART,
RELATION_MEMBER_OF,
@@ -90,7 +92,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
function doEmit(
targets: string | string[] | undefined,
context: { defaultKind: string; defaultNamespace: string },
context: { defaultKind?: string; defaultNamespace: string },
outgoingRelation: string,
incomingRelation: string,
): void {
@@ -99,16 +101,29 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
}
for (const target of [targets].flat()) {
const targetRef = parseEntityRef(target, context);
if (targetRef.kind === undefined) {
throw new Error(
`Entity reference "${target}" did not specify a kind (e.g. starting with "Component:"), and has no default`,
);
}
emit(
result.relation({
source: selfRef,
type: outgoingRelation,
target: targetRef,
target: {
kind: targetRef.kind,
namespace: targetRef.namespace,
name: targetRef.name,
},
}),
);
emit(
result.relation({
source: targetRef,
source: {
kind: targetRef.kind,
namespace: targetRef.namespace,
name: targetRef.name,
},
type: incomingRelation,
target: selfRef,
}),
@@ -146,6 +161,12 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
RELATION_CONSUMES_API,
RELATION_API_CONSUMED_BY,
);
doEmit(
component.spec.dependsOn,
{ defaultNamespace: selfRef.namespace },
RELATION_DEPENDS_ON,
RELATION_DEPENDENCY_OF,
);
doEmit(
component.spec.system,
{ defaultKind: 'System', defaultNamespace: selfRef.namespace },
@@ -186,6 +207,12 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
RELATION_OWNED_BY,
RELATION_OWNER_OF,
);
doEmit(
resource.spec.dependsOn,
{ defaultNamespace: selfRef.namespace },
RELATION_DEPENDS_ON,
RELATION_DEPENDENCY_OF,
);
doEmit(
resource.spec.system,
{ defaultKind: 'System', defaultNamespace: selfRef.namespace },
@@ -16,6 +16,7 @@
import {
Entity,
RELATION_DEPENDS_ON,
RELATION_PROVIDES_API,
RELATION_PART_OF,
serializeEntityRef,
@@ -124,7 +125,6 @@ export function SystemDiagramCard() {
catalogItem,
RELATION_PROVIDES_API,
);
catalogItemRelations_providesApi.forEach(foundRelation =>
systemEdges.push({
from: simplifiedEntityName(catalogItem),
@@ -132,6 +132,18 @@ export function SystemDiagramCard() {
label: 'provides API',
}),
);
const catalogItemRelations_dependsOn = getEntityRelations(
catalogItem,
RELATION_DEPENDS_ON,
);
catalogItemRelations_dependsOn.forEach(foundRelation =>
systemEdges.push({
from: simplifiedEntityName(catalogItem),
to: simplifiedEntityName(foundRelation),
label: 'depends on',
}),
);
}
}