Merge pull request #4031 from tragiclifestories/component-partof-2
Add subcomponent relations to catalog backend
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Add subcomponentOf to Component kind to represent subsystems of larger components.
|
||||
@@ -454,6 +454,15 @@ 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.subcomponentOf` [optional]
|
||||
|
||||
An [entity reference](#string-references) to another component of which the
|
||||
component is a part, e.g. `spotify-ios-app`. This field is optional.
|
||||
|
||||
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
|
||||
| ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- |
|
||||
| [`Component`](#kind-component) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) |
|
||||
|
||||
### `spec.providesApis` [optional]
|
||||
|
||||
An array of [entity references](#string-references) to the APIs that are
|
||||
|
||||
@@ -92,13 +92,15 @@ This relation is commonly based on `spec.memberOf`.
|
||||
|
||||
### `partOf` and `hasPart`
|
||||
|
||||
A relation with a [Domain](descriptor-format.md#kind-domain) or
|
||||
[System](descriptor-format.md#kind-system) entity, typically from a
|
||||
A relation with a [Domain](descriptor-format.md#kind-domain),
|
||||
[System](descriptor-format.md#kind-system) or
|
||||
[Component](descriptor-format.md#kind-component) entity, typically from a
|
||||
[Component](descriptor-format.md#kind-component),
|
||||
[API](descriptor-format.md#kind-api), or
|
||||
[System](descriptor-format.md#kind-system).
|
||||
|
||||
These relations express that a component, API or resource belongs to a system,
|
||||
or that a system is grouped under a domain.
|
||||
These relations express that a component belongs to a larger component; a
|
||||
component, API or resource belongs to a system; or that a system is grouped
|
||||
under a domain.
|
||||
|
||||
This relation is commonly based on `spec.system` or `spec.domain`.
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('ComponentV1alpha1Validator', () => {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'me',
|
||||
subcomponentOf: 'monolith',
|
||||
providesApis: ['api-0'],
|
||||
consumesApis: ['api-0'],
|
||||
},
|
||||
@@ -103,6 +104,21 @@ describe('ComponentV1alpha1Validator', () => {
|
||||
await expect(validator.check(entity)).rejects.toThrow(/owner/);
|
||||
});
|
||||
|
||||
it('accepts missing subcomponentOf', async () => {
|
||||
delete (entity as any).spec.subcomponentOf;
|
||||
await expect(validator.check(entity)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('rejects wrong subcomponentOf', async () => {
|
||||
(entity as any).spec.subcomponentOf = 7;
|
||||
await expect(validator.check(entity)).rejects.toThrow(/subcomponentOf/);
|
||||
});
|
||||
|
||||
it('rejects empty subcomponentOf', async () => {
|
||||
(entity as any).spec.subcomponentOf = '';
|
||||
await expect(validator.check(entity)).rejects.toThrow(/subcomponentOf/);
|
||||
});
|
||||
|
||||
it('accepts missing providesApis', async () => {
|
||||
delete (entity as any).spec.providesApis;
|
||||
await expect(validator.check(entity)).resolves.toBe(true);
|
||||
|
||||
@@ -29,6 +29,7 @@ const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
|
||||
type: yup.string().required().min(1),
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
subcomponentOf: yup.string().notRequired().min(1),
|
||||
providesApis: yup.array(yup.string().required()).notRequired(),
|
||||
consumesApis: yup.array(yup.string().required()).notRequired(),
|
||||
})
|
||||
@@ -42,6 +43,7 @@ export interface ComponentEntityV1alpha1 extends Entity {
|
||||
type: string;
|
||||
lifecycle: string;
|
||||
owner: string;
|
||||
subcomponentOf?: string;
|
||||
providesApis?: string[];
|
||||
consumesApis?: string[];
|
||||
};
|
||||
|
||||
@@ -55,3 +55,10 @@ export const RELATION_CHILD_OF = 'childOf';
|
||||
*/
|
||||
export const RELATION_MEMBER_OF = 'memberOf';
|
||||
export const RELATION_HAS_MEMBER = 'hasMember';
|
||||
|
||||
/**
|
||||
* A part/whole relation, typically for components in a system and systems
|
||||
* in a domain.
|
||||
*/
|
||||
export const RELATION_PART_OF = 'partOf';
|
||||
export const RELATION_HAS_PART = 'hasPart';
|
||||
|
||||
+18
-1
@@ -38,6 +38,7 @@ describe('BuiltinKindsEntityProcessor', () => {
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'o',
|
||||
subcomponentOf: 's',
|
||||
lifecycle: 'l',
|
||||
providesApis: ['b'],
|
||||
consumesApis: ['c'],
|
||||
@@ -46,7 +47,7 @@ describe('BuiltinKindsEntityProcessor', () => {
|
||||
|
||||
await processor.postProcessEntity(entity, location, emit);
|
||||
|
||||
expect(emit).toBeCalledTimes(6);
|
||||
expect(emit).toBeCalledTimes(8);
|
||||
expect(emit).toBeCalledWith({
|
||||
type: 'relation',
|
||||
relation: {
|
||||
@@ -95,6 +96,22 @@ describe('BuiltinKindsEntityProcessor', () => {
|
||||
target: { kind: 'API', namespace: 'default', name: 'c' },
|
||||
},
|
||||
});
|
||||
expect(emit).toBeCalledWith({
|
||||
type: 'relation',
|
||||
relation: {
|
||||
source: { kind: 'Component', namespace: 'default', name: 's' },
|
||||
type: 'hasPart',
|
||||
target: { kind: 'Component', namespace: 'default', name: 'n' },
|
||||
},
|
||||
});
|
||||
expect(emit).toBeCalledWith({
|
||||
type: 'relation',
|
||||
relation: {
|
||||
source: { kind: 'Component', namespace: 'default', name: 'n' },
|
||||
type: 'partOf',
|
||||
target: { kind: 'Component', namespace: 'default', name: 's' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates relations for api entities', async () => {
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
RELATION_CONSUMES_API,
|
||||
RELATION_HAS_MEMBER,
|
||||
RELATION_MEMBER_OF,
|
||||
RELATION_HAS_PART,
|
||||
RELATION_PART_OF,
|
||||
RELATION_OWNED_BY,
|
||||
RELATION_OWNER_OF,
|
||||
RELATION_PARENT_OF,
|
||||
@@ -115,6 +117,12 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
|
||||
RELATION_OWNED_BY,
|
||||
RELATION_OWNER_OF,
|
||||
);
|
||||
doEmit(
|
||||
component.spec.subcomponentOf,
|
||||
{ defaultKind: 'Component', defaultNamespace: selfRef.namespace },
|
||||
RELATION_PART_OF,
|
||||
RELATION_HAS_PART,
|
||||
);
|
||||
doEmit(
|
||||
component.spec.providesApis,
|
||||
{ defaultKind: 'API', defaultNamespace: selfRef.namespace },
|
||||
|
||||
Reference in New Issue
Block a user