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
+8 -9
View File
@@ -3,22 +3,21 @@
'@backstage/plugin-techdocs-backend': minor
---
Introduce the annotation `backstage.io/techdocs-ref: <relative-target>` as an alias for `backstage.io/techdocs-ref: dir:<relative-target>`.
This annotation works with both the basic and the recommended flow, however, it will be most useful with the basic approach.
Improve the annotation `backstage.io/techdocs-ref: dir:<relative-target>` that links to a path that is relative to the source of the annotated entity.
This annotation works with the basic and the recommended flow, however, it will be most useful with the basic approach.
This change remove the deprecation of the `dir` reference and provides first-class support for it.
In addition, this change removes the support of the deprecated `github`, `gitlab`, and `azure/api` locations from the `dir` reference preparer.
#### Example Usage
The new annotation is convenient if the documentation is stored in the same location, i.e. the same git repository, as the `catalog-info.yaml`.
The annotation is convenient if the documentation is stored in the same location, i.e. the same git repository, as the `catalog-info.yaml`.
While it is still supported to add full URLs such as `backstage.io/techdocs-ref: url:https://...` for custom setups, documentation is mostly stored in the same repository as the entity definition.
By automatically resolving the target relative to the registration location of the entity, the configuration overhead for this default setup is minimized.
Since it leverages the `@backstage/integrations` package for the URL resolution, this is compatible with every supported source.
Consider the following examples:
> Note that the short version `<target>` is only an alias for the still supported `dir:<target>`.
1. "I have a repository with a single `catalog-info.yaml` and a TechDocs page in the root folder!"
```
@@ -29,7 +28,7 @@ https://github.com/backstage/example/tree/main/
| > metadata:
| > name: example
| > annotations:
| > backstage.io/techdocs-ref: . # -> same folder
| > backstage.io/techdocs-ref: dir:. # -> same folder
| > spec: {}
|- docs/
|- mkdocs.yml
@@ -45,7 +44,7 @@ https://bitbucket.org/my-owner/my-project/src/master/
| > metadata:
| > name: example
| > annotations:
| > backstage.io/techdocs-ref: ./some-folder # -> subfolder
| > backstage.io/techdocs-ref: dir:./some-folder # -> subfolder
| > spec: {}
|- some-folder/
|- docs/
@@ -63,7 +62,7 @@ https://dev.azure.com/organization/project/_git/repository
| > metadata:
| > name: my-1st-module
| > annotations:
| > backstage.io/techdocs-ref: . # -> same folder
| > backstage.io/techdocs-ref: dir:. # -> same folder
| > spec: {}
|- docs/
|- mkdocs.yml
@@ -74,7 +73,7 @@ https://dev.azure.com/organization/project/_git/repository
| > metadata:
| > name: my-2nd-module
| > annotations:
| > backstage.io/techdocs-ref: . # -> same folder
| > backstage.io/techdocs-ref: dir:. # -> same folder
| > spec: {}
|- docs/
|- mkdocs.yml
+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) {