Mob/techdocs end to end (#1736)
* Initial techdocs end to end heavily inspired by scaffolder * added some tests and fixed linting * fix(techdocs-backend): fix eslint * fix(techdocs-backend): cleanup commented test * Removed unused dependency * fix(techdocs-backend): updated standalone server * moved type dependency to devDependencies * fix: updated dependencies and devDependencies. * Update plugins/techdocs-backend/src/techdocs/stages/generate/types.ts Co-authored-by: Emma Indal <emma.indahl@gmail.com> Co-authored-by: Emma Indal <emmai@spotify.com> Co-authored-by: ellinors <ellinors@spotify.com> Co-authored-by: Emma Indal <emma.indahl@gmail.com>
This commit is contained in:
committed by
GitHub
parent
9d6a1c350a
commit
97f3f58844
@@ -0,0 +1,2 @@
|
||||
static
|
||||
!src/techdocs/stages/build
|
||||
@@ -22,14 +22,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.16",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.16",
|
||||
"@backstage/config": "^0.1.1-alpha.16",
|
||||
"@types/dockerode": "^2.5.34",
|
||||
"@types/express": "^4.17.6",
|
||||
"dockerode": "^3.2.1",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.1",
|
||||
"knex": "^0.21.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.16",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
export * from './techdocs';
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { Preparers, LocalPublish, Generators } from '../techdocs';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import Docker from 'dockerode';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { createRouter } from './router';
|
||||
@@ -24,7 +27,12 @@ describe('createRouter', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
preparers: new Preparers(),
|
||||
generators: new Generators(),
|
||||
publisher: new LocalPublish(),
|
||||
logger: getVoidLogger(),
|
||||
dockerClient: new Docker(),
|
||||
config: ConfigReader.fromConfigs([]),
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
@@ -17,18 +17,76 @@ import { Logger } from 'winston';
|
||||
import Router from 'express-promise-router';
|
||||
import express from 'express';
|
||||
import Knex from 'knex';
|
||||
import fetch from 'node-fetch';
|
||||
import { Config } from '@backstage/config';
|
||||
import path from 'path';
|
||||
import Docker from 'dockerode';
|
||||
import {
|
||||
GeneratorBuilder,
|
||||
PreparerBuilder,
|
||||
PublisherBase,
|
||||
LocalPublish,
|
||||
} from '../techdocs';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
type RouterOptions = {
|
||||
preparers: PreparerBuilder;
|
||||
generators: GeneratorBuilder;
|
||||
publisher: PublisherBase;
|
||||
logger: Logger;
|
||||
database?: Knex; // TODO: Make database required when we're implementing database stuff.
|
||||
config: Config;
|
||||
dockerClient: Docker;
|
||||
};
|
||||
|
||||
export async function createRouter({}: RouterOptions): Promise<express.Router> {
|
||||
export async function createRouter({
|
||||
preparers,
|
||||
generators,
|
||||
publisher,
|
||||
config,
|
||||
dockerClient,
|
||||
}: RouterOptions): Promise<express.Router> {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (_, res) => {
|
||||
router.get('/', async (_, res) => {
|
||||
res.status(200).send('Hello TechDocs Backend');
|
||||
});
|
||||
|
||||
// TODO: This route should not exist in the future
|
||||
router.get('/buildall', async (_, res) => {
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const entitiesResponse = (await (
|
||||
await fetch(`${baseUrl}/catalog/entities`)
|
||||
).json()) as Entity[];
|
||||
|
||||
const entitiesWithDocs = entitiesResponse.filter(
|
||||
entity => entity.metadata.annotations?.['backstage.io/techdocs-ref'],
|
||||
);
|
||||
|
||||
entitiesWithDocs.forEach(async entity => {
|
||||
const preparer = preparers.get(entity);
|
||||
const generator = generators.get(entity);
|
||||
|
||||
const { resultDir } = await generator.run({
|
||||
directory: await preparer.prepare(entity),
|
||||
dockerClient,
|
||||
});
|
||||
|
||||
publisher.publish({
|
||||
entity,
|
||||
directory: resultDir,
|
||||
});
|
||||
});
|
||||
|
||||
res.send('Successfully generated documentation');
|
||||
});
|
||||
|
||||
if (publisher instanceof LocalPublish) {
|
||||
router.use(
|
||||
'/static/docs/',
|
||||
express.static(path.resolve(__dirname, `../../static/docs`)),
|
||||
);
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ import { createServiceBuilder } from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
import Docker from 'dockerode';
|
||||
import {
|
||||
Preparers,
|
||||
DirectoryPreparer,
|
||||
Generators,
|
||||
TechdocsGenerator,
|
||||
LocalPublish,
|
||||
} from '../techdocs';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
@@ -31,9 +40,27 @@ export async function startStandaloneServer(
|
||||
const logger = options.logger.child({ service: 'techdocs-backend' });
|
||||
|
||||
logger.debug('Creating application...');
|
||||
const preparers = new Preparers();
|
||||
const directoryPreparer = new DirectoryPreparer();
|
||||
preparers.register('dir', directoryPreparer);
|
||||
|
||||
const generators = new Generators();
|
||||
const techdocsGenerator = new TechdocsGenerator();
|
||||
generators.register('techdocs', techdocsGenerator);
|
||||
|
||||
const publisher = new LocalPublish();
|
||||
|
||||
const dockerClient = new Docker();
|
||||
|
||||
logger.debug('Starting application server...');
|
||||
const router = await createRouter({ logger });
|
||||
const router = await createRouter({
|
||||
preparers,
|
||||
generators,
|
||||
logger,
|
||||
publisher,
|
||||
dockerClient,
|
||||
config: ConfigReader.fromConfigs([]),
|
||||
});
|
||||
const service = createServiceBuilder(module)
|
||||
.enableCors({ origin: 'http://localhost:3000' })
|
||||
.addRouter('/techdocs', router);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './stages';
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
GeneratorBase,
|
||||
SupportedGeneratorKey,
|
||||
GeneratorBuilder,
|
||||
} from './types';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { getGeneratorKey } from './helpers';
|
||||
|
||||
export class Generators implements GeneratorBuilder {
|
||||
private generatorMap = new Map<SupportedGeneratorKey, GeneratorBase>();
|
||||
|
||||
register(templaterKey: SupportedGeneratorKey, templater: GeneratorBase) {
|
||||
this.generatorMap.set(templaterKey, templater);
|
||||
}
|
||||
|
||||
get(entity: Entity): GeneratorBase {
|
||||
const generatorKey = getGeneratorKey(entity);
|
||||
const generator = this.generatorMap.get(generatorKey);
|
||||
|
||||
if (!generator) {
|
||||
throw new Error(
|
||||
`No generator registered for entity: "${generatorKey}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return generator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Writable, PassThrough } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
import { SupportedGeneratorKey } from './types';
|
||||
|
||||
// TODO: Implement proper support for more generators.
|
||||
export function getGeneratorKey(entity: Entity): SupportedGeneratorKey {
|
||||
if (!entity) {
|
||||
throw new Error('No entity provided');
|
||||
}
|
||||
|
||||
return 'techdocs';
|
||||
}
|
||||
|
||||
type RunDockerContainerOptions = {
|
||||
imageName: string;
|
||||
args: string[];
|
||||
logStream?: Writable;
|
||||
docsDir: string;
|
||||
resultDir: string;
|
||||
dockerClient: Docker;
|
||||
createOptions?: Docker.ContainerCreateOptions;
|
||||
};
|
||||
|
||||
export async function runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
logStream = new PassThrough(),
|
||||
docsDir,
|
||||
resultDir,
|
||||
dockerClient,
|
||||
createOptions,
|
||||
}: RunDockerContainerOptions) {
|
||||
const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run(
|
||||
imageName,
|
||||
args,
|
||||
logStream,
|
||||
{
|
||||
Volumes: {
|
||||
'/content': {},
|
||||
'/result': {},
|
||||
},
|
||||
WorkingDir: '/content',
|
||||
HostConfig: {
|
||||
Binds: [`${docsDir}:/content`, `${resultDir}:/result`],
|
||||
},
|
||||
...createOptions,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(
|
||||
`Docker failed to run with the following error message: ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (statusCode !== 0) {
|
||||
throw new Error(
|
||||
`Docker container returned a non-zero exit code (${statusCode})`,
|
||||
);
|
||||
}
|
||||
|
||||
return { error, statusCode };
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { TechdocsGenerator } from './techdocs';
|
||||
export { Generators } from './generators';
|
||||
export type { GeneratorBuilder } from './types';
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
GeneratorBase,
|
||||
GeneratorRunOptions,
|
||||
GeneratorRunResult,
|
||||
} from './types';
|
||||
import { runDockerContainer } from './helpers';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
export class TechdocsGenerator implements GeneratorBase {
|
||||
public async run({
|
||||
directory,
|
||||
logStream,
|
||||
dockerClient,
|
||||
}: GeneratorRunOptions): Promise<GeneratorRunResult> {
|
||||
const resultDir = fs.mkdtempSync(path.join(os.tmpdir(), `techdocs-tmp-`));
|
||||
|
||||
await runDockerContainer({
|
||||
imageName: 'spotify/techdocs',
|
||||
args: ['build', '-d', '/result'],
|
||||
logStream,
|
||||
docsDir: directory,
|
||||
resultDir,
|
||||
dockerClient,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`,
|
||||
);
|
||||
return { resultDir };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { Writable } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* The returned directory from the generator which is ready
|
||||
* to pass to the next stage of the TechDocs which is publishing
|
||||
*/
|
||||
export type GeneratorRunResult = {
|
||||
resultDir: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The values that the generator will recieve. The directory of the
|
||||
* uncompiled documentation, with the values from the frontend. A dedicated log stream and a docker
|
||||
* client to run any generator on top of your directory.
|
||||
*/
|
||||
export type GeneratorRunOptions = {
|
||||
directory: string;
|
||||
logStream?: Writable;
|
||||
dockerClient: Docker;
|
||||
};
|
||||
|
||||
export type GeneratorBase = {
|
||||
// runs the generator with the values and returns the directory to be published
|
||||
run(opts: GeneratorRunOptions): Promise<GeneratorRunResult>;
|
||||
};
|
||||
|
||||
/**
|
||||
* List of supported generator options
|
||||
*/
|
||||
export type SupportedGeneratorKey = 'techdocs' | string;
|
||||
|
||||
/**
|
||||
* The generator builder holds the generator ready for run time
|
||||
*/
|
||||
export type GeneratorBuilder = {
|
||||
register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void;
|
||||
get(entity: Entity): GeneratorBase;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './generate';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { DirectoryPreparer } from './dir';
|
||||
|
||||
const createMockEntity = (annotations: {}) => {
|
||||
return {
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
annotations: {
|
||||
...annotations,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('directory preparer', () => {
|
||||
it('should merge managed-by-location and techdocs-ref when techdocs-ref is relative', async () => {
|
||||
const directoryPreparer = new DirectoryPreparer();
|
||||
|
||||
const mockEntity = createMockEntity({
|
||||
'backstage.io/managed-by-location':
|
||||
'file:/directory/documented-component.yaml',
|
||||
'backstage.io/techdocs-ref': 'dir:./our-documentation',
|
||||
});
|
||||
|
||||
expect(await directoryPreparer.prepare(mockEntity)).toEqual(
|
||||
'/directory/our-documentation',
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge managed-by-location and techdocs-ref when techdocs-ref is absolute', async () => {
|
||||
const directoryPreparer = new DirectoryPreparer();
|
||||
|
||||
const mockEntity = createMockEntity({
|
||||
'backstage.io/managed-by-location':
|
||||
'file:/directory/documented-component.yaml',
|
||||
'backstage.io/techdocs-ref': 'dir:/our-documentation/techdocs',
|
||||
});
|
||||
|
||||
expect(await directoryPreparer.prepare(mockEntity)).toEqual(
|
||||
'/our-documentation/techdocs',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PreparerBase } from './types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import path from 'path';
|
||||
import { parseReferenceAnnotation } from './helpers';
|
||||
|
||||
export class DirectoryPreparer implements PreparerBase {
|
||||
prepare(entity: Entity): Promise<string> {
|
||||
const { location: managedByLocation } = parseReferenceAnnotation(
|
||||
'backstage.io/managed-by-location',
|
||||
entity,
|
||||
);
|
||||
const { location: techdocsLocation } = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
|
||||
const managedByLocationDirectory = path.dirname(managedByLocation);
|
||||
|
||||
return new Promise(resolve => {
|
||||
resolve(path.resolve(managedByLocationDirectory, techdocsLocation));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { RemoteProtocol } from './types';
|
||||
|
||||
export type ParsedLocationAnnotation = {
|
||||
protocol: RemoteProtocol;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export const parseReferenceAnnotation = (
|
||||
annotationName: string,
|
||||
entity: Entity,
|
||||
): ParsedLocationAnnotation => {
|
||||
const annotation = entity.metadata.annotations?.[annotationName];
|
||||
|
||||
if (!annotation) {
|
||||
throw new InputError(
|
||||
`No location annotation provided in entity: ${entity.metadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
// split on the first colon for the protocol and the rest after the first split
|
||||
// is the location.
|
||||
const [protocol, location] = annotation.split(/:(.+)/) as [
|
||||
RemoteProtocol?,
|
||||
string?,
|
||||
];
|
||||
|
||||
if (!protocol || !location) {
|
||||
throw new InputError(
|
||||
`Failure to parse either protocol or location for entity: ${entity.metadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
protocol,
|
||||
location,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { DirectoryPreparer } from './dir';
|
||||
export { Preparers } from './preparers';
|
||||
export type { PreparerBuilder } from './types';
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PreparerBase, RemoteProtocol, PreparerBuilder } from './types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { parseReferenceAnnotation } from './helpers';
|
||||
|
||||
export class Preparers implements PreparerBuilder {
|
||||
private preparerMap = new Map<RemoteProtocol, PreparerBase>();
|
||||
|
||||
register(protocol: RemoteProtocol, preparer: PreparerBase) {
|
||||
this.preparerMap.set(protocol, preparer);
|
||||
}
|
||||
|
||||
get(entity: Entity): PreparerBase {
|
||||
const { protocol } = parseReferenceAnnotation('backstage.io/techdocs-ref', entity);
|
||||
const preparer = this.preparerMap.get(protocol);
|
||||
|
||||
if (!preparer) {
|
||||
throw new Error(`No preparer registered for type: "${protocol}"`);
|
||||
}
|
||||
|
||||
return preparer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export type PreparerBase = {
|
||||
/**
|
||||
* Given an Entity definition from the Service Catalog, go and prepare a directory
|
||||
* with contents from the location in temporary storage and return the path
|
||||
* @param entity The entity from the Service Catalog
|
||||
*/
|
||||
prepare(entity: Entity, opts?: { logger: Logger }): Promise<string>;
|
||||
};
|
||||
|
||||
export type PreparerBuilder = {
|
||||
register(protocol: RemoteProtocol, preparer: PreparerBase): void;
|
||||
get(entity: Entity): PreparerBase;
|
||||
};
|
||||
|
||||
export type RemoteProtocol = 'dir' | string;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { LocalPublish } from './local';
|
||||
export type { PublisherBase } from './types';
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { LocalPublish } from './local';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const createMockEntity = (annotations = {}) => {
|
||||
return {
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'test-component-name',
|
||||
annotations: {
|
||||
...annotations,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('local publisher', () => {
|
||||
it('should publish generated documentation dir', async () => {
|
||||
const publisher = new LocalPublish();
|
||||
|
||||
const mockEntity = createMockEntity();
|
||||
|
||||
const tempDir = fs.mkdtempSync(`${__dirname}/test-component-folder-`);
|
||||
|
||||
expect(tempDir).toBeTruthy();
|
||||
|
||||
fs.closeSync(fs.openSync(path.join(tempDir, '/mock-file'), 'w'));
|
||||
|
||||
await publisher.publish({ entity: mockEntity, directory: tempDir });
|
||||
const publishDir = path.resolve(
|
||||
__dirname,
|
||||
`../../../../static/docs/${mockEntity.metadata.name}`,
|
||||
);
|
||||
|
||||
expect(fs.existsSync(publishDir)).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(publishDir, '/mock-file'))).toBeTruthy();
|
||||
|
||||
fs.removeSync(publishDir);
|
||||
fs.removeSync(tempDir);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PublisherBase } from './types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export class LocalPublish implements PublisherBase {
|
||||
publish({
|
||||
entity,
|
||||
directory,
|
||||
}: {
|
||||
entity: Entity;
|
||||
directory: string;
|
||||
}):
|
||||
| Promise<{
|
||||
remoteUrl: string;
|
||||
}>
|
||||
| { remoteUrl: string } {
|
||||
const publishDir = path.resolve(
|
||||
__dirname,
|
||||
`../../../../static/docs/${entity.metadata.name}`,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(publishDir)) {
|
||||
fs.mkdirSync(publishDir, { recursive: true });
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.copy(directory, publishDir, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve({
|
||||
remoteUrl: `http://localhost:7000/techdocs/static/docs/${entity.metadata.name}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* Publisher is in charge of taking a folder created by
|
||||
* the builder, and pushing it to storage
|
||||
*/
|
||||
export type PublisherBase = {
|
||||
/**
|
||||
*
|
||||
* @param opts object containing the entity from the service
|
||||
* catalog, and the directory that has been generated
|
||||
*/
|
||||
publish(opts: {
|
||||
entity: Entity;
|
||||
directory: string;
|
||||
}): Promise<{ remoteUrl: string }> | { remoteUrl: string };
|
||||
};
|
||||
Reference in New Issue
Block a user