Merge pull request #5578 from GunnerStraiker1/template-card-owner

Add Owner Field into TemplateCard and ownedBy & ownerOf relations for TemplateEntity
This commit is contained in:
Fredrik Adelöw
2021-05-20 11:46:44 +02:00
committed by GitHub
14 changed files with 176 additions and 7 deletions
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/catalog-model': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-scaffolder-backend': patch
---
Add Owner field in template card and new data distribution
Add spec.owner as optional field into TemplateV1Alpha and TemplateV1Beta Schema
Add relations ownedBy and ownerOf into Template entity
Template documentation updated
@@ -706,6 +706,25 @@ You can find out more about the `parameters` key
You can find out more about the `steps` key
[here](../software-templates/writing-templates.md)
### `spec.owner` [optional]
An [entity reference](#string-references) to the owner of the component, e.g.
`artist-relations-team`. This field is required.
In Backstage, the owner of a Template is the singular entity (commonly a team)
that bears ultimate responsibility for the Template, and has the authority and
capability to develop and maintain it. They will be the point of contact if
something goes wrong, or if features are to be requested. The main purpose of
this field is for display purposes in Backstage, so that people looking at
catalog items can get an understanding of to whom this Template belongs. It is
not to be used by automated processes to for example assign authorization in
runtime systems. There may be others that also develop or otherwise touch the
Template, but there will always be one ultimate owner.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- |
| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) |
## Kind: API
Describes the following entity kind:
+2
View File
@@ -487,6 +487,7 @@ interface TemplateEntityV1alpha1 extends Entity {
templater: string;
path?: string;
schema: JSONSchema;
owner?: string;
};
}
@@ -520,6 +521,7 @@ export interface TemplateEntityV1beta2 extends Entity {
output?: {
[name: string]: string;
};
owner?: string;
};
}
@@ -48,6 +48,7 @@ describe('templateEntityV1alpha1Validator', () => {
},
},
},
owner: 'team-a@example.com',
},
};
});
@@ -90,4 +91,16 @@ describe('templateEntityV1alpha1Validator', () => {
(entity as any).spec.templater = '';
await expect(validator.check(entity)).rejects.toThrow(/templater/);
});
it('accepts missing owner', async () => {
delete (entity as any).spec.owner;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty owner', async () => {
(entity as any).spec.owner = '';
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects wrong type owner', async () => {
(entity as any).spec.owner = 5;
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
});
@@ -33,6 +33,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
templater: string;
path?: string;
schema: JSONSchema;
owner?: string;
};
}
@@ -59,6 +59,7 @@ describe('templateEntityV1beta2Validator', () => {
output: {
fetchUrl: '{{ steps.fetch.output.targetUrl }}',
},
owner: 'team-b@example.com',
},
};
});
@@ -121,4 +122,16 @@ describe('templateEntityV1beta2Validator', () => {
delete (entity as any).spec.steps[0].action;
await expect(validator.check(entity)).rejects.toThrow(/action/);
});
it('accepts missing owner', async () => {
delete (entity as any).spec.owner;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty owner', async () => {
(entity as any).spec.owner = '';
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects wrong type owner', async () => {
(entity as any).spec.owner = 5;
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
});
@@ -41,6 +41,7 @@ export interface TemplateEntityV1beta2 extends Entity {
input?: JsonObject;
}>;
output?: { [name: string]: string };
owner?: string;
};
}
@@ -85,6 +85,11 @@
"schema": {
"type": "object",
"description": "The JSONSchema describing the inputs for the template."
},
"owner": {
"type": "string",
"description": "The user (or group) owner of the template",
"minLength": 1
}
}
}
@@ -168,6 +168,11 @@
"additionalProperties": {
"type": "string"
}
},
"owner": {
"type": "string",
"description": "The user (or group) owner of the template",
"minLength": 1
}
}
}
@@ -21,6 +21,7 @@ import {
GroupEntity,
ResourceEntity,
SystemEntity,
TemplateEntity,
UserEntity,
} from '@backstage/catalog-model';
import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor';
@@ -520,5 +521,47 @@ describe('BuiltinKindsEntityProcessor', () => {
},
});
});
it('generates relations for template entities', async () => {
const entity: TemplateEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: { name: 'n' },
spec: {
schema: {
properties: {
description: {
title: 'd',
type: 'string',
description: 'des',
},
},
},
templater: 'cookiecutter',
path: '.',
type: 'service',
owner: 'o',
},
};
await processor.postProcessEntity(entity, location, emit);
expect(emit).toBeCalledTimes(2);
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Group', namespace: 'default', name: 'o' },
type: 'ownerOf',
target: { kind: 'Template', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Template', namespace: 'default', name: 'n' },
type: 'ownedBy',
target: { kind: 'Group', namespace: 'default', name: 'o' },
},
});
});
});
});
@@ -46,6 +46,7 @@ import {
resourceEntityV1alpha1Validator,
SystemEntity,
systemEntityV1alpha1Validator,
TemplateEntity,
templateEntityV1alpha1Validator,
templateEntityV1beta2Validator,
UserEntity,
@@ -131,6 +132,19 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
}
}
/*
* Emit relations for the Template kind
*/
if (entity.kind === 'Template') {
const template = entity as TemplateEntity;
doEmit(
template.spec.owner,
{ defaultKind: 'Group', defaultNamespace: selfRef.namespace },
RELATION_OWNED_BY,
RELATION_OWNER_OF,
);
}
/*
* Emit relations for the Component kind
*/
@@ -60,6 +60,7 @@ describe('JobProcessor', () => {
},
},
},
owner: 'example@email.com',
},
};
@@ -55,6 +55,7 @@ describe('Helpers', () => {
},
},
},
owner: 'team-d@example.com',
},
};
@@ -103,6 +104,7 @@ describe('Helpers', () => {
},
},
},
owner: 'team-b@example.com',
},
};
@@ -151,6 +153,7 @@ describe('Helpers', () => {
},
},
},
owner: 'team-a@example.com',
},
};
@@ -198,6 +201,7 @@ describe('Helpers', () => {
},
},
},
owner: 'team-b@example.com',
},
};
@@ -243,6 +247,7 @@ describe('Helpers', () => {
},
},
},
owner: 'team-c@example.com',
},
};
@@ -23,29 +23,48 @@ import {
CardMedia,
Chip,
makeStyles,
Typography,
useTheme,
} from '@material-ui/core';
import React from 'react';
import { generatePath } from 'react-router';
import { rootRouteRef } from '../../routes';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import {
TemplateEntityV1alpha1,
Entity,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import { FavouriteTemplate } from '../FavouriteTemplate/FavouriteTemplate';
import {
getEntityRelations,
EntityRefLinks,
} from '@backstage/plugin-catalog-react';
const useStyles = makeStyles({
const useStyles = makeStyles(theme => ({
cardHeader: {
position: 'relative',
},
title: {
backgroundImage: ({ backgroundImage }: any) => backgroundImage,
},
description: {
box: {
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
'-webkit-line-clamp': 10,
'-webkit-box-orient': 'vertical',
paddingBottom: '0.8em',
},
});
label: {
color: theme.palette.text.secondary,
textTransform: 'uppercase',
fontSize: '0.65rem',
fontWeight: 'bold',
letterSpacing: 0.5,
lineHeight: 1,
paddingBottom: '0.2rem',
},
}));
export type TemplateCardProps = {
template: TemplateEntityV1alpha1;
@@ -76,7 +95,10 @@ export const TemplateCard = ({ template }: TemplateCardProps) => {
const backstageTheme = useTheme<BackstageTheme>();
const rootLink = useRouteRef(rootRouteRef);
const templateProps = getTemplateCardProps(template);
const ownedByRelations = getEntityRelations(
template as Entity,
RELATION_OWNED_BY,
);
const themeId = pageTheme[templateProps.type] ? templateProps.type : 'other';
const theme = backstageTheme.getPageTheme({ themeId });
const classes = useStyles({ backgroundImage: theme.backgroundImage });
@@ -94,13 +116,27 @@ export const TemplateCard = ({ template }: TemplateCardProps) => {
classes={{ root: classes.title }}
/>
</CardMedia>
<CardContent>
<CardContent style={{ display: 'grid' }}>
<Box className={classes.box}>
<Typography variant="body2" className={classes.label}>
Description
</Typography>
{templateProps.description}
</Box>
<Box className={classes.box}>
<Typography variant="body2" className={classes.label}>
Owner
</Typography>
<EntityRefLinks entityRefs={ownedByRelations} defaultKind="Group" />
</Box>
<Box>
<Typography variant="body2" className={classes.label}>
Tags
</Typography>
{templateProps.tags?.map(tag => (
<Chip size="small" label={tag} key={tag} />
))}
</Box>
<Box className={classes.description}>{templateProps.description}</Box>
</CardContent>
<CardActions>
<Button