playwright/packages/playwright-test/src/docker/dockerApi.ts

213 lines
6.4 KiB
TypeScript
Raw Normal View History

/**
* Copyright (c) Microsoft Corporation.
*
* 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 http from 'http';
// Docker engine API.
// See https://docs.docker.com/engine/api/v1.41/
const DOCKER_API_VERSION = '1.41';
export interface DockerImage {
imageId: string;
names: string[];
}
export interface PortBinding {
ip: string;
hostPort: number;
containerPort: number;
}
export interface DockerContainer {
containerId: string;
imageId: string;
state: 'created'|'restarting'|'running'|'removing'|'paused'|'exited'|'dead';
names: string[];
portBindings: PortBinding[];
}
export async function listContainers(): Promise<DockerContainer[]> {
const containers = (await getJSON('/containers/json')) ?? [];
return containers.map((container: any) => ({
containerId: container.Id,
imageId: container.ImageID,
state: container.State,
names: container.Names,
portBindings: container.Ports?.map((portInfo: any) => ({
ip: portInfo.IP,
hostPort: portInfo.PublicPort,
containerPort: portInfo.PrivatePort,
})) ?? [],
}));
}
interface LaunchContainerOptions {
imageId: string;
autoRemove: boolean;
command?: string[];
ports?: Number[];
name?: string;
waitUntil?: 'not-running' | 'next-exit' | 'removed';
}
export async function launchContainer(options: LaunchContainerOptions): Promise<string> {
const ExposedPorts: any = {};
const PortBindings: any = {};
for (const port of (options.ports ?? [])) {
ExposedPorts[`${port}/tcp`] = {};
PortBindings[`${port}/tcp`] = [{ HostPort: '0', HostIp: '127.0.0.1' }];
}
const container = await postJSON(`/containers/create` + (options.name ? '?name=' + options.name : ''), {
Cmd: options.command,
AttachStdout: true,
AttachStderr: true,
Image: options.imageId,
ExposedPorts,
HostConfig: {
Init: true,
AutoRemove: options.autoRemove,
ShmSize: 2 * 1024 * 1024 * 1024,
PortBindings,
},
});
await postJSON(`/containers/${container.Id}/start`);
if (options.waitUntil)
await postJSON(`/containers/${container.Id}/wait?condition=${options.waitUntil}`);
return container.Id;
}
interface StopContainerOptions {
containerId: string,
waitUntil?: 'not-running' | 'next-exit' | 'removed';
}
export async function stopContainer(options: StopContainerOptions) {
await Promise.all([
// Make sure to wait for the container to be removed.
postJSON(`/containers/${options.containerId}/wait?condition=${options.waitUntil ?? 'not-running'}`),
postJSON(`/containers/${options.containerId}/kill`),
]);
}
export async function removeContainer(containerId: string) {
await Promise.all([
// Make sure to wait for the container to be removed.
postJSON(`/containers/${containerId}/wait?condition=removed`),
callDockerAPI('delete', `/containers/${containerId}`),
]);
}
export async function getContainerLogs(containerId: string): Promise<string[]> {
const rawLogs = await callDockerAPI('get', `/containers/${containerId}/logs?stdout=true&stderr=true`).catch(e => '');
if (!rawLogs)
return [];
// Docker might prefix every log line with 8 characters. Stip them out.
// See https://github.com/moby/moby/issues/7375
// This doesn't happen if the containers is launched manually with attached terminal.
return rawLogs.split('\n').map(line => {
if ([0, 1, 2].includes(line.charCodeAt(0)))
return line.substring(8);
return line;
});
}
interface CommitContainerOptions {
containerId: string,
repo: string,
tag: string,
entrypoint?: string,
env?: {[key: string]: string | number | boolean | undefined},
}
export async function commitContainer(options: CommitContainerOptions) {
const Env = [];
for (const [key, value] of Object.entries(options.env ?? {}))
Env.push(`${key}=${value}`);
await postJSON(`/commit?container=${options.containerId}&repo=${options.repo}&tag=${options.tag}`, {
Entrypoint: options.entrypoint ?? '',
Env,
});
}
export async function listImages(): Promise<DockerImage[]> {
const rawImages: any[] = (await getJSON('/images/json')) ?? [];
return rawImages.map((rawImage: any) => ({
imageId: rawImage.Id,
names: rawImage.RepoTags ?? [],
}));
}
export async function removeImage(imageId: string) {
await callDockerAPI('delete', `/images/${imageId}`);
}
export async function checkEngineRunning() {
try {
await callDockerAPI('get', '/info');
return true;
} catch (e) {
return false;
}
}
async function getJSON(url: string): Promise<any> {
const result = await callDockerAPI('get', url);
if (!result)
return result;
return JSON.parse(result);
}
async function postJSON(url: string, json: any = undefined) {
const result = await callDockerAPI('post', url, json ? JSON.stringify(json) : undefined);
if (!result)
return result;
return JSON.parse(result);
}
function callDockerAPI(method: 'post'|'get'|'delete', url: string, body: Buffer|string|undefined = undefined): Promise<string> {
const dockerSocket = process.platform === 'win32' ? '\\\\.\\pipe\\docker_engine' : '/var/run/docker.sock';
return new Promise((resolve, reject) => {
const request = http.request({
socketPath: dockerSocket,
path: `/v${DOCKER_API_VERSION}${url}`,
timeout: 30000,
method,
}, (response: http.IncomingMessage) => {
let body = '';
response.on('data', function(chunk){
body += chunk;
});
response.on('end', function(){
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300)
reject(new Error(`${method} ${url} FAILED with statusCode ${response.statusCode} and body\n${body}`));
else
resolve(body);
});
});
request.on('error', function(e){
reject(e);
});
if (body) {
request.setHeader('Content-Type', 'application/json');
request.setHeader('Content-Length', body.length);
request.write(body);
} else {
request.setHeader('Content-Type', 'text/plain');
}
request.end();
});
}