diff --git a/app-config.yaml b/app-config.yaml index 5f75ff522e..f578fb3fca 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -21,7 +21,7 @@ organization: name: Spotify techdocs: - storageUrl: https://techdocs-mock-sites.storage.googleapis.com + storageUrl: http://localhost:7000/techdocs/static/docs sentry: organization: spotify diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index 0d03bfabab..812a56cd9f 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -14,9 +14,37 @@ * limitations under the License. */ -import { createRouter } from '@backstage/plugin-techdocs-backend'; +import { + createRouter, + DirectoryPreparer, + Preparers, + Generators, + LocalPublish, + TechdocsGenerator +} from '@backstage/plugin-techdocs-backend'; import { PluginEnvironment } from '../types'; +import Docker from 'dockerode'; -export default async function createPlugin({ logger }: PluginEnvironment) { - return await createRouter({ logger }); +export default async function createPlugin({ logger, config }: PluginEnvironment) { + const generators = new Generators(); + const techdocsGenerator = new TechdocsGenerator(); + generators.register('techdocs', techdocsGenerator); + + const directoryPreparer = new DirectoryPreparer(); + const preparers = new Preparers(); + + preparers.register('dir', directoryPreparer); + + const publisher = new LocalPublish(); + + const dockerClient = new Docker(); + + return await createRouter({ + preparers, + generators, + publisher, + dockerClient, + logger, + config, + }); } diff --git a/plugins/techdocs-backend/.gitignore b/plugins/techdocs-backend/.gitignore new file mode 100644 index 0000000000..650853097a --- /dev/null +++ b/plugins/techdocs-backend/.gitignore @@ -0,0 +1,2 @@ +static +!src/techdocs/stages/build \ No newline at end of file diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index edc8908cfc..e5c97f9d0d 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -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": [ diff --git a/plugins/techdocs-backend/src/index.ts b/plugins/techdocs-backend/src/index.ts index 7612c392a2..8c65708017 100644 --- a/plugins/techdocs-backend/src/index.ts +++ b/plugins/techdocs-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export * from './techdocs'; diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index f4dbb706fd..b28010e059 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -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); }); diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 7c91dd4ef7..874ab7c89c 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -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 { +export async function createRouter({ + preparers, + generators, + publisher, + config, + dockerClient, +}: RouterOptions): Promise { 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; } diff --git a/plugins/techdocs-backend/src/service/standaloneServer.ts b/plugins/techdocs-backend/src/service/standaloneServer.ts index 09acfc9e27..76de973875 100644 --- a/plugins/techdocs-backend/src/service/standaloneServer.ts +++ b/plugins/techdocs-backend/src/service/standaloneServer.ts @@ -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); diff --git a/plugins/techdocs-backend/src/techdocs/index.ts b/plugins/techdocs-backend/src/techdocs/index.ts new file mode 100644 index 0000000000..7113525bb8 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/index.ts @@ -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'; diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/generators.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/generators.ts new file mode 100644 index 0000000000..da9d3c418b --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/generators.ts @@ -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(); + + 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; + } + } + \ No newline at end of file diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts new file mode 100644 index 0000000000..7dc4403977 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts @@ -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 }; +} diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/index.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/index.ts new file mode 100644 index 0000000000..5c086a976a --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/index.ts @@ -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'; diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts new file mode 100644 index 0000000000..d6650fb744 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts @@ -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 { + 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 }; + } +} diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/types.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/types.ts new file mode 100644 index 0000000000..4292aa68f6 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/types.ts @@ -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; +}; + +/** + * 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; +}; diff --git a/plugins/techdocs-backend/src/techdocs/stages/index.ts b/plugins/techdocs-backend/src/techdocs/stages/index.ts new file mode 100644 index 0000000000..40988483a3 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/index.ts @@ -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'; diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts new file mode 100644 index 0000000000..8a99a8b59a --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts @@ -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', + ); + }); +}); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.ts new file mode 100644 index 0000000000..e61a3e48ac --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.ts @@ -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 { + 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)); + }); + } +} diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts new file mode 100644 index 0000000000..1fd86805ca --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/helpers.ts @@ -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, + }; +}; diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts new file mode 100644 index 0000000000..9f928e7413 --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts @@ -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'; diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/preparers.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/preparers.ts new file mode 100644 index 0000000000..1d5b689d8b --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/preparers.ts @@ -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(); + + 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; + } +} \ No newline at end of file diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/types.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/types.ts new file mode 100644 index 0000000000..8baf6347bb --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/types.ts @@ -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; +}; + +export type PreparerBuilder = { + register(protocol: RemoteProtocol, preparer: PreparerBase): void; + get(entity: Entity): PreparerBase; +}; + +export type RemoteProtocol = 'dir' | string; diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/index.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/index.ts new file mode 100644 index 0000000000..0029ea5c4e --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/index.ts @@ -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'; diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.test.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.test.ts new file mode 100644 index 0000000000..b5a96e4feb --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.test.ts @@ -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); + }); +}); diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts new file mode 100644 index 0000000000..227ba445ae --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts @@ -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}`, + }); + }); + }); + } +} diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/types.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/types.ts new file mode 100644 index 0000000000..ca85d9f56e --- /dev/null +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/types.ts @@ -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 }; +}; diff --git a/yarn.lock b/yarn.lock index 99db406126..cd152470fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4065,6 +4065,13 @@ dependencies: "@types/node" "*" +"@types/dockerode@^2.5.34": + version "2.5.34" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.34.tgz#9adb884f7cc6c012a6eb4b2ad794cc5d01439959" + integrity sha512-LcbLGcvcBwBAvjH9UrUI+4qotY+A5WCer5r43DR5XHv2ZIEByNXFdPLo1XxR+v/BjkGjlggW8qUiXuVEhqfkpA== + dependencies: + "@types/node" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -8590,6 +8597,14 @@ dockerode@^3.2.0: docker-modem "^2.1.0" tar-fs "~2.0.1" +dockerode@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/dockerode/-/dockerode-3.2.1.tgz#4a2222e3e1df536bf595e78e76d3cfbf6d4d93b9" + integrity sha512-XsSVB5Wu5HWMg1aelV5hFSqFJaKS5x1aiV/+sT7YOzOq1IRl49I/UwV8Pe4x6t0iF9kiGkWu5jwfvbkcFVupBw== + dependencies: + docker-modem "^2.1.0" + tar-fs "~2.0.1" + doctrine@1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"