Remove the short-form annotation and only support the full dir:. option

Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
Dominik Henneke
2021-07-21 15:13:16 +02:00
parent eabf5bb598
commit d591b764d0
6 changed files with 73 additions and 208 deletions
+1 -12
View File
@@ -137,18 +137,6 @@ export const getDefaultBranch: (
config: Config,
) => Promise<string>;
// Warning: (ae-missing-release-tag) "getDirLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export const getDirLocation: (
entity: Entity,
) =>
| {
type: 'dir';
target: string;
}
| undefined;
// Warning: (ae-missing-release-tag) "getDocFilesFromRepository" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -382,6 +370,7 @@ export type TechDocsMetadata = {
// @public
export const transformDirLocation: (
entity: Entity,
dirAnnotation: ParsedLocationAnnotation,
scmIntegrations: ScmIntegrationRegistry,
) => {
type: 'dir' | 'url';
+32 -105
View File
@@ -26,7 +26,6 @@ import os from 'os';
import path from 'path';
import { Readable } from 'stream';
import {
getDirLocation,
getDocFilesFromRepository,
getLocationForEntity,
parseReferenceAnnotation,
@@ -126,76 +125,11 @@ describe('parseReferenceAnnotation', () => {
});
});
describe('getDirLocation', () => {
it.each`
techdocsRef | responseTarget
${undefined} | ${undefined}
${16} | ${undefined}
${'.'} | ${'.'}
${'dir:.'} | ${'.'}
${'./relative'} | ${'./relative'}
${'dir:./relative'} | ${'./relative'}
${'dir:https://github.com...'} | ${'https://github.com...'}
${'url:https://github.com...'} | ${undefined}
`(
'should handle "backstage.io/techdocs-ref: $techdocsRef" correctly',
({ techdocsRef, responseTarget }) => {
const entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'test',
annotations: {
'backstage.io/techdocs-ref': techdocsRef,
},
},
};
const result = getDirLocation(entity);
expect(result).toEqual(
responseTarget && { type: 'dir', target: responseTarget },
);
},
);
it('Reject https urls and hint to the url: location type', async () => {
const entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'test',
annotations: {
'backstage.io/techdocs-ref': 'https://github.com/backstage/backstage',
},
},
};
expect(() => getDirLocation(entity)).toThrow(
/please prefix it with 'url:'/,
);
});
});
describe('transformDirLocation', () => {
it('should reject missing annotation', () => {
const entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'test',
},
};
expect(() => transformDirLocation(entity, scmIntegrations)).toThrow(
/No techdocs location annotation provided in entity: component:default\/test/,
);
});
it.each`
techdocsRef | target
${'.'} | ${'https://my-url/folder/'}
${'./sub-folder'} | ${'https://my-url/folder/sub-folder'}
techdocsRef | target
${'dir:.'} | ${'https://my-url/folder/'}
${'dir:./sub-folder'} | ${'https://my-url/folder/sub-folder'}
`(
'should transform "$techdocsRef" for url type locations',
({ techdocsRef, target }) => {
@@ -215,16 +149,20 @@ describe('transformDirLocation', () => {
},
};
const result = transformDirLocation(entity, scmIntegrations);
const result = transformDirLocation(
entity,
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
scmIntegrations,
);
expect(result).toEqual({ type: 'url', target });
},
);
it.each`
techdocsRef | target
${'.'} | ${path.join(rootDir, 'working-copy')}
${'./sub-folder'} | ${path.join(rootDir, 'working-copy', 'sub-folder')}
techdocsRef | target
${'dir:.'} | ${path.join(rootDir, 'working-copy')}
${'dir:./sub-folder'} | ${path.join(rootDir, 'working-copy', 'sub-folder')}
`(
'should transform "$techdocsRef" for file type locations',
({ techdocsRef, target }) => {
@@ -244,7 +182,11 @@ describe('transformDirLocation', () => {
},
};
const result = transformDirLocation(entity, scmIntegrations);
const result = transformDirLocation(
entity,
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
scmIntegrations,
);
expect(result).toEqual({ type: 'dir', target });
},
@@ -262,12 +204,18 @@ describe('transformDirLocation', () => {
metadata: {
name: 'test',
annotations: {
'backstage.io/techdocs-ref': '..',
'backstage.io/techdocs-ref': 'dir:..',
},
},
};
expect(() => transformDirLocation(entity, scmIntegrations)).toThrow(
expect(() =>
transformDirLocation(
entity,
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
scmIntegrations,
),
).toThrow(
/Relative path is not allowed to refer to a directory outside its parent/,
);
});
@@ -284,43 +232,22 @@ describe('transformDirLocation', () => {
metadata: {
name: 'test',
annotations: {
'backstage.io/techdocs-ref': '.',
'backstage.io/techdocs-ref': 'dir:.',
},
},
};
expect(() => transformDirLocation(entity, scmIntegrations)).toThrow(
/Unable to resolve location type other/,
);
expect(() =>
transformDirLocation(
entity,
parseReferenceAnnotation('backstage.io/techdocs-ref', entity),
scmIntegrations,
),
).toThrow(/Unable to resolve location type other/);
});
});
describe('getLocationForEntity', () => {
it('should handle implicit dir locations', () => {
(getEntitySourceLocation as jest.Mock).mockReturnValue({
type: 'url',
target: 'https://my-url/folder/',
});
const entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'test',
annotations: {
'backstage.io/techdocs-ref': '.',
},
},
};
const parsedLocationAnnotation = getLocationForEntity(
entity,
scmIntegrations,
);
expect(parsedLocationAnnotation.type).toBe('url');
expect(parsedLocationAnnotation.target).toBe('https://my-url/folder/');
});
it('should handle dir locations', () => {
(getEntitySourceLocation as jest.Mock).mockReturnValue({
type: 'url',
+11 -61
View File
@@ -23,7 +23,6 @@ import {
Entity,
getEntitySourceLocation,
parseLocationReference,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
@@ -60,44 +59,6 @@ export const parseReferenceAnnotation = (
};
};
/**
* Check if the entity provides a `backstage.io/techdocs-ref` annotation of type `dir`
* and return the annotation. It accepts the two variants `<target>` and `dir:<target>`.
*
* @param entity - the entity to check
*/
export const getDirLocation = (
entity: Entity,
): { type: 'dir'; target: string } | undefined => {
const annotation = entity.metadata.annotations?.['backstage.io/techdocs-ref'];
if (!annotation) {
return undefined;
}
if (typeof annotation !== 'string') {
return undefined;
}
// if the string doesn't contain `:`, interpret it as the target of a dir type
if (!annotation.includes(':')) {
return { type: 'dir', target: annotation };
}
// note that `backstage.io/techdocs-ref: https://...` is invalid and will throw
const reference = parseLocationReference(annotation);
if (reference.type === 'dir') {
return {
type: 'dir',
target: reference.target,
};
}
// ignore any other types
return undefined;
};
/**
* TechDocs references of type `dir` are relative the source location of the entity.
* This function transforms relative references to absolute ones, based on the
@@ -107,30 +68,22 @@ export const getDirLocation = (
* an absolute `dir` location.
*
* @param entity - the entity with annotations
* @param scmIntegrations - access to the scmIntegrationt to do url transformations
* @param dirAnnotation - the parsed techdocs-ref annotation of type 'dir'
* @param scmIntegrations - access to the scmIntegration to do url transformations
* @throws if the entity doesn't specify a `dir` location or is ingested from an unsupported location.
* @returns the transformed location with an absolute target.
*/
export const transformDirLocation = (
entity: Entity,
dirAnnotation: ParsedLocationAnnotation,
scmIntegrations: ScmIntegrationRegistry,
): { type: 'dir' | 'url'; target: string } => {
const dirLocation = getDirLocation(entity);
if (!dirLocation) {
throw new InputError(
`No techdocs location annotation provided in entity: ${stringifyEntityRef(
entity,
)}`,
);
}
const location = getEntitySourceLocation(entity);
switch (location.type) {
case 'url': {
const target = scmIntegrations.resolveUrl({
url: dirLocation.target,
url: dirAnnotation.target,
base: location.target,
});
@@ -144,7 +97,7 @@ export const transformDirLocation = (
// only permit targets in the same folder as the target of the `file` location!
const target = resolveSafeChildPath(
path.dirname(location.target),
dirLocation.target,
dirAnnotation.target,
);
return {
@@ -162,24 +115,21 @@ export const getLocationForEntity = (
entity: Entity,
scmIntegration: ScmIntegrationRegistry,
): ParsedLocationAnnotation => {
// try to resolve relative references first
if (getDirLocation(entity) !== undefined) {
return transformDirLocation(entity, scmIntegration);
}
const { type, target } = parseReferenceAnnotation(
const annotation = parseReferenceAnnotation(
'backstage.io/techdocs-ref',
entity,
);
switch (type) {
switch (annotation.type) {
case 'github':
case 'gitlab':
case 'azure/api':
case 'url':
return { type, target };
return annotation;
case 'dir':
return transformDirLocation(entity, annotation, scmIntegration);
default:
throw new Error(`Invalid reference annotation ${type}`);
throw new Error(`Invalid reference annotation ${annotation.type}`);
}
};
@@ -23,7 +23,7 @@ import {
ScmIntegrations,
} from '@backstage/integration';
import { Logger } from 'winston';
import { transformDirLocation } from '../../helpers';
import { parseReferenceAnnotation, transformDirLocation } from '../../helpers';
import { PreparerBase, PreparerResponse } from './types';
export class DirectoryPreparer implements PreparerBase {
@@ -39,18 +39,29 @@ export class DirectoryPreparer implements PreparerBase {
entity: Entity,
options?: { logger?: Logger; etag?: string },
): Promise<PreparerResponse> {
const { type, target } = transformDirLocation(entity, this.scmIntegrations);
const annotation = parseReferenceAnnotation(
'backstage.io/techdocs-ref',
entity,
);
const { type, target } = transformDirLocation(
entity,
annotation,
this.scmIntegrations,
);
switch (type) {
case 'url': {
options?.logger?.info(`Download documentation from ${target}`);
options?.logger?.debug(`Reading files from ${target}`);
// the target is an absolute url since it has already been transformed
const response = await this.reader.readTree(target, {
etag: options?.etag,
});
const preparedDir = await response.dir();
options?.logger?.debug(`Tree downloaded and stored at ${preparedDir}`);
return {
preparedDir: await response.dir(),
preparedDir,
etag: response.etag,
};
}
@@ -15,11 +15,10 @@
*/
import { UrlReader } from '@backstage/backend-common';
import { Entity, parseLocationReference } from '@backstage/catalog-model';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { Logger } from 'winston';
import { getDirLocation } from '../../helpers';
import { parseReferenceAnnotation } from '../../helpers';
import { CommonGitPreparer } from './commonGit';
import { DirectoryPreparer } from './dir';
import { PreparerBase, PreparerBuilder, RemoteProtocol } from './types';
@@ -63,20 +62,10 @@ export class Preparers implements PreparerBuilder {
}
get(entity: Entity): PreparerBase {
const annotation =
entity.metadata.annotations?.['backstage.io/techdocs-ref'];
if (!annotation) {
throw new InputError(
`No location annotation provided in entity: ${entity.metadata.name}`,
);
}
// the dir processor handles both `<target>` and `dir:<target>`
if (getDirLocation(entity) !== undefined) {
return this.preparerMap.get('dir')!;
}
const { type } = parseLocationReference(annotation);
const { type } = parseReferenceAnnotation(
'backstage.io/techdocs-ref',
entity,
);
const preparer = this.preparerMap.get(type as RemoteProtocol);
if (!preparer) {