Merge pull request #9754 from backstage/freben/the-ref-revolution

Remove all usage of, and deprecate, `EntityRef`
This commit is contained in:
Fredrik Adelöw
2022-02-23 15:34:08 +01:00
committed by GitHub
21 changed files with 142 additions and 139 deletions
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-bazaar': patch
'@backstage/plugin-code-coverage-backend': patch
'@backstage/plugin-jenkins': patch
'@backstage/plugin-jenkins-backend': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-tech-insights-backend': patch
'@backstage/plugin-todo-backend': patch
---
Remove usages of `EntityRef` and `parseEntityName` from `@backstage/catalog-model`
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/catalog-model': patch
---
**DEPRECATED**: The `EntityRef` type and `parseEntityName` function are now
deprecated, and will soon be removed. This is part of a larger movement toward
fixing the poorly named `EntityName` type which should instead have been named
`EntityRef`. Please remove any usage of these as soon as possible.
+38 -7
View File
@@ -69,7 +69,14 @@ export class CommonValidatorFunctions {
// @public @deprecated
export function compareEntityToRef(
entity: Entity,
ref: EntityRef | EntityName,
ref:
| string
| {
kind?: string;
namespace?: string;
name: string;
}
| EntityName,
context?: {
defaultKind?: string;
defaultNamespace?: string;
@@ -212,7 +219,7 @@ export type EntityPolicy = {
enforce(entity: Entity): Promise<Entity | undefined>;
};
// @public
// @public @deprecated
export type EntityRef =
| string
| {
@@ -385,9 +392,15 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy {
export const ORIGIN_LOCATION_ANNOTATION =
'backstage.io/managed-by-origin-location';
// @public
// @public @deprecated
export function parseEntityName(
ref: EntityRef,
ref:
| string
| {
kind?: string;
namespace?: string;
name: string;
},
context?: {
defaultKind?: string;
defaultNamespace?: string;
@@ -396,7 +409,13 @@ export function parseEntityName(
// @public
export function parseEntityRef(
ref: EntityRef,
ref:
| string
| {
kind?: string;
namespace?: string;
name: string;
},
context?: {
defaultKind: string;
defaultNamespace: string;
@@ -405,7 +424,13 @@ export function parseEntityRef(
// @public
export function parseEntityRef(
ref: EntityRef,
ref:
| string
| {
kind?: string;
namespace?: string;
name: string;
},
context?: {
defaultKind: string;
},
@@ -417,7 +442,13 @@ export function parseEntityRef(
// @public
export function parseEntityRef(
ref: EntityRef,
ref:
| string
| {
kind?: string;
namespace?: string;
name: string;
},
context?: {
defaultNamespace: string;
},
@@ -194,12 +194,12 @@ describe('ref', () => {
});
expect(parseEntityRef('a:c')).toEqual({
kind: 'a',
namespace: undefined,
namespace: 'default',
name: 'c',
});
expect(parseEntityRef('c')).toEqual({
kind: undefined,
namespace: undefined,
namespace: 'default',
name: 'c',
});
});
+15 -9
View File
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { EntityName, EntityRef } from '../types';
import { DEFAULT_NAMESPACE } from './constants';
import { EntityName } from '../types';
import { Entity } from './Entity';
function parseRefString(ref: string): {
@@ -76,13 +76,14 @@ export type EntityRefContext = {
* otherwise specified as part of the options, and will throw an error if no
* kind was specified in the input reference and no default kind was given.
*
* @deprecated Please use parseEntityRef instead
* @public
* @param ref - The reference to parse
* @param context - The context of defaults that the parsing happens within
* @returns A complete entity name
*/
export function parseEntityName(
ref: EntityRef,
ref: string | { kind?: string; namespace?: string; name: string },
context: {
/** The default kind, if none is given in the reference */
defaultKind?: string;
@@ -119,7 +120,7 @@ export function parseEntityName(
* @returns The compound form of the reference
*/
export function parseEntityRef(
ref: EntityRef,
ref: string | { kind?: string; namespace?: string; name: string },
context?: { defaultKind: string; defaultNamespace: string },
): EntityName;
/**
@@ -128,7 +129,7 @@ export function parseEntityRef(
* @public
*/
export function parseEntityRef(
ref: EntityRef,
ref: string | { kind?: string; namespace?: string; name: string },
context?: { defaultKind: string },
): {
kind: string;
@@ -141,7 +142,7 @@ export function parseEntityRef(
* @public
*/
export function parseEntityRef(
ref: EntityRef,
ref: string | { kind?: string; namespace?: string; name: string },
context?: { defaultNamespace: string },
): {
kind?: string;
@@ -154,7 +155,7 @@ export function parseEntityRef(
* @public
*/
export function parseEntityRef(
ref: EntityRef,
ref: string | { kind?: string; namespace?: string; name: string },
context: {
/** The default kind, if none is given in the reference */
defaultKind?: string;
@@ -170,11 +171,13 @@ export function parseEntityRef(
throw new Error(`Entity reference must not be empty`);
}
const defaultNamespace = context.defaultNamespace || DEFAULT_NAMESPACE;
if (typeof ref === 'string') {
const parsed = parseRefString(ref);
return {
kind: parsed.kind ?? context.defaultKind,
namespace: parsed.namespace ?? context.defaultNamespace,
namespace: parsed.namespace ?? defaultNamespace,
name: parsed.name,
};
}
@@ -190,7 +193,7 @@ export function parseEntityRef(
return {
kind: kind ?? context.defaultKind,
namespace: namespace ?? context.defaultNamespace,
namespace: namespace ?? defaultNamespace,
name,
};
}
@@ -249,7 +252,10 @@ export function stringifyEntityRef(
*/
export function compareEntityToRef(
entity: Entity,
ref: EntityRef | EntityName,
ref:
| string
| { kind?: string; namespace?: string; name: string }
| EntityName,
context?: {
/** The default kind, if none is given in the reference */
defaultKind?: string;
+1
View File
@@ -40,6 +40,7 @@ export type EntityName = {
* A reference by name to an entity, either as a compact string representation,
* or as a compound reference structure.
*
* @deprecated Please use string directly, or EntityName (depending on what you actually need)
* @remarks
*
* The string representation is on the form `[<kind>:][<namespace>/]<name>`.
@@ -47,7 +47,6 @@ import useAsyncFn from 'react-use/lib/useAsyncFn';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import {
parseEntityName,
stringifyEntityRef,
Entity,
parseEntityRef,
@@ -150,7 +149,7 @@ export const HomePageBazaarInfoCard = ({
const getEntityPageLink = () => {
if (bazaarProject?.value?.entityRef) {
const { name, kind, namespace } = parseEntityName(
const { name, kind, namespace } = parseEntityRef(
bazaarProject.value.entityRef,
);
return entityLink({ kind, namespace, name });
@@ -107,7 +107,7 @@ export const SortView = () => {
if (catalogEntityRefs) {
bazaarProjects.value?.forEach(async (project: BazaarProject) => {
if (project.entityRef) {
if (!catalogEntityRefs?.includes(project.entityRef as string)) {
if (!catalogEntityRefs?.includes(project.entityRef)) {
await bazaarApi.updateProject({
...project,
entityRef: null,
+1 -3
View File
@@ -14,8 +14,6 @@
* limitations under the License.
*/
import { EntityRef } from '@backstage/catalog-model';
export type Member = {
itemId: number;
userId: string;
@@ -30,7 +28,7 @@ export type Size = 'small' | 'medium' | 'large';
export type BazaarProject = {
name: string;
id: number;
entityRef?: EntityRef;
entityRef?: string;
community: string;
status: Status;
description: string;
@@ -15,7 +15,7 @@
*/
import { resolvePackagePath } from '@backstage/backend-common';
import { NotFoundError } from '@backstage/errors';
import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model';
import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
import { Knex } from 'knex';
import { v4 as uuid } from 'uuid';
import { aggregateCoverage } from './CoverageUtils';
@@ -103,7 +103,7 @@ export class CodeCoverageDatabase implements CodeCoverageStore {
.map(r => JSON.parse(r.coverage))
.map(c => aggregateCoverage(c));
const entityName = parseEntityName(entity);
const entityName = parseEntityRef(entity);
return {
entity: {
@@ -34,9 +34,8 @@ import { Cobertura } from './converter/cobertura';
import { Jacoco } from './converter/jacoco';
import { Converter } from './converter';
import {
EntityRef,
getEntitySourceLocation,
parseEntityName,
parseEntityRef,
} from '@backstage/catalog-model';
export interface RouterOptions {
@@ -78,7 +77,7 @@ export const makeRouter = async (
*/
router.get('/report', async (req, res) => {
const { entity } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityName = parseEntityRef(entity as string);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
@@ -101,7 +100,7 @@ export const makeRouter = async (
*/
router.get('/history', async (req, res) => {
const { entity } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityName = parseEntityRef(entity as string);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
@@ -120,7 +119,7 @@ export const makeRouter = async (
*/
router.get('/file-content', async (req, res) => {
const { entity, path } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityName = parseEntityRef(entity as string);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
@@ -172,7 +171,7 @@ export const makeRouter = async (
*/
router.post('/report', async (req, res) => {
const { entity, coverageType } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityName = parseEntityRef(entity as string);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
@@ -18,7 +18,7 @@ import { createServiceBuilder } from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
import { EntityRef } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { JenkinsInfo } from './jenkinsInfoProvider';
export interface ServerOptions {
@@ -35,7 +35,7 @@ export async function startStandaloneServer(
const router = await createRouter({
logger,
jenkinsInfoProvider: {
async getInstance(_: { entityRef: EntityRef }): Promise<JenkinsInfo> {
async getInstance(_: { entityRef: EntityName }): Promise<JenkinsInfo> {
return { baseUrl: 'https://example.com/', jobFullName: 'build-foo' };
},
},
+4 -16
View File
@@ -10,7 +10,6 @@ import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import type { EntityName } from '@backstage/catalog-model';
import type { EntityRef } from '@backstage/catalog-model';
import { IdentityApi } from '@backstage/core-plugin-api';
import { InfoCardVariants } from '@backstage/core-components';
import { RouteRef } from '@backstage/core-plugin-api';
@@ -55,7 +54,7 @@ export interface JenkinsApi {
}): Promise<Build>;
// Warning: (ae-forgotten-export) The symbol "Project" needs to be exported by the entry point index.d.ts
getProjects(options: {
entity: EntityRef;
entity: EntityName;
filter: {
branch?: string;
};
@@ -82,31 +81,20 @@ export class JenkinsClient implements JenkinsApi {
identityApi: IdentityApi;
});
// (undocumented)
getBuild({
entity,
jobFullName,
buildNumber,
}: {
getBuild(options: {
entity: EntityName;
jobFullName: string;
buildNumber: string;
}): Promise<Build>;
// (undocumented)
getProjects({
entity,
filter,
}: {
getProjects(options: {
entity: EntityName;
filter: {
branch?: string;
};
}): Promise<Project[]>;
// (undocumented)
retry({
entity,
jobFullName,
buildNumber,
}: {
retry(options: {
entity: EntityName;
jobFullName: string;
buildNumber: string;
+8 -16
View File
@@ -19,7 +19,7 @@ import {
DiscoveryApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import type { EntityName, EntityRef } from '@backstage/catalog-model';
import type { EntityName } from '@backstage/catalog-model';
import { ResponseError } from '@backstage/errors';
export const jenkinsApiRef = createApiRef<JenkinsApi>({
@@ -80,7 +80,7 @@ export interface JenkinsApi {
*/
getProjects(options: {
/** the entity whose jobs should be retrieved. */
entity: EntityRef;
entity: EntityName;
/** a filter on jobs. Currently this just takes a branch (and assumes certain structures in jenkins) */
filter: { branch?: string };
}): Promise<Project[]>;
@@ -117,13 +117,11 @@ export class JenkinsClient implements JenkinsApi {
this.identityApi = options.identityApi;
}
async getProjects({
entity,
filter,
}: {
async getProjects(options: {
entity: EntityName;
filter: { branch?: string };
}): Promise<Project[]> {
const { entity, filter } = options;
const url = new URL(
`${await this.discoveryApi.getBaseUrl(
'jenkins',
@@ -158,15 +156,12 @@ export class JenkinsClient implements JenkinsApi {
);
}
async getBuild({
entity,
jobFullName,
buildNumber,
}: {
async getBuild(options: {
entity: EntityName;
jobFullName: string;
buildNumber: string;
}): Promise<Build> {
const { entity, jobFullName, buildNumber } = options;
const url = `${await this.discoveryApi.getBaseUrl(
'jenkins',
)}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent(
@@ -186,15 +181,12 @@ export class JenkinsClient implements JenkinsApi {
return (await response.json()).build;
}
async retry({
entity,
jobFullName,
buildNumber,
}: {
async retry(options: {
entity: EntityName;
jobFullName: string;
buildNumber: string;
}): Promise<void> {
const { entity, jobFullName, buildNumber } = options;
const url = `${await this.discoveryApi.getBaseUrl(
'jenkins',
)}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent(
@@ -20,11 +20,11 @@ import {
ANNOTATION_LOCATION,
parseLocationRef,
ANNOTATION_SOURCE_LOCATION,
EntityRef,
parseEntityRef,
EntityName,
DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, ConflictError, NotFoundError } from '@backstage/errors';
import { assertError, InputError, NotFoundError } from '@backstage/errors';
import {
TemplateEntityV1beta2,
TemplateEntityV1beta3,
@@ -87,47 +87,29 @@ export function getEntityBaseUrl(entity: Entity): string | undefined {
}
/**
* Will use the provided CatalogApi to go find the template entity ref that is provided with and additional token
* Returns the first matching template, throws a NotFoundError or ConflictError if 0 or multiple templates are found.
* Will use the provided CatalogApi to go find the given template entity with an additional token.
* Returns the matching template, or throws a NotFoundError if no such template existed.
*/
export async function findTemplate({
entityRef,
token,
catalogApi,
}: {
entityRef: EntityRef;
export async function findTemplate(options: {
entityRef: EntityName;
token?: string;
catalogApi: CatalogApi;
}): Promise<TemplateEntityV1beta3 | TemplateEntityV1beta2> {
const parsedEntityRef = parseEntityRef(entityRef, {
defaultKind: 'template',
defaultNamespace: 'default',
});
const { items } = await catalogApi.getEntities(
{
filter: {
kind: 'template',
'metadata.name': parsedEntityRef.name,
'metadata.namespace': parsedEntityRef.namespace,
},
},
{
token,
},
);
const { entityRef, token, catalogApi } = options;
const templates = items.filter(
(entity): entity is TemplateEntityV1beta3 | TemplateEntityV1beta2 =>
entity.kind === 'Template',
);
if (templates.length !== 1) {
if (templates.length > 1) {
throw new ConflictError('Templates lookup resulted in multiple matches');
} else {
throw new NotFoundError('Template not found');
}
if (entityRef.namespace.toLocaleLowerCase('en-US') !== DEFAULT_NAMESPACE) {
throw new InputError(
`Invalid namespace, only '${DEFAULT_NAMESPACE}' namespace is supported`,
);
}
if (entityRef.kind.toLocaleLowerCase('en-US') !== 'template') {
throw new InputError(`Invalid kind, only 'Template' kind is supported`);
}
return templates[0];
const template = await catalogApi.getEntityByName(entityRef, { token });
if (!template) {
throw new NotFoundError(`Template ${entityRef} not found`);
}
return template as TemplateEntityV1beta3 | TemplateEntityV1beta2;
}
@@ -50,10 +50,10 @@ import request from 'supertest';
import { createRouter, DatabaseTaskStore, TaskBroker } from '../index';
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
const createCatalogClient = (templates: any[] = []) =>
const createCatalogClient = (template: any) =>
({
getEntities: async () => ({ items: templates }),
} as CatalogApi);
getEntityByName: async () => template,
} as unknown as CatalogApi);
function createDatabase(): PluginDatabaseManager {
return DatabaseManager.fromConfig(
@@ -120,7 +120,7 @@ describe('createRouter', () => {
logger: getVoidLogger(),
config: new ConfigReader({}),
database: createDatabase(),
catalogClient: createCatalogClient([template]),
catalogClient: createCatalogClient(template),
containerRunner: new DockerContainerRunner({} as any),
reader: mockUrlReader,
taskBroker,
@@ -20,7 +20,10 @@ import {
UrlReader,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { stringifyEntityRef } from '@backstage/catalog-model';
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { InputError, NotFoundError } from '@backstage/errors';
@@ -141,18 +144,6 @@ export async function createRouter(
'/v2/templates/:namespace/:kind/:name/parameter-schema',
async (req, res) => {
const { namespace, kind, name } = req.params;
if (namespace !== 'default') {
throw new InputError(
`Invalid namespace, only 'default' namespace is supported`,
);
}
if (kind.toLowerCase() !== 'template') {
throw new InputError(
`Invalid kind, only 'Template' kind is supported`,
);
}
const template = await findTemplate({
catalogApi: catalogClient,
entityRef: { kind, namespace, name },
@@ -188,16 +179,13 @@ export async function createRouter(
})
.post('/v2/tasks', async (req, res) => {
const templateName: string = req.body.templateName;
const { kind, namespace } = { kind: 'template', namespace: 'default' };
const kind = 'template';
const namespace = DEFAULT_NAMESPACE;
const values = req.body.values;
const token = getBearerToken(req.headers.authorization);
const template = await findTemplate({
catalogApi: catalogClient,
entityRef: {
name: templateName,
kind,
namespace,
},
entityRef: { kind, namespace, name: templateName },
token: getBearerToken(req.headers.authorization),
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { parseEntityName } from '@backstage/catalog-model';
import { parseEntityRef } from '@backstage/catalog-model';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { Box } from '@material-ui/core';
import LanguageIcon from '@material-ui/icons/Language';
@@ -57,7 +57,7 @@ export const TaskPageLinks = ({ output }: TaskPageLinksProps) => {
.filter(({ url, entityRef }) => url || entityRef)
.map(({ url, entityRef, title, icon }) => {
if (entityRef) {
const entityName = parseEntityName(entityRef);
const entityName = parseEntityRef(entityRef);
const target = entityRoute(entityName);
return { title, icon, url: target };
}
@@ -26,7 +26,7 @@ import { rsort } from 'semver';
import { groupBy, omit } from 'lodash';
import { DateTime } from 'luxon';
import { Logger } from 'winston';
import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model';
import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
import { isMaxItems, isTtl } from '../fact/factRetrievers/utils';
type Transaction = Knex.Transaction;
@@ -161,7 +161,7 @@ export class TechInsightsDatabase implements TechInsightsStore {
return groupBy(
results.map(it => {
const { namespace, kind, name } = parseEntityName(it.entity);
const { namespace, kind, name } = parseEntityRef(it.entity);
const timestamp =
typeof it.timestamp === 'string'
? DateTime.fromISO(it.timestamp)
@@ -253,7 +253,7 @@ export class TechInsightsDatabase implements TechInsightsStore {
private dbFactRowsToTechInsightFacts(rows: RawDbFactRow[]) {
return rows.reduce((acc, it) => {
const { namespace, kind, name } = parseEntityName(it.entity);
const { namespace, kind, name } = parseEntityRef(it.entity);
const timestamp =
typeof it.timestamp === 'string'
? DateTime.fromISO(it.timestamp)
@@ -28,8 +28,7 @@ import { DateTime } from 'luxon';
import { PersistenceContext } from './persistence/persistenceContext';
import {
EntityName,
EntityRef,
parseEntityName,
parseEntityRef,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { errorHandler } from '@backstage/backend-common';
@@ -130,7 +129,7 @@ export async function createRouter<
*/
router.get('/facts/latest', async (req, res) => {
const { entity } = req.query;
const { namespace, kind, name } = parseEntityName(entity as EntityRef);
const { namespace, kind, name } = parseEntityRef(entity as string);
const ids = req.query.ids as string[];
return res.send(
await techInsightsStore.getLatestFactsByIds(
@@ -145,7 +144,7 @@ export async function createRouter<
*/
router.get('/facts/range', async (req, res) => {
const { entity } = req.query;
const { namespace, kind, name } = parseEntityName(entity as EntityRef);
const { namespace, kind, name } = parseEntityRef(entity as string);
const ids = req.query.ids as string[];
const startDatetime = DateTime.fromISO(req.query.startDatetime as string);
+2 -2
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { EntityName, parseEntityName } from '@backstage/catalog-model';
import { EntityName, parseEntityRef } from '@backstage/catalog-model';
import { InputError } from '@backstage/errors';
import express from 'express';
import Router from 'express-promise-router';
@@ -55,7 +55,7 @@ export async function createRouter(
let entity: EntityName | undefined = undefined;
if (entityRef) {
try {
entity = parseEntityName(entityRef);
entity = parseEntityRef(entityRef);
} catch (error) {
throw new InputError(`Invalid entity ref, ${error}`);
}