Introduce the annotation backstage.io/techdocs-ref: <relative-target> as an alias for backstage.io/techdocs-ref: dir:<relative-target>
Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
import { GitLabIntegrationConfig } from '@backstage/integration';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
@@ -47,9 +48,15 @@ export class CommonGitPreparer implements PreparerBase {
|
||||
//
|
||||
// @public (undocumented)
|
||||
export class DirectoryPreparer implements PreparerBase {
|
||||
constructor(config: Config, logger: Logger_2, reader: UrlReader);
|
||||
constructor(config: Config, _logger: Logger_2, reader: UrlReader);
|
||||
// (undocumented)
|
||||
prepare(entity: Entity): Promise<PreparerResponse>;
|
||||
prepare(
|
||||
entity: Entity,
|
||||
options?: {
|
||||
logger?: Logger_2;
|
||||
etag?: string;
|
||||
},
|
||||
): Promise<PreparerResponse>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "GeneratorBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -130,6 +137,18 @@ 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)
|
||||
@@ -189,7 +208,10 @@ export const getLastCommitTimestamp: (
|
||||
// Warning: (ae-missing-release-tag) "getLocationForEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const getLocationForEntity: (entity: Entity) => ParsedLocationAnnotation;
|
||||
export const getLocationForEntity: (
|
||||
entity: Entity,
|
||||
scmIntegration: ScmIntegrationRegistry,
|
||||
) => ParsedLocationAnnotation;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getTokenForGitRepo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -355,6 +377,17 @@ export type TechDocsMetadata = {
|
||||
etag: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "transformDirLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const transformDirLocation: (
|
||||
entity: Entity,
|
||||
scmIntegrations: ScmIntegrationRegistry,
|
||||
) => {
|
||||
type: 'dir' | 'url';
|
||||
target: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "UrlPreparer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -19,14 +19,27 @@ import {
|
||||
SearchResponse,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, getEntitySourceLocation } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import {
|
||||
getDirLocation,
|
||||
getDocFilesFromRepository,
|
||||
getLocationForEntity,
|
||||
parseReferenceAnnotation,
|
||||
transformDirLocation,
|
||||
} from './helpers';
|
||||
|
||||
jest.mock('@backstage/catalog-model', () => ({
|
||||
...jest.requireActual('@backstage/catalog-model'),
|
||||
getEntitySourceLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
|
||||
const entityBase: Entity = {
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
@@ -81,6 +94,10 @@ const mockEntityWithBadAnnotation: Entity = {
|
||||
},
|
||||
};
|
||||
|
||||
const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({}));
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('parseReferenceAnnotation', () => {
|
||||
it('should parse annotation', () => {
|
||||
const parsedLocationAnnotation = parseReferenceAnnotation(
|
||||
@@ -109,10 +126,230 @@ 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'}
|
||||
`(
|
||||
'should transform "$techdocsRef" for url type locations',
|
||||
({ techdocsRef, target }) => {
|
||||
(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': techdocsRef,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformDirLocation(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')}
|
||||
`(
|
||||
'should transform "$techdocsRef" for file type locations',
|
||||
({ techdocsRef, target }) => {
|
||||
(getEntitySourceLocation as jest.Mock).mockReturnValue({
|
||||
type: 'file',
|
||||
target: path.join(rootDir, 'working-copy', 'catalog-info.yaml'),
|
||||
});
|
||||
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': techdocsRef,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformDirLocation(entity, scmIntegrations);
|
||||
|
||||
expect(result).toEqual({ type: 'dir', target });
|
||||
},
|
||||
);
|
||||
|
||||
it('should reject unsafe file location', () => {
|
||||
(getEntitySourceLocation as jest.Mock).mockReturnValue({
|
||||
type: 'file',
|
||||
target: '/tmp/catalog-info.yaml',
|
||||
});
|
||||
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': '..',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => transformDirLocation(entity, scmIntegrations)).toThrow(
|
||||
/Relative path is not allowed to refer to a directory outside its parent/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject other location types', () => {
|
||||
(getEntitySourceLocation as jest.Mock).mockReturnValue({
|
||||
type: 'other',
|
||||
target: '/',
|
||||
});
|
||||
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': '.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => transformDirLocation(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',
|
||||
target: 'https://my-url/folder/',
|
||||
});
|
||||
|
||||
const entity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': 'dir:.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const parsedLocationAnnotation = getLocationForEntity(
|
||||
entity,
|
||||
scmIntegrations,
|
||||
);
|
||||
expect(parsedLocationAnnotation.type).toBe('url');
|
||||
expect(parsedLocationAnnotation.target).toBe('https://my-url/folder/');
|
||||
});
|
||||
|
||||
it('should get location for entity', () => {
|
||||
const parsedLocationAnnotation = getLocationForEntity(
|
||||
mockEntityWithAnnotation,
|
||||
scmIntegrations,
|
||||
);
|
||||
expect(parsedLocationAnnotation.type).toBe('url');
|
||||
expect(parsedLocationAnnotation.target).toBe(
|
||||
|
||||
@@ -14,10 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Git, UrlReader } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Entity, parseLocationReference } from '@backstage/catalog-model';
|
||||
import {
|
||||
Git,
|
||||
resolveSafeChildPath,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
getEntitySourceLocation,
|
||||
parseLocationReference,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import fs from 'fs-extra';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import os from 'os';
|
||||
@@ -50,9 +60,113 @@ 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
|
||||
* location the entity was ingested from. If the entity was registered by a `url`
|
||||
* location, it returns a `url` location with a resolved target that points to the
|
||||
* targeted subfolder. If the entity was registered by a `file` location, it returns
|
||||
* an absolute `dir` location.
|
||||
*
|
||||
* @param entity - the entity with annotations
|
||||
* @param scmIntegrations - access to the scmIntegrationt 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,
|
||||
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,
|
||||
base: location.target,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'url',
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
case 'file': {
|
||||
// only permit targets in the same folder as the target of the `file` location!
|
||||
const target = resolveSafeChildPath(
|
||||
path.dirname(location.target),
|
||||
dirLocation.target,
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'dir',
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InputError(`Unable to resolve location type ${location.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
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(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
@@ -64,14 +178,6 @@ export const getLocationForEntity = (
|
||||
case 'azure/api':
|
||||
case 'url':
|
||||
return { type, target };
|
||||
case 'dir':
|
||||
if (path.isAbsolute(target)) {
|
||||
return { type, target };
|
||||
}
|
||||
return parseReferenceAnnotation(
|
||||
'backstage.io/managed-by-location',
|
||||
entity,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Invalid reference annotation ${type}`);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
import { getVoidLogger, UrlReader } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { checkoutGitRepository } from '../../helpers';
|
||||
import { DirectoryPreparer } from './dir';
|
||||
|
||||
function normalizePath(path: string) {
|
||||
@@ -27,8 +26,6 @@ function normalizePath(path: string) {
|
||||
|
||||
jest.mock('../../helpers', () => ({
|
||||
...jest.requireActual<{}>('../../helpers'),
|
||||
checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch/'),
|
||||
getLastCommitTimestamp: jest.fn(() => 12345678),
|
||||
}));
|
||||
|
||||
const logger = getVoidLogger();
|
||||
@@ -71,7 +68,7 @@ describe('directory preparer', () => {
|
||||
expect(normalizePath(preparedDir)).toEqual('/directory/our-documentation');
|
||||
});
|
||||
|
||||
it('should merge managed-by-location and techdocs-ref when techdocs-ref is absolute', async () => {
|
||||
it('should reject when techdocs-ref is absolute', async () => {
|
||||
const directoryPreparer = new DirectoryPreparer(
|
||||
mockConfig,
|
||||
logger,
|
||||
@@ -84,11 +81,12 @@ describe('directory preparer', () => {
|
||||
'backstage.io/techdocs-ref': 'dir:/our-documentation/techdocs',
|
||||
});
|
||||
|
||||
const { preparedDir } = await directoryPreparer.prepare(mockEntity);
|
||||
expect(normalizePath(preparedDir)).toEqual('/our-documentation/techdocs');
|
||||
await expect(directoryPreparer.prepare(mockEntity)).rejects.toThrow(
|
||||
/Relative path is not allowed to refer to a directory outside its parent/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge managed-by-location and techdocs-ref when managed-by-location is a git repository', async () => {
|
||||
it('should reject when managed-by-location is a git repository', async () => {
|
||||
const directoryPreparer = new DirectoryPreparer(
|
||||
mockConfig,
|
||||
logger,
|
||||
@@ -101,10 +99,8 @@ describe('directory preparer', () => {
|
||||
'backstage.io/techdocs-ref': 'dir:./docs',
|
||||
});
|
||||
|
||||
const { preparedDir } = await directoryPreparer.prepare(mockEntity);
|
||||
expect(normalizePath(preparedDir)).toEqual(
|
||||
'/tmp/backstage-repo/org/name/branch/docs',
|
||||
await expect(directoryPreparer.prepare(mockEntity)).rejects.toThrow(
|
||||
/Unable to resolve location type github/,
|
||||
);
|
||||
expect(checkoutGitRepository).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,104 +15,57 @@
|
||||
*/
|
||||
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { InputError, NotModifiedError } from '@backstage/errors';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
checkoutGitRepository,
|
||||
getLastCommitTimestamp,
|
||||
parseReferenceAnnotation,
|
||||
} from '../../helpers';
|
||||
ScmIntegrationRegistry,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { Logger } from 'winston';
|
||||
import { transformDirLocation } from '../../helpers';
|
||||
import { PreparerBase, PreparerResponse } from './types';
|
||||
|
||||
export class DirectoryPreparer implements PreparerBase {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly logger: Logger,
|
||||
private readonly reader: UrlReader,
|
||||
) {
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
private readonly scmIntegrations: ScmIntegrationRegistry;
|
||||
private readonly reader: UrlReader;
|
||||
|
||||
constructor(config: Config, _logger: Logger, reader: UrlReader) {
|
||||
this.reader = reader;
|
||||
this.scmIntegrations = ScmIntegrations.fromConfig(config);
|
||||
}
|
||||
|
||||
private async resolveManagedByLocationToDir(
|
||||
async prepare(
|
||||
entity: Entity,
|
||||
options?: { etag?: string },
|
||||
options?: { logger?: Logger; etag?: string },
|
||||
): Promise<PreparerResponse> {
|
||||
const { type, target } = parseReferenceAnnotation(
|
||||
'backstage.io/managed-by-location',
|
||||
entity,
|
||||
);
|
||||
const { type, target } = transformDirLocation(entity, this.scmIntegrations);
|
||||
|
||||
this.logger.debug(
|
||||
`Building docs for entity with type 'dir' and managed-by-location '${type}'`,
|
||||
);
|
||||
switch (type) {
|
||||
case 'url': {
|
||||
options?.logger?.info(`Download documentation 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();
|
||||
|
||||
return {
|
||||
preparedDir,
|
||||
preparedDir: await response.dir(),
|
||||
etag: response.etag,
|
||||
};
|
||||
}
|
||||
case 'github':
|
||||
case 'gitlab':
|
||||
case 'azure/api': {
|
||||
const parsedGitLocation = parseGitUrl(target);
|
||||
const repoLocation = await checkoutGitRepository(
|
||||
target,
|
||||
this.config,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
// Check if etag has changed for cache invalidation.
|
||||
const etag = await getLastCommitTimestamp(repoLocation, this.logger);
|
||||
if (options?.etag === etag.toString()) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
case 'dir': {
|
||||
return {
|
||||
preparedDir: path.dirname(
|
||||
path.join(repoLocation, parsedGitLocation.filepath),
|
||||
),
|
||||
etag: etag.toString(),
|
||||
};
|
||||
}
|
||||
case 'file':
|
||||
return {
|
||||
preparedDir: path.dirname(target),
|
||||
// the transformation already validated that the target is in a safe location
|
||||
preparedDir: target,
|
||||
// Instead of supporting caching on local sources, use techdocs-cli for local development and debugging.
|
||||
etag: '',
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InputError(`Unable to resolve location type ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async prepare(entity: Entity): Promise<PreparerResponse> {
|
||||
this.logger.warn(
|
||||
'You are using the legacy dir preparer in TechDocs which will be removed in near future (March 2021). ' +
|
||||
'Migrate to URL reader by updating `backstage.io/techdocs-ref` annotation in `catalog-info.yaml` ' +
|
||||
'to be prefixed with `url:`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 ',
|
||||
);
|
||||
|
||||
const { target } = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
|
||||
// This will throw NotModified error if etag has not changed.
|
||||
const response = await this.resolveManagedByLocationToDir(entity);
|
||||
|
||||
return {
|
||||
preparedDir: path.resolve(response.preparedDir, target),
|
||||
etag: response.etag,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, parseLocationReference } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Logger } from 'winston';
|
||||
import { parseReferenceAnnotation } from '../../helpers';
|
||||
import { DirectoryPreparer } from './dir';
|
||||
import { getDirLocation } from '../../helpers';
|
||||
import { CommonGitPreparer } from './commonGit';
|
||||
import { UrlPreparer } from './url';
|
||||
import { DirectoryPreparer } from './dir';
|
||||
import { PreparerBase, PreparerBuilder, RemoteProtocol } from './types';
|
||||
import { UrlPreparer } from './url';
|
||||
|
||||
type factoryOptions = {
|
||||
logger: Logger;
|
||||
@@ -61,11 +63,21 @@ export class Preparers implements PreparerBuilder {
|
||||
}
|
||||
|
||||
get(entity: Entity): PreparerBase {
|
||||
const { type } = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
const preparer = this.preparerMap.get(type);
|
||||
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 preparer = this.preparerMap.get(type as RemoteProtocol);
|
||||
|
||||
if (!preparer) {
|
||||
throw new Error(`No preparer registered for type: "${type}"`);
|
||||
|
||||
Reference in New Issue
Block a user