Refactor the runDockerContainer function to a ContainerRunner interface.

Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
Dominik Henneke
2021-04-21 17:14:11 +02:00
parent 044f9930d1
commit e0bfd3d448
39 changed files with 652 additions and 347 deletions
+56
View File
@@ -0,0 +1,56 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Migrate the plugin to use the `ContainerRunner` interface instead of `runDockerContainer(…)`.
It also provides the `ContainerRunner` to the individual templaters instead of to the `createRouter` function.
To apply this change to an existing backend application, add the following to `src/plugins/scaffolder.ts`:
```diff
- import { SingleHostDiscovery } from '@backstage/backend-common';
+ import {
+ DockerContainerRunner,
+ SingleHostDiscovery,
+ } from '@backstage/backend-common';
export default async function createPlugin({
logger,
config,
database,
reader,
}: PluginEnvironment): Promise<Router> {
+ const dockerClient = new Docker();
+ const containerRunner = new DockerContainerRunner({ dockerClient });
+ const cookiecutterTemplater = new CookieCutter({ containerRunner });
- const cookiecutterTemplater = new CookieCutter();
+ const craTemplater = new CreateReactAppTemplater({ containerRunner });
- const craTemplater = new CreateReactAppTemplater();
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
- const dockerClient = new Docker();
const discovery = SingleHostDiscovery.fromConfig(config);
const catalogClient = new CatalogClient({ discoveryApi: discovery });
return await createRouter({
preparers,
templaters,
publishers,
logger,
config,
- dockerClient,
database,
catalogClient,
reader,
});
}
```
+58
View File
@@ -0,0 +1,58 @@
---
'@backstage/backend-common': minor
---
Refactor the `runDockerContainer(…)` function to an interface-based api.
This gives the option to replace the docker runtime in the future.
Packages and plugins that previously used the `dockerode` as argument should be migrated to use the new `ContainerRunner` interface instead.
```diff
import {
- runDockerContainer,
+ ContainerRunner,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
- import Docker from 'dockerode';
type RouterOptions = {
// ...
- dockerClient: Docker,
+ containerRunner: ContainerRunner;
};
export async function createRouter({
// ...
- dockerClient,
+ containerRunner,
}: RouterOptions): Promise<express.Router> {
// ...
+ await containerRunner.runContainer({
- await runDockerContainer({
image: 'docker',
// ...
- dockerClient,
});
// ...
}
```
To keep the `dockerode` based runtime, use the `DockerContainerRunner` implementation:
```diff
+ import {
+ ContainerRunner,
+ DockerContainerRunner
+ } from '@backstage/backend-common';
- import { runDockerContainer } from '@backstage/backend-common';
+ const containerRunner: ContainerRUnner = new DockerContainerRunner({dockerClient});
+ await containerRunner.runContainer({
- await runDockerContainer({
image: 'docker',
// ...
- dockerClient,
});
```
+61
View File
@@ -0,0 +1,61 @@
---
'@backstage/plugin-techdocs-backend': minor
---
Migrate the plugin to use the `ContainerRunner` interface instead of `runDockerContainer(…)`.
It also provides the `ContainerRunner` to the generators instead of to the `createRouter` function.
To apply this change to an existing backend application, add the following to `src/plugins/techdocs.ts`:
```diff
+ import { DockerContainerRunner } from '@backstage/backend-common';
// ...
export default async function createPlugin({
logger,
config,
discovery,
reader,
}: PluginEnvironment): Promise<Router> {
// Preparers are responsible for fetching source files for documentation.
const preparers = await Preparers.fromConfig(config, {
logger,
reader,
});
+ // Docker client (conditionally) used by the generators, based on techdocs.generators config.
+ const dockerClient = new Docker();
+ const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(config, {
logger,
+ containerRunner,
});
// Publisher is used for
// 1. Publishing generated files to storage
// 2. Fetching files from storage and passing them to TechDocs frontend.
const publisher = await Publisher.fromConfig(config, {
logger,
discovery,
});
// checks if the publisher is working and logs the result
await publisher.getReadiness();
- // Docker client (conditionally) used by the generators, based on techdocs.generators config.
- const dockerClient = new Docker();
return await createRouter({
preparers,
generators,
publisher,
- dockerClient,
logger,
config,
discovery,
});
}
```
+27
View File
@@ -0,0 +1,27 @@
---
'@backstage/techdocs-common': minor
---
Migrate the package to use the `ContainerRunner` interface instead of `runDockerContainer(…)`.
It also no longer provides the `ContainerRunner` as an input to the `GeneratorBase#run(…)` function, but expects it as a constructor parameter instead.
If you use the `TechdocsGenerator` you need to update the usage:
```diff
+ const containerRunner = new DockerContainerRunner({ dockerClient });
- const generator = new TechdocsGenerator(logger, config);
+ const techdocsGenerator = new TechdocsGenerator({
+ logger,
+ containerRunner,
+ config,
+ });
await this.generator.run({
inputDir: preparedDir,
outputDir,
- dockerClient: this.dockerClient,
parsedLocationAnnotation,
etag: newEtag,
});
```
+115
View File
@@ -0,0 +1,115 @@
---
'@backstage/create-app': patch
---
The `scaffolder-backend` and `techdocs-backend` plugins have been updated.
In order to update, you need to apply the following changes to your existing backend application:
`@backstage/plugin-techdocs-backend`:
```diff
// packages/backend/src/plugin/techdocs.ts
+ import { DockerContainerRunner } from '@backstage/backend-common';
// ...
export default async function createPlugin({
logger,
config,
discovery,
reader,
}: PluginEnvironment): Promise<Router> {
// Preparers are responsible for fetching source files for documentation.
const preparers = await Preparers.fromConfig(config, {
logger,
reader,
});
+ // Docker client (conditionally) used by the generators, based on techdocs.generators config.
+ const dockerClient = new Docker();
+ const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(config, {
logger,
+ containerRunner,
});
// Publisher is used for
// 1. Publishing generated files to storage
// 2. Fetching files from storage and passing them to TechDocs frontend.
const publisher = await Publisher.fromConfig(config, {
logger,
discovery,
});
// checks if the publisher is working and logs the result
await publisher.getReadiness();
- // Docker client (conditionally) used by the generators, based on techdocs.generators config.
- const dockerClient = new Docker();
return await createRouter({
preparers,
generators,
publisher,
- dockerClient,
logger,
config,
discovery,
});
}
```
`@backstage/plugin-scaffolder-backend`:
```diff
// packages/backend/src/plugin/scaffolder.ts
- import { SingleHostDiscovery } from '@backstage/backend-common';
+ import {
+ DockerContainerRunner,
+ SingleHostDiscovery,
+ } from '@backstage/backend-common';
export default async function createPlugin({
logger,
config,
database,
reader,
}: PluginEnvironment): Promise<Router> {
+ const dockerClient = new Docker();
+ const containerRunner = new DockerContainerRunner({ dockerClient });
+ const cookiecutterTemplater = new CookieCutter({ containerRunner });
- const cookiecutterTemplater = new CookieCutter();
+ const craTemplater = new CreateReactAppTemplater({ containerRunner });
- const craTemplater = new CreateReactAppTemplater();
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
- const dockerClient = new Docker();
const discovery = SingleHostDiscovery.fromConfig(config);
const catalogClient = new CatalogClient({ discoveryApi: discovery });
return await createRouter({
preparers,
templaters,
publishers,
logger,
config,
- dockerClient,
database,
catalogClient,
reader,
});
}
```
@@ -70,6 +70,10 @@ creating a file called `packages/backend/src/plugins/scaffolder.ts` with the
following contents to get you up and running quickly.
```ts
import {
DockerContainerRunner,
SingleHostDiscovery,
} from '@backstage/backend-common';
import {
CookieCutter,
createRouter,
@@ -78,7 +82,6 @@ import {
CreateReactAppTemplater,
Templaters,
} from '@backstage/plugin-scaffolder-backend';
import { SingleHostDiscovery } from '@backstage/backend-common';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
import { CatalogClient } from '@backstage/catalog-client';
@@ -89,8 +92,11 @@ export default async function createPlugin({
database,
reader,
}: PluginEnvironment) {
const cookiecutterTemplater = new CookieCutter();
const craTemplater = new CreateReactAppTemplater();
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
const cookiecutterTemplater = new CookieCutter({ containerRunner });
const craTemplater = new CreateReactAppTemplater({ containerRunner });
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
@@ -99,8 +105,6 @@ export default async function createPlugin({
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
const discovery = SingleHostDiscovery.fromConfig(config);
const catalogClient = new CatalogClient({ discoveryApi: discovery });
@@ -110,7 +114,6 @@ export default async function createPlugin({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
@@ -106,7 +106,6 @@ return await createRouter({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
@@ -124,7 +123,6 @@ return await createRouter({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
@@ -136,11 +134,9 @@ return await createRouter({
want to have those as well as your new one, you'll need to do the following:
```ts
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend`;
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend';
const builtInActions = createBuiltinActions({
dockerClient,
integrations,
catalogClient,
templaters,
@@ -155,7 +151,6 @@ return await createRouter({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
+8 -3
View File
@@ -63,6 +63,7 @@ Create a file called `techdocs.ts` inside `packages/backend/src/plugins/` and
add the following
```typescript
import { DockerContainerRunner } from '@backstage/backend-common';
import {
createRouter,
Generators,
@@ -84,9 +85,14 @@ export default async function createPlugin({
reader,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(config, {
logger,
containerRunner,
});
// Publisher is used for
@@ -97,14 +103,13 @@ export default async function createPlugin({
discovery,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
// checks if the publisher is working and logs the result
await publisher.getReadiness();
return await createRouter({
preparers,
generators,
publisher,
dockerClient,
logger,
config,
discovery,
@@ -0,0 +1,31 @@
/*
* 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 { Writable } from 'stream';
export type RunContainerOptions = {
imageName: string;
command?: string | string[];
args: string[];
logStream?: Writable;
mountDirs?: Record<string, string>;
workingDir?: string;
envVars?: Record<string, string>;
};
export interface ContainerRunner {
runContainer(opts: RunContainerOptions): Promise<void>;
}
@@ -13,17 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Docker from 'dockerode';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
import Stream, { PassThrough } from 'stream';
import { runDockerContainer, UserOptions } from './docker';
import { ContainerRunner } from './ContainerRunner';
import { DockerContainerRunner, UserOptions } from './DockerContainerRunner';
const mockDocker = new Docker() as jest.Mocked<Docker>;
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
describe('runDockerContainer', () => {
describe('DockerContainerRunner', () => {
let containerTaskApi: ContainerRunner;
beforeEach(() => {
mockFs({
[rootDir]: {
@@ -49,6 +53,8 @@ describe('runDockerContainer', () => {
jest
.spyOn(mockDocker, 'ping')
.mockResolvedValue(Buffer.from('OK', 'utf-8'));
containerTaskApi = new DockerContainerRunner({ dockerClient: mockDocker });
});
afterEach(() => {
@@ -66,10 +72,9 @@ describe('runDockerContainer', () => {
const envVarsArray = ['HOME=/tmp', 'LOG_LEVEL=debug'];
it('should pull the docker container', async () => {
await runDockerContainer({
await containerTaskApi.runContainer({
imageName,
args,
dockerClient: mockDocker,
});
expect(mockDocker.pull).toHaveBeenCalledWith(
@@ -82,13 +87,12 @@ describe('runDockerContainer', () => {
});
it('should call the dockerClient run command with the correct arguments passed through', async () => {
await runDockerContainer({
await containerTaskApi.runContainer({
imageName,
args,
mountDirs,
envVars,
workingDir,
dockerClient: mockDocker,
});
expect(mockDocker.run).toHaveBeenCalledWith(
@@ -113,20 +117,18 @@ describe('runDockerContainer', () => {
});
it('should ping docker to test availability', async () => {
await runDockerContainer({
await containerTaskApi.runContainer({
imageName,
args,
dockerClient: mockDocker,
});
expect(mockDocker.ping).toHaveBeenCalled();
});
it('should pass through the user and group id from the host machine and set the home dir', async () => {
await runDockerContainer({
await containerTaskApi.runContainer({
imageName,
args,
dockerClient: mockDocker,
});
const userOptions: UserOptions = {};
@@ -153,10 +155,9 @@ describe('runDockerContainer', () => {
]);
await expect(
runDockerContainer({
containerTaskApi.runContainer({
imageName,
args,
dockerClient: mockDocker,
}),
).rejects.toThrow(/Something went wrong with docker/);
});
@@ -172,10 +173,9 @@ describe('runDockerContainer', () => {
it('should throw with a descriptive error message including the docker error message', async () => {
await expect(
runDockerContainer({
containerTaskApi.runContainer({
imageName,
args,
dockerClient: mockDocker,
}),
).rejects.toThrow(new RegExp(`.+: ${dockerError}`));
});
@@ -183,11 +183,10 @@ describe('runDockerContainer', () => {
it('should pass through the log stream to the docker client', async () => {
const logStream = new PassThrough();
await runDockerContainer({
await containerTaskApi.runContainer({
imageName,
args,
logStream,
dockerClient: mockDocker,
});
expect(mockDocker.run).toHaveBeenCalledWith(
@@ -0,0 +1,117 @@
/*
* 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 Docker from 'dockerode';
import fs from 'fs-extra';
import { PassThrough } from 'stream';
import { ContainerRunner, RunContainerOptions } from './ContainerRunner';
export type UserOptions = {
User?: string;
};
export class DockerContainerRunner implements ContainerRunner {
private readonly dockerClient: Docker;
constructor({ dockerClient }: { dockerClient: Docker }) {
this.dockerClient = dockerClient;
}
async runContainer({
imageName,
command,
args,
logStream = new PassThrough(),
mountDirs = {},
workingDir,
envVars = {},
}: RunContainerOptions) {
// Show a better error message when Docker is unavailable.
try {
await this.dockerClient.ping();
} catch (e) {
throw new Error(
`This operation requires Docker. Docker does not appear to be available. Docker.ping() failed with: ${e.message}`,
);
}
await new Promise<void>((resolve, reject) => {
this.dockerClient.pull(imageName, {}, (err, stream) => {
if (err) return reject(err);
stream.pipe(logStream, { end: false });
stream.on('end', () => resolve());
stream.on('error', (error: Error) => reject(error));
return undefined;
});
});
const userOptions: UserOptions = {};
if (process.getuid && process.getgid) {
// Files that are created inside the Docker container will be owned by
// root on the host system on non Mac systems, because of reasons. Mainly the fact that
// volume sharing is done using NFS on Mac and actual mounts in Linux world.
// So we set the user in the container as the same user and group id as the host.
// On Windows we don't have process.getuid nor process.getgid
userOptions.User = `${process.getuid()}:${process.getgid()}`;
}
// Initialize volumes to mount based on mountDirs map
const Volumes: { [T: string]: object } = {};
for (const containerDir of Object.values(mountDirs)) {
Volumes[containerDir] = {};
}
// Create bind volumes
const Binds: string[] = [];
for (const [hostDir, containerDir] of Object.entries(mountDirs)) {
// Need to use realpath here as Docker mounting does not like
// symlinks for binding volumes
const realHostDir = await fs.realpath(hostDir);
Binds.push(`${realHostDir}:${containerDir}`);
}
// Create docker environment variables array
const Env = [];
for (const [key, value] of Object.entries(envVars)) {
Env.push(`${key}=${value}`);
}
const [
{ Error: error, StatusCode: statusCode },
] = await this.dockerClient.run(imageName, args, logStream, {
Volumes,
HostConfig: {
Binds,
},
...(workingDir ? { WorkingDir: workingDir } : {}),
Entrypoint: command,
Env,
...userOptions,
} as Docker.ContainerCreateOptions);
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})`,
);
}
}
}
-137
View File
@@ -1,137 +0,0 @@
/*
* 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 Docker from 'dockerode';
import fs from 'fs-extra';
import { PassThrough, Writable } from 'stream';
export type UserOptions = {
User?: string;
};
export type RunDockerContainerOptions = {
imageName: string;
args: string[];
logStream?: Writable;
dockerClient: Docker;
mountDirs?: Record<string, string>;
workingDir?: string;
envVars?: Record<string, string>;
createOptions?: Docker.ContainerCreateOptions;
};
/**
*
* @param options the options object
* @param options.imageName the image to run
* @param options.args the arguments to pass the container
* @param options.logStream the log streamer to capture log messages
* @param options.dockerClient the dockerClient to use
* @param options.mountDirs A map of host directories to mount on the container.
* Object Key: Path on host machine, Value: Path on Docker container
* @param options.workingDir Working dir in the container
* @param options.envVars Environment variables to set in the container. e.g. {'HOME': '/tmp'}
*/
export const runDockerContainer = async ({
imageName,
args,
logStream = new PassThrough(),
dockerClient,
mountDirs = {},
workingDir,
envVars = {},
createOptions = {},
}: RunDockerContainerOptions) => {
// Show a better error message when Docker is unavailable.
try {
await dockerClient.ping();
} catch (e) {
throw new Error(
`This operation requires Docker. Docker does not appear to be available. Docker.ping() failed with: ${e.message}`,
);
}
await new Promise<void>((resolve, reject) => {
dockerClient.pull(imageName, {}, (err, stream) => {
if (err) return reject(err);
stream.pipe(logStream, { end: false });
stream.on('end', () => resolve());
stream.on('error', (error: Error) => reject(error));
return undefined;
});
});
const userOptions: UserOptions = {};
if (process.getuid && process.getgid) {
// Files that are created inside the Docker container will be owned by
// root on the host system on non Mac systems, because of reasons. Mainly the fact that
// volume sharing is done using NFS on Mac and actual mounts in Linux world.
// So we set the user in the container as the same user and group id as the host.
// On Windows we don't have process.getuid nor process.getgid
userOptions.User = `${process.getuid()}:${process.getgid()}`;
}
// Initialize volumes to mount based on mountDirs map
const Volumes: { [T: string]: object } = {};
for (const containerDir of Object.values(mountDirs)) {
Volumes[containerDir] = {};
}
// Create bind volumes
const Binds: string[] = [];
for (const [hostDir, containerDir] of Object.entries(mountDirs)) {
// Need to use realpath here as Docker mounting does not like
// symlinks for binding volumes
const realHostDir = await fs.realpath(hostDir);
Binds.push(`${realHostDir}:${containerDir}`);
}
// Create docker environment variables array
const Env = [];
for (const [key, value] of Object.entries(envVars)) {
Env.push(`${key}=${value}`);
}
const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run(
imageName,
args,
logStream,
{
Volumes,
HostConfig: {
Binds,
},
...(workingDir ? { WorkingDir: workingDir } : {}),
Env,
...userOptions,
...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 };
};
+2 -1
View File
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export { runDockerContainer } from './docker';
export type { ContainerRunner, RunContainerOptions } from './ContainerRunner';
export { DockerContainerRunner } from './DockerContainerRunner';
+9 -6
View File
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { SingleHostDiscovery } from '@backstage/backend-common';
import {
DockerContainerRunner,
SingleHostDiscovery,
} from '@backstage/backend-common';
import { CatalogClient } from '@backstage/catalog-client';
import {
CookieCutter,
@@ -34,8 +37,11 @@ export default async function createPlugin({
database,
reader,
}: PluginEnvironment): Promise<Router> {
const cookiecutterTemplater = new CookieCutter();
const craTemplater = new CreateReactAppTemplater();
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
const cookiecutterTemplater = new CookieCutter({ containerRunner });
const craTemplater = new CreateReactAppTemplater({ containerRunner });
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
@@ -44,8 +50,6 @@ export default async function createPlugin({
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
const discovery = SingleHostDiscovery.fromConfig(config);
const catalogClient = new CatalogClient({ discoveryApi: discovery });
@@ -55,7 +59,6 @@ export default async function createPlugin({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
+6 -4
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DockerContainerRunner } from '@backstage/backend-common';
import {
createRouter,
Generators,
@@ -35,9 +36,14 @@ export default async function createPlugin({
reader,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(config, {
logger,
containerRunner,
});
// Publisher is used for
@@ -51,14 +57,10 @@ export default async function createPlugin({
// checks if the publisher is working and logs the result
await publisher.getReadiness();
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
return await createRouter({
preparers,
generators,
publisher,
dockerClient,
logger,
config,
discovery,
@@ -1,4 +1,7 @@
import { SingleHostDiscovery } from '@backstage/backend-common';
import {
DockerContainerRunner,
SingleHostDiscovery,
} from '@backstage/backend-common';
import { CatalogClient } from '@backstage/catalog-client';
import {
CookieCutter,
@@ -6,7 +9,7 @@ import {
createRouter,
Preparers,
Publishers,
Templaters
Templaters,
} from '@backstage/plugin-scaffolder-backend';
import Docker from 'dockerode';
import { Router } from 'express';
@@ -18,8 +21,11 @@ export default async function createPlugin({
database,
reader,
}: PluginEnvironment): Promise<Router> {
const cookiecutterTemplater = new CookieCutter();
const craTemplater = new CreateReactAppTemplater();
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
const cookiecutterTemplater = new CookieCutter({ containerRunner });
const craTemplater = new CreateReactAppTemplater({ containerRunner });
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
@@ -28,8 +34,6 @@ export default async function createPlugin({
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
const discovery = SingleHostDiscovery.fromConfig(config);
const catalogClient = new CatalogClient({ discoveryApi: discovery });
@@ -39,9 +43,8 @@ export default async function createPlugin({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader
reader,
});
}
@@ -1,9 +1,9 @@
import { DockerContainerRunner } from '@backstage/backend-common';
import {
createRouter,
Generators, Preparers,
Publisher
Generators,
Preparers,
Publisher,
} from '@backstage/plugin-techdocs-backend';
import Docker from 'dockerode';
import { Router } from 'express';
@@ -21,9 +21,14 @@ export default async function createPlugin({
reader,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(config, {
logger,
containerRunner,
});
// Publisher is used for
@@ -37,14 +42,10 @@ export default async function createPlugin({
// checks if the publisher is working and logs the result
await publisher.getReadiness();
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
return await createRouter({
preparers,
generators,
publisher,
dockerClient,
logger,
config,
discovery,
-2
View File
@@ -44,11 +44,9 @@
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@google-cloud/storage": "^5.6.0",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"aws-sdk": "^2.840.0",
"cross-fetch": "^3.0.6",
"dockerode": "^3.2.1",
"express": "^4.17.1",
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.4",
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { ContainerRunner, getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { Generators } from './generators';
import { TechdocsGenerator } from './techdocs';
@@ -30,6 +30,10 @@ const mockEntity = {
};
describe('generators', () => {
const containerRunner: jest.Mocked<ContainerRunner> = {
runContainer: jest.fn(),
};
it('should return error if no generator is registered', async () => {
const generators = new Generators();
@@ -40,7 +44,11 @@ describe('generators', () => {
it('should return correct registered generator', async () => {
const generators = new Generators();
const techdocs = new TechdocsGenerator(logger, new ConfigReader({}));
const techdocs = new TechdocsGenerator({
logger,
containerRunner,
config: new ConfigReader({}),
});
generators.register('techdocs', techdocs);
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ContainerRunner } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { Logger } from 'winston';
@@ -30,11 +31,18 @@ export class Generators implements GeneratorBuilder {
static async fromConfig(
config: Config,
{ logger }: { logger: Logger },
{
logger,
containerRunner,
}: { logger: Logger; containerRunner: ContainerRunner },
): Promise<GeneratorBuilder> {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger, config);
const techdocsGenerator = new TechdocsGenerator({
logger,
containerRunner,
config,
});
generators.register('techdocs', techdocsGenerator);
return generators;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { runDockerContainer } from '@backstage/backend-common';
import { ContainerRunner } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import path from 'path';
import { PassThrough } from 'stream';
@@ -48,20 +48,29 @@ const createStream = (): [string[], PassThrough] => {
export class TechdocsGenerator implements GeneratorBase {
private readonly logger: Logger;
private readonly containerRunner: ContainerRunner;
private readonly options: TechdocsGeneratorOptions;
constructor(logger: Logger, config: Config) {
constructor({
logger,
containerRunner,
config,
}: {
logger: Logger;
containerRunner: ContainerRunner;
config: Config;
}) {
this.logger = logger;
this.options = {
runGeneratorIn:
config.getOptionalString('techdocs.generators.techdocs') ?? 'docker',
};
this.containerRunner = containerRunner;
}
public async run({
inputDir,
outputDir,
dockerClient,
parsedLocationAnnotation,
etag,
}: GeneratorRunOptions): Promise<void> {
@@ -100,7 +109,7 @@ export class TechdocsGenerator implements GeneratorBase {
);
break;
case 'docker':
await runDockerContainer({
await this.containerRunner.runContainer({
imageName: 'spotify/techdocs',
args: ['build', '-d', '/output'],
logStream,
@@ -109,7 +118,6 @@ export class TechdocsGenerator implements GeneratorBase {
// Set the home directory inside the container as something that applications can
// write to, otherwise they will just fail trying to write to /
envVars: { HOME: '/tmp' },
dockerClient,
});
this.logger.info(
`Successfully generated docs from ${inputDir} into ${outputDir} using techdocs-container`,
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import Docker from 'dockerode';
import { Writable } from 'stream';
import { ParsedLocationAnnotation } from '../../helpers';
@@ -23,7 +22,6 @@ import { ParsedLocationAnnotation } from '../../helpers';
*
* @param {string} inputDir The directory of the uncompiled documentation, with the values from the frontend
* @param {string} outputDir Directory to store generated docs in. Usually - a newly created temporary directory.
* @param {Docker} dockerClient A docker client to run any generator on top of your directory
* @param {ParsedLocationAnnotation} parsedLocationAnnotation backstage.io/techdocs-ref annotation of an entity
* @param {string} etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.
* @param {Writable} [logStream] A dedicated log stream
@@ -31,7 +29,6 @@ import { ParsedLocationAnnotation } from '../../helpers';
export type GeneratorRunOptions = {
inputDir: string;
outputDir: string;
dockerClient: Docker;
parsedLocationAnnotation?: ParsedLocationAnnotation;
etag?: string;
logStream?: Writable;
-2
View File
@@ -38,7 +38,6 @@
"@gitbeaker/core": "^28.0.2",
"@gitbeaker/node": "^28.0.2",
"@octokit/rest": "^18.5.3",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"@types/git-url-parse": "^9.0.0",
"azure-devops-node-api": "^10.1.1",
@@ -46,7 +45,6 @@
"compression": "^1.7.4",
"cors": "^2.8.5",
"cross-fetch": "^3.0.6",
"dockerode": "^3.2.1",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"fs-extra": "^9.0.0",
@@ -17,6 +17,7 @@
import { UrlReader } from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { ScmIntegrations } from '@backstage/integration';
import { TemplaterBuilder } from '../../stages';
import { createCatalogRegisterAction } from './catalog';
import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch';
import {
@@ -26,23 +27,14 @@ import {
createPublishGithubPullRequestAction,
createPublishGitlabAction,
} from './publish';
import Docker from 'dockerode';
import { TemplaterBuilder } from '../../stages';
export const createBuiltinActions = (options: {
reader: UrlReader;
integrations: ScmIntegrations;
dockerClient: Docker;
catalogClient: CatalogApi;
templaters: TemplaterBuilder;
}) => {
const {
reader,
integrations,
dockerClient,
templaters,
catalogClient,
} = options;
const { reader, integrations, templaters, catalogClient } = options;
return [
createFetchPlainAction({
@@ -52,7 +44,6 @@ export const createBuiltinActions = (options: {
createFetchCookiecutterAction({
reader,
integrations,
dockerClient,
templaters,
}),
createPublishGithubAction({
@@ -15,16 +15,16 @@
*/
jest.mock('./helpers');
import { getVoidLogger, UrlReader } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import mock from 'mock-fs';
import os from 'os';
import { resolve as resolvePath } from 'path';
import { createFetchCookiecutterAction } from './cookiecutter';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { Templaters } from '../../../stages/templater';
import { PassThrough } from 'stream';
import { getVoidLogger, UrlReader } from '@backstage/backend-common';
import { Templaters } from '../../../stages/templater';
import { createFetchCookiecutterAction } from './cookiecutter';
import { fetchContents } from './helpers';
import mock from 'mock-fs';
describe('fetch:cookiecutter', () => {
const integrations = ScmIntegrations.fromConfig(
@@ -40,7 +40,6 @@ describe('fetch:cookiecutter', () => {
const templaters = new Templaters();
const cookiecutterTemplater = { run: jest.fn() };
const mockDockerClient = {};
const mockTmpDir = os.tmpdir();
const mockContext = {
input: {
@@ -67,7 +66,6 @@ describe('fetch:cookiecutter', () => {
const action = createFetchCookiecutterAction({
integrations,
templaters,
dockerClient: mockDockerClient as any,
reader: mockReader,
});
@@ -102,7 +100,6 @@ describe('fetch:cookiecutter', () => {
expect(cookiecutterTemplater.run).toHaveBeenCalledWith({
workspacePath: mockTmpDir,
dockerClient: mockDockerClient,
logStream: mockContext.logStream,
values: mockContext.input.values,
});
@@ -123,7 +120,6 @@ describe('fetch:cookiecutter', () => {
expect(cookiecutterTemplater.run).toHaveBeenCalledWith({
workspacePath: mockTmpDir,
dockerClient: mockDockerClient,
logStream: mockContext.logStream,
values: {
...mockContext.input.values,
@@ -166,7 +162,6 @@ describe('fetch:cookiecutter', () => {
const newAction = createFetchCookiecutterAction({
integrations,
templaters: templatersWithoutCookiecutter,
dockerClient: mockDockerClient as any,
reader: mockReader,
});
@@ -14,24 +14,22 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import Docker from 'dockerode';
import { UrlReader } from '@backstage/backend-common';
import { JsonObject } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { JsonObject } from '@backstage/config';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { TemplaterBuilder, TemplaterValues } from '../../../stages/templater';
import { fetchContents } from './helpers';
import { createTemplateAction } from '../../createTemplateAction';
import { fetchContents } from './helpers';
export function createFetchCookiecutterAction(options: {
dockerClient: Docker;
reader: UrlReader;
integrations: ScmIntegrations;
templaters: TemplaterBuilder;
}) {
const { dockerClient, reader, templaters, integrations } = options;
const { reader, templaters, integrations } = options;
return createTemplateAction<{
url: string;
@@ -134,7 +132,6 @@ export function createFetchCookiecutterAction(options: {
// Will execute the template in ./template and put the result in ./result
await cookiecutter.run({
workspacePath: workDir,
dockerClient,
logStream: ctx.logStream,
values,
});
@@ -19,20 +19,12 @@ import fs from 'fs-extra';
import { Processor, Job, StageContext, StageInput } from './types';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import * as uuid from 'uuid';
import Docker from 'dockerode';
import path from 'path';
import { TemplaterValues, TemplaterBase } from '../stages/templater';
import { PreparerBuilder } from '../stages/prepare';
import { TemplaterValues } from '../stages/templater';
import { makeLogStream } from './logger';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
export type JobProcessorArguments = {
preparers: PreparerBuilder;
templater: TemplaterBase;
dockerClient: Docker;
};
export type JobAndDirectoryTuple = {
job: Job;
directory: string;
@@ -14,21 +14,19 @@
* limitations under the License.
*/
import { FilePreparer, PreparerBuilder } from './prepare';
import Docker from 'dockerode';
import { TemplaterBuilder, TemplaterValues } from './templater';
import { PublisherBuilder } from './publish';
import { createTemplateAction } from '../actions';
import { FilePreparer, PreparerBuilder } from './prepare';
import { PublisherBuilder } from './publish';
import { TemplaterBuilder, TemplaterValues } from './templater';
type Options = {
dockerClient: Docker;
preparers: PreparerBuilder;
templaters: TemplaterBuilder;
publishers: PublisherBuilder;
};
export function createLegacyActions(options: Options) {
const { dockerClient, preparers, templaters, publishers } = options;
const { preparers, templaters, publishers } = options;
return [
createTemplateAction({
@@ -55,7 +53,6 @@ export function createLegacyActions(options: Options) {
const templater = templaters.get(ctx.input.templater as string);
await templater.run({
workspacePath: ctx.workspacePath,
dockerClient,
logStream: ctx.logStream,
values: ctx.input.values as TemplaterValues,
});
@@ -14,16 +14,14 @@
* limitations under the License.
*/
const runDockerContainer = jest.fn();
const runCommand = jest.fn();
const commandExists = jest.fn();
jest.mock('./helpers', () => ({ runCommand }));
jest.mock('@backstage/backend-common', () => ({ runDockerContainer }));
jest.mock('command-exists-promise', () => commandExists);
jest.mock('fs-extra');
import Docker from 'dockerode';
import { ContainerRunner } from '@backstage/backend-common';
import fs from 'fs-extra';
import parseGitUrl from 'git-url-parse';
import path from 'path';
@@ -31,7 +29,9 @@ import { PassThrough } from 'stream';
import { CookieCutter } from './cookiecutter';
describe('CookieCutter Templater', () => {
const mockDocker = {} as Docker;
const containerRunner: jest.Mocked<ContainerRunner> = {
runContainer: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
@@ -50,11 +50,10 @@ describe('CookieCutter Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
dockerClient: mockDocker,
});
expect(fs.ensureDir).toBeCalledWith(path.join('tempdir', 'intermediate'));
@@ -83,11 +82,10 @@ describe('CookieCutter Templater', () => {
},
};
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
dockerClient: mockDocker,
});
expect(fs.writeJSON).toBeCalledWith(
@@ -115,12 +113,11 @@ describe('CookieCutter Templater', () => {
},
};
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await expect(
templater.run({
workspacePath: 'tempdir',
values,
dockerClient: mockDocker,
}),
).rejects.toThrow('BAM');
});
@@ -140,14 +137,13 @@ describe('CookieCutter Templater', () => {
.spyOn(fs, 'realpath')
.mockImplementation(x => Promise.resolve(x.toString()));
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
dockerClient: mockDocker,
});
expect(runDockerContainer).toHaveBeenCalledWith({
expect(containerRunner.runContainer).toHaveBeenCalledWith({
imageName: 'spotify/backstage-cookiecutter',
args: [
'cookiecutter',
@@ -164,7 +160,6 @@ describe('CookieCutter Templater', () => {
},
workingDir: '/input',
logStream: undefined,
dockerClient: mockDocker,
});
});
@@ -177,14 +172,13 @@ describe('CookieCutter Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
dockerClient: mockDocker,
});
expect(runDockerContainer).toHaveBeenCalledWith(
expect(containerRunner.runContainer).toHaveBeenCalledWith(
expect.objectContaining({
imageName: 'foo/cookiecutter-image-with-extensions',
}),
@@ -205,15 +199,14 @@ describe('CookieCutter Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream: stream,
dockerClient: mockDocker,
});
expect(runDockerContainer).toHaveBeenCalledWith({
expect(containerRunner.runContainer).toHaveBeenCalledWith({
imageName: 'spotify/backstage-cookiecutter',
args: [
'cookiecutter',
@@ -230,7 +223,6 @@ describe('CookieCutter Templater', () => {
},
workingDir: '/input',
logStream: stream,
dockerClient: mockDocker,
});
});
@@ -250,12 +242,11 @@ describe('CookieCutter Templater', () => {
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
commandExists.mockImplementationOnce(() => () => true);
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream: stream,
dockerClient: mockDocker,
});
expect(runCommand).toHaveBeenCalledWith({
@@ -280,7 +271,7 @@ describe('CookieCutter Templater', () => {
.spyOn(fs, 'readdir')
.mockImplementationOnce(() => Promise.resolve([]));
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
await expect(
templater.run({
workspacePath: 'tempdir',
@@ -292,7 +283,6 @@ describe('CookieCutter Templater', () => {
},
},
logStream: stream,
dockerClient: mockDocker,
}),
).rejects.toThrow(/No data generated by cookiecutter/);
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { runDockerContainer } from '@backstage/backend-common';
import { ContainerRunner } from '@backstage/backend-common';
import { JsonValue } from '@backstage/config';
import fs from 'fs-extra';
import path from 'path';
@@ -24,6 +24,12 @@ import { TemplaterBase, TemplaterRunOptions } from './types';
const commandExists = require('command-exists-promise');
export class CookieCutter implements TemplaterBase {
private readonly containerRunner: ContainerRunner;
constructor({ containerRunner }: { containerRunner: ContainerRunner }) {
this.containerRunner = containerRunner;
}
private async fetchTemplateCookieCutter(
directory: string,
): Promise<Record<string, JsonValue>> {
@@ -40,7 +46,6 @@ export class CookieCutter implements TemplaterBase {
public async run({
workspacePath,
dockerClient,
values,
logStream,
}: TemplaterRunOptions): Promise<void> {
@@ -74,23 +79,16 @@ export class CookieCutter implements TemplaterBase {
logStream,
});
} else {
await runDockerContainer({
await this.containerRunner.runContainer({
imageName: imageName || 'spotify/backstage-cookiecutter',
args: [
'cookiecutter',
'--no-input',
'-o',
'/output',
'/input',
'--verbose',
],
command: 'cookiecutter',
args: ['--no-input', '-o', '/output', '/input', '--verbose'],
mountDirs,
workingDir: '/input',
// Set the home directory inside the container as something that applications can
// write to, otherwise they will just fail trying to write to /
envVars: { HOME: '/tmp' },
logStream,
dockerClient,
});
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { runDockerContainer } from '@backstage/backend-common';
import { ContainerRunner } from '@backstage/backend-common';
import fs from 'fs-extra';
import path from 'path';
import * as yaml from 'yaml';
@@ -25,11 +25,16 @@ import { TemplaterBase, TemplaterRunOptions } from '../types';
const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug';
export class CreateReactAppTemplater implements TemplaterBase {
private readonly containerRunner: ContainerRunner;
constructor({ containerRunner }: { containerRunner: ContainerRunner }) {
this.containerRunner = containerRunner;
}
public async run({
workspacePath,
values,
logStream,
dockerClient,
}: TemplaterRunOptions): Promise<void> {
const {
component_id: componentName,
@@ -46,23 +51,20 @@ export class CreateReactAppTemplater implements TemplaterBase {
[intermediateDir]: '/result',
};
await runDockerContainer({
await this.containerRunner.runContainer({
imageName: 'node:lts-alpine',
command: ['npx'],
args: [
'create-react-app',
componentName as string,
withTypescript ? ' --template typescript' : '',
],
mountDirs,
workingDir: '/result',
logStream: logStream,
dockerClient: dockerClient,
// Set the home directory inside the container as something that applications can
// write to, otherwise they will just fail trying to write to /
envVars: { HOME: '/tmp' },
createOptions: {
Entrypoint: ['npx'],
WorkingDir: '/result',
},
});
// if cookiecutter was successful, intermediateDir will contain
@@ -14,10 +14,15 @@
* limitations under the License.
*/
import { ContainerRunner } from '@backstage/backend-common';
import { CookieCutter } from './cookiecutter';
import { Templaters } from './templaters';
describe('Templaters', () => {
const containerRunner: jest.Mocked<ContainerRunner> = {
runContainer: jest.fn(),
};
it('should throw an error when the templater is not registered', () => {
const templaters = new Templaters();
@@ -29,7 +34,7 @@ describe('Templaters', () => {
});
it('should return the correct templater when the templater matches', () => {
const templaters = new Templaters();
const templater = new CookieCutter();
const templater = new CookieCutter({ containerRunner });
templaters.register('cookiecutter', templater);
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Writable } from 'stream';
import Docker from 'dockerode';
import gitUrlParse from 'git-url-parse';
import type { Writable } from 'stream';
/**
* Currently the required template values. The owner
@@ -48,7 +48,6 @@ export type TemplaterRunOptions = {
workspacePath: string;
values: TemplaterValues;
logStream?: Writable;
dockerClient: Docker;
};
export type TemplaterBase = {
@@ -29,20 +29,17 @@ jest.doMock('fs-extra', () => ({
}));
import {
SingleConnectionDatabaseManager,
PluginDatabaseManager,
getVoidLogger,
PluginDatabaseManager,
SingleConnectionDatabaseManager,
UrlReaders,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import { Preparers, Publishers, Templaters } from '../scaffolder';
import { createRouter } from './router';
import { Templaters, Preparers, Publishers } from '../scaffolder';
import Docker from 'dockerode';
import { CatalogApi } from '@backstage/catalog-client';
jest.mock('dockerode');
const createCatalogClient = (templates: any[] = []) =>
({
@@ -115,7 +112,6 @@ describe('createRouter - working directory', () => {
templaters: new Templaters(),
publishers: new Publishers(),
config: new ConfigReader(workDirConfig('/path')),
dockerClient: new Docker(),
database: createDatabase(),
catalogClient: createCatalogClient([template]),
reader: mockUrlReader,
@@ -130,7 +126,6 @@ describe('createRouter - working directory', () => {
templaters: new Templaters(),
publishers: new Publishers(),
config: new ConfigReader(workDirConfig('/path')),
dockerClient: new Docker(),
database: createDatabase(),
catalogClient: createCatalogClient([template]),
reader: mockUrlReader,
@@ -160,7 +155,6 @@ describe('createRouter - working directory', () => {
templaters: new Templaters(),
publishers: new Publishers(),
config: new ConfigReader({}),
dockerClient: new Docker(),
database: createDatabase(),
catalogClient: createCatalogClient([template]),
reader: mockUrlReader,
@@ -234,7 +228,6 @@ describe('createRouter', () => {
templaters: new Templaters(),
publishers: new Publishers(),
config: new ConfigReader({}),
dockerClient: new Docker(),
database: createDatabase(),
catalogClient: createCatalogClient([template]),
reader: mockUrlReader,
@@ -15,7 +15,6 @@
*/
import { Config } from '@backstage/config';
import Docker from 'dockerode';
import express from 'express';
import { resolve as resolvePath, dirname } from 'path';
import Router from 'express-promise-router';
@@ -62,7 +61,6 @@ export interface RouterOptions {
logger: Logger;
config: Config;
reader: UrlReader;
dockerClient: Docker;
database: PluginDatabaseManager;
catalogClient: CatalogApi;
actions?: TemplateAction<any>[];
@@ -96,7 +94,6 @@ export async function createRouter(
logger: parentLogger,
config,
reader,
dockerClient,
database,
catalogClient,
actions,
@@ -124,13 +121,11 @@ export async function createRouter(
? actions
: [
...createLegacyActions({
dockerClient,
preparers,
publishers,
templaters,
}),
...createBuiltinActions({
dockerClient,
integrations,
catalogClient,
templaters,
@@ -243,7 +238,6 @@ export async function createRouter(
const templater = templaters.get(ctx.entity.spec.templater);
await templater.run({
workspacePath: ctx.workspacePath,
dockerClient,
logStream: ctx.logStream,
values: ctx.values,
});
+1 -1
View File
@@ -35,7 +35,6 @@
"@backstage/config": "^0.1.4",
"@backstage/errors": "^0.1.1",
"@backstage/techdocs-common": "^0.5.0",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"cross-fetch": "^3.0.6",
"dockerode": "^3.2.1",
@@ -47,6 +46,7 @@
},
"devDependencies": {
"@backstage/cli": "^0.6.8",
"@types/dockerode": "^3.2.1",
"supertest": "^6.1.3"
},
"files": [
@@ -28,7 +28,6 @@ import {
PublisherBase,
UrlPreparer,
} from '@backstage/techdocs-common';
import Docker from 'dockerode';
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
@@ -41,7 +40,6 @@ type DocsBuilderArguments = {
publisher: PublisherBase;
entity: Entity;
logger: Logger;
dockerClient: Docker;
};
export class DocsBuilder {
@@ -50,7 +48,6 @@ export class DocsBuilder {
private publisher: PublisherBase;
private entity: Entity;
private logger: Logger;
private dockerClient: Docker;
constructor({
preparers,
@@ -58,14 +55,12 @@ export class DocsBuilder {
publisher,
entity,
logger,
dockerClient,
}: DocsBuilderArguments) {
this.preparer = preparers.get(entity);
this.generator = generators.get(entity);
this.publisher = publisher;
this.entity = entity;
this.logger = logger;
this.dockerClient = dockerClient;
}
public async build(): Promise<void> {
@@ -157,7 +152,6 @@ export class DocsBuilder {
await this.generator.run({
inputDir: preparedDir,
outputDir,
dockerClient: this.dockerClient,
parsedLocationAnnotation,
etag: newEtag,
});
@@ -24,7 +24,6 @@ import {
PublisherBase,
} from '@backstage/techdocs-common';
import fetch from 'cross-fetch';
import Docker from 'dockerode';
import express from 'express';
import Router from 'express-promise-router';
import { Knex } from 'knex';
@@ -40,7 +39,6 @@ type RouterOptions = {
discovery: PluginEndpointDiscovery;
database?: Knex; // TODO: Make database required when we're implementing database stuff.
config: Config;
dockerClient: Docker;
};
export async function createRouter({
@@ -48,7 +46,6 @@ export async function createRouter({
generators,
publisher,
config,
dockerClient,
logger,
discovery,
}: RouterOptions): Promise<express.Router> {
@@ -165,7 +162,6 @@ export async function createRouter({
preparers,
generators,
publisher,
dockerClient,
logger,
entity,
});
@@ -16,21 +16,22 @@
import {
createServiceBuilder,
DockerContainerRunner,
SingleHostDiscovery,
UrlReader,
} from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import {
DirectoryPreparer,
Generators,
Preparers,
Publisher,
TechdocsGenerator,
} from '@backstage/techdocs-common';
import Docker from 'dockerode';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
import Docker from 'dockerode';
import {
Preparers,
DirectoryPreparer,
Generators,
TechdocsGenerator,
Publisher,
} from '@backstage/techdocs-common';
import { ConfigReader } from '@backstage/config';
export interface ServerOptions {
port: number;
@@ -65,21 +66,25 @@ export async function startStandaloneServer(
);
preparers.register('dir', directoryPreparer);
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger, config);
const techdocsGenerator = new TechdocsGenerator({
logger,
containerRunner,
config,
});
generators.register('techdocs', techdocsGenerator);
const publisher = await Publisher.fromConfig(config, { logger, discovery });
const dockerClient = new Docker();
logger.debug('Starting application server...');
const router = await createRouter({
preparers,
generators,
logger,
publisher,
dockerClient,
config,
discovery,
});