Merge pull request #3449 from SDA-SE/feat/provides-consumse-api

Replace implementsApis with providesApis and add consumesApis
This commit is contained in:
Oliver Sand
2020-11-26 17:27:30 +01:00
committed by GitHub
14 changed files with 175 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-api-docs': patch
'@backstage/plugin-catalog': patch
---
Replace usage of implementsApis with relations
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/catalog-model': patch
'@backstage/plugin-catalog-backend': patch
---
Add `providesApis` and `consumesApis` to the component entity spec.
+15
View File
@@ -0,0 +1,15 @@
---
'@backstage/catalog-model': patch
---
Marked the field `spec.implementsApis` on `Component` entities for deprecation on Dec 14th, 2020.
Code that consumes these fields should remove those usages as soon as possible and migrate to using
relations instead. Producers should fill the field `spec.providesApis` instead, which has the same
semantic.
After Dec 14th, the fields will be removed from types and classes of the Backstage repository. At
the first release after that, they will not be present in released packages either.
If your catalog-info.yaml files still contain this field after the deletion, they will still be
valid and your ingestion will not break, but they won't be visible in the types for consuming code, and the expected relations will not be generated based on them either.```
@@ -381,7 +381,7 @@ spec:
type: website
lifecycle: production
owner: artist-relations@example.com
implementsApis:
providesApis:
- artist-api
```
@@ -445,12 +445,35 @@ group of people in an organizational structure.
### `spec.implementsApis` [optional]
**NOTE**: This field was marked for deprecation on Nov 25nd, 2020. It will be
removed entirely from the model on Dec 14th, 2020 in the repository and will not
be present in released packages following the next release after that. Please
update your code to not consume this field before the removal date.
Links APIs that are implemented by the component, e.g. `artist-api`. This field
is optional.
The software catalog expects a list of one or more strings that references the
names of other entities of the `kind` `API`.
This field has the same behavior as `spec.providesApis`.
### `spec.providesApis` [optional]
Links APIs that are provided by the component, e.g. `artist-api`. This field is
optional.
The software catalog expects a list of one or more strings that references the
names of other entities of the `kind` `API`.
### `spec.consumesApis` [optional]
Links APIs that are consumed by the component, e.g. `artist-api`. This field is
optional.
The software catalog expects a list of one or more strings that references the
names of other entities of the `kind` `API`.
## Kind: Template
Describes the following entity kind:
+2 -2
View File
@@ -51,7 +51,7 @@ spec:
type: service
lifecycle: experimental
owner: group:pet-managers
implementsApis:
providesApis:
- petstore
- internal/streetlights
- hello-world
@@ -66,7 +66,7 @@ catalog that is of kind `Group`, namespace `default` (which, actually, also can
be left out in its own yaml file because that's the default value there too),
and name `pet-managers`.
The entries in `implementsApis` are also references. In this case, none of them
The entries in `providesApis` are also references. In this case, none of them
needs to specify a kind since we know from the context that that's the only kind
that's supported here. The second entry specifies a namespace but the other ones
don't, and in this context, the default is to refer to the same namespace as the
@@ -34,6 +34,8 @@ describe('ComponentV1alpha1Validator', () => {
lifecycle: 'production',
owner: 'me',
implementsApis: ['api-0'],
providesApis: ['api-0'],
consumesApis: ['api-0'],
},
};
});
@@ -121,4 +123,44 @@ describe('ComponentV1alpha1Validator', () => {
(entity as any).spec.implementsApis = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing providesApis', async () => {
delete (entity as any).spec.providesApis;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty providesApis', async () => {
(entity as any).spec.providesApis = [''];
await expect(validator.check(entity)).rejects.toThrow(/providesApis/);
});
it('rejects undefined providesApis', async () => {
(entity as any).spec.providesApis = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/providesApis/);
});
it('accepts no providesApis', async () => {
(entity as any).spec.providesApis = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing consumesApis', async () => {
delete (entity as any).spec.consumesApis;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty consumesApis', async () => {
(entity as any).spec.consumesApis = [''];
await expect(validator.check(entity)).rejects.toThrow(/consumesApis/);
});
it('rejects undefined consumesApis', async () => {
(entity as any).spec.consumesApis = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/consumesApis/);
});
it('accepts no consumesApis', async () => {
(entity as any).spec.consumesApis = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
});
@@ -30,6 +30,8 @@ const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
implementsApis: yup.array(yup.string().required()).notRequired(),
providesApis: yup.array(yup.string().required()).notRequired(),
consumesApis: yup.array(yup.string().required()).notRequired(),
kubernetes: yup
.object<any>({
selector: yup
@@ -50,7 +52,14 @@ export interface ComponentEntityV1alpha1 extends Entity {
type: string;
lifecycle: string;
owner: string;
/**
* @deprecated This field will disappear on Dec 14th, 2020. Please remove
* any consuming code. The new field providesApis provides the
* same functionality like before.
*/
implementsApis?: string[];
providesApis?: string[];
consumesApis?: string[];
kubernetes?: {
selector: {
matchLabels: {
+1 -1
View File
@@ -21,7 +21,7 @@ Right now, the following API formats are supported:
Other formats are displayed as plain text, but this can easily be extended.
To fill the catalog with APIs, [provide entities of kind API](https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api).
To link that an component implements an API, see [`implementsApis` property on components](https://backstage.io/docs/features/software-catalog/descriptor-format#specimplementsapis-optional).
To link that a component provides or consumes an API, see the [`providesApis`](https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional) and [`consumesApis`](https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional) properties on the Component kind.
## Links
@@ -28,7 +28,7 @@ spec:
type: service
lifecycle: production
owner: guest
implementsApis:
providesApis:
- example-api
`;
@@ -49,8 +49,7 @@ export const MissingImplementsApisEmptyState = () => {
description={
<Typography>
Components can implement APIs that are displayed on this page. You
need to fill the <code>implementsApis</code> field to enable this
tool.
need to fill the <code>providesApis</code> field to enable this tool.
</Typography>
}
action={
@@ -71,7 +70,7 @@ export const MissingImplementsApisEmptyState = () => {
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/descriptor-format#specimplementsapis-optional"
href="https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional"
>
Read more
</Button>
+3 -2
View File
@@ -15,14 +15,15 @@
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model';
import { Route, Routes } from 'react-router';
import { catalogRoute } from '../routes';
import { EntityPageApi } from './EntityPageApi';
import { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState';
const isPluginApplicableToEntity = (entity: Entity) => {
return ((entity.spec?.implementsApis as string[]) || []).length > 0;
// TODO: Also support RELATION_CONSUMES_API
return entity.relations?.some(r => r.type === RELATION_PROVIDES_API);
};
export const Router = ({ entity }: { entity: Entity }) =>
@@ -14,8 +14,16 @@
* limitations under the License.
*/
import { ComponentEntity } from '@backstage/catalog-model';
import {
ComponentEntity,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
export const useComponentApiNames = (entity: ComponentEntity) => {
return (entity.spec?.implementsApis as string[]) || [];
// TODO: This code doesn't handle namespaces and kinds correctly, but will be removed soon
return (
entity.relations
?.filter(r => r.type === RELATION_PROVIDES_API)
?.map(r => r.target.name) || []
);
};
@@ -68,12 +68,14 @@ describe('BuiltinKindsEntityProcessor', () => {
owner: 'o',
lifecycle: 'l',
implementsApis: ['a'],
providesApis: ['b'],
consumesApis: ['c'],
},
};
await processor.postProcessEntity(entity, location, emit);
expect(emit).toBeCalledTimes(4);
expect(emit).toBeCalledTimes(8);
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
@@ -106,6 +108,38 @@ describe('BuiltinKindsEntityProcessor', () => {
target: { kind: 'API', namespace: 'default', name: 'a' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'API', namespace: 'default', name: 'b' },
type: 'apiProvidedBy',
target: { kind: 'Component', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'n' },
type: 'providesApi',
target: { kind: 'API', namespace: 'default', name: 'b' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'API', namespace: 'default', name: 'c' },
type: 'apiConsumedBy',
target: { kind: 'Component', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'n' },
type: 'consumesApi',
target: { kind: 'API', namespace: 'default', name: 'c' },
},
});
});
it('generates relations for api entities', async () => {
@@ -26,8 +26,10 @@ import {
locationEntityV1alpha1Validator,
LocationSpec,
parseEntityRef,
RELATION_API_CONSUMED_BY,
RELATION_API_PROVIDED_BY,
RELATION_CHILD_OF,
RELATION_CONSUMES_API,
RELATION_HAS_MEMBER,
RELATION_MEMBER_OF,
RELATION_OWNED_BY,
@@ -138,6 +140,18 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
RELATION_PROVIDES_API,
RELATION_API_PROVIDED_BY,
);
doEmit(
component.spec.providesApis,
{ defaultKind: 'API', defaultNamespace: selfRef.namespace },
RELATION_PROVIDES_API,
RELATION_API_PROVIDED_BY,
);
doEmit(
component.spec.consumesApis,
{ defaultKind: 'API', defaultNamespace: selfRef.namespace },
RELATION_CONSUMES_API,
RELATION_API_CONSUMED_BY,
);
}
/*
@@ -18,6 +18,7 @@ import {
Entity,
ENTITY_DEFAULT_NAMESPACE,
RELATION_OWNED_BY,
RELATION_PROVIDES_API,
serializeEntityRef,
} from '@backstage/catalog-model';
import {
@@ -111,6 +112,8 @@ type AboutCardProps = {
export function AboutCard({ entity, variant }: AboutCardProps) {
const classes = useStyles();
const codeLink = getCodeLinkInfo(entity);
// TODO: Also support RELATION_CONSUMES_API here
const hasApis = entity.relations?.some(r => r.type === RELATION_PROVIDES_API);
return (
<Card className={variant === 'gridItem' ? classes.gridItemCard : ''}>
@@ -146,9 +149,9 @@ export function AboutCard({ entity, variant }: AboutCardProps) {
}/${entity.kind}/${entity.metadata.name}`}
/>
<IconLinkVertical
disabled={!entity.spec?.implementsApis}
disabled={!hasApis}
label="View API"
title={!entity.spec?.implementsApis ? 'No APIs available' : ''}
title={hasApis ? '' : 'No APIs available'}
icon={<ExtensionIcon />}
href="api"
/>