techdocs-backend: instantiate containerRunner internally
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
@@ -64,6 +64,7 @@
|
||||
"@smithy/node-http-handler": "^2.1.7",
|
||||
"@trendyol-js/openstack-swift-sdk": "^0.0.7",
|
||||
"@types/express": "^4.17.6",
|
||||
"dockerode": "^4.0.0",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { ForwardedError } from '@backstage/errors';
|
||||
import { PassThrough } from 'stream';
|
||||
import { pipeline as pipelineStream } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import { ContainerRunner, RunContainerOptions } from './types';
|
||||
|
||||
const pipeline = promisify(pipelineStream);
|
||||
|
||||
export type UserOptions = {
|
||||
User?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A {@link ContainerRunner} for Docker containers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class DockerContainerRunner implements ContainerRunner {
|
||||
private readonly dockerClient: Docker;
|
||||
|
||||
constructor() {
|
||||
this.dockerClient = new Docker();
|
||||
}
|
||||
|
||||
async runContainer(options: RunContainerOptions) {
|
||||
const {
|
||||
imageName,
|
||||
command,
|
||||
args,
|
||||
logStream = new PassThrough(),
|
||||
mountDirs = {},
|
||||
workingDir,
|
||||
envVars = {},
|
||||
pullImage = true,
|
||||
defaultUser = false,
|
||||
} = options;
|
||||
|
||||
// Show a better error message when Docker is unavailable.
|
||||
try {
|
||||
await this.dockerClient.ping();
|
||||
} catch (e) {
|
||||
throw new ForwardedError(
|
||||
'This operation requires Docker. Docker does not appear to be available. Docker.ping() failed with',
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
if (pullImage) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.dockerClient.pull(imageName, {}, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline(stream, logStream, { end: false })
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const userOptions: UserOptions = {};
|
||||
if (!defaultUser && 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: {
|
||||
AutoRemove: true,
|
||||
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})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
* 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';
|
||||
@@ -42,7 +41,6 @@ export class Generators implements GeneratorBuilder {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
containerRunner: ContainerRunner;
|
||||
customGenerator?: TechdocsGenerator;
|
||||
},
|
||||
): Promise<GeneratorBuilder> {
|
||||
|
||||
@@ -17,6 +17,8 @@ export { TechdocsGenerator } from './techdocs';
|
||||
export { Generators } from './generators';
|
||||
export { getMkdocsYml } from './helpers';
|
||||
export type {
|
||||
RunContainerOptions,
|
||||
ContainerRunner,
|
||||
GeneratorBase,
|
||||
GeneratorOptions,
|
||||
GeneratorBuilder,
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerRunner } from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
patchMkdocsYmlWithPlugins,
|
||||
} from './mkdocsPatchers';
|
||||
import {
|
||||
ContainerRunner,
|
||||
GeneratorBase,
|
||||
GeneratorConfig,
|
||||
GeneratorOptions,
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
GeneratorRunOptions,
|
||||
} from './types';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { DockerContainerRunner } from './DockerContainerRunner';
|
||||
|
||||
/**
|
||||
* Generates documentation files
|
||||
@@ -55,10 +56,9 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
*/
|
||||
public static readonly defaultDockerImage = 'spotify/techdocs:v1.2.3';
|
||||
private readonly logger: Logger;
|
||||
private readonly containerRunner?: ContainerRunner;
|
||||
private readonly options: GeneratorConfig;
|
||||
private readonly scmIntegrations: ScmIntegrationRegistry;
|
||||
|
||||
private containerRunner?: ContainerRunner;
|
||||
/**
|
||||
* Returns a instance of TechDocs generator
|
||||
* @param config - A Backstage configuration
|
||||
@@ -157,9 +157,7 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
break;
|
||||
case 'docker':
|
||||
if (this.containerRunner === undefined) {
|
||||
throw new Error(
|
||||
"Invalid state: containerRunner cannot be undefined when runIn is 'docker'",
|
||||
);
|
||||
this.containerRunner = new DockerContainerRunner();
|
||||
}
|
||||
await this.containerRunner.runContainer({
|
||||
imageName:
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerRunner } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Writable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
@@ -99,3 +98,32 @@ export type DefaultMkdocsContent = {
|
||||
docs_dir: string;
|
||||
plugins: String[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Options passed to the {@link ContainerRunner.runContainer} method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RunContainerOptions = {
|
||||
imageName: string;
|
||||
command?: string | string[];
|
||||
args: string[];
|
||||
logStream?: Writable;
|
||||
mountDirs?: Record<string, string>;
|
||||
workingDir?: string;
|
||||
envVars?: Record<string, string>;
|
||||
pullImage?: boolean;
|
||||
defaultUser?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the running of containers, on behalf of others.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContainerRunner {
|
||||
/**
|
||||
* Runs a container image to completion.
|
||||
*/
|
||||
runContainer(opts: RunContainerOptions): Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user