From 705bc28e925415b622cd6e2b0ca49837600d9717 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 13 Sep 2022 13:23:04 -0700 Subject: [PATCH] feat(docker): auto-bind container ports to host ports (#17307) Drive-by: make sure docker container does not expose ports on `0.0.0.0` and instead registers to localhost. This way websocket and vnc ports are not exposed to the public internet. --- packages/playwright-test/src/docker/docker.ts | 37 +++++++++++++------ .../playwright-test/src/docker/dockerApi.ts | 16 +++++++- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/playwright-test/src/docker/docker.ts b/packages/playwright-test/src/docker/docker.ts index 51bdab26e5..aa8900196a 100644 --- a/packages/playwright-test/src/docker/docker.ts +++ b/packages/playwright-test/src/docker/docker.ts @@ -96,18 +96,31 @@ interface ContainerInfo { } export async function containerInfo(): Promise { - const containerId = await findRunningDockerContainerId(); - if (!containerId) + const container = await findRunningDockerContainer(); + if (!container) return undefined; - const logLines = await dockerApi.getContainerLogs(containerId); + const logLines = await dockerApi.getContainerLogs(container.containerId); + + const containerUrlToHostUrl = (address: string) => { + const url = new URL(address); + const portBinding = container.portBindings.find(binding => binding.containerPort === +url.port); + if (!portBinding) + return undefined; + + url.host = portBinding.ip; + url.port = portBinding.hostPort + ''; + return url.toString(); + }; + const WS_LINE_PREFIX = 'Listening on ws://'; const webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX)); const NOVNC_LINE_PREFIX = 'novnc is listening on '; const novncLine = logLines.find(line => line.startsWith(NOVNC_LINE_PREFIX)); - return novncLine && webSocketLine ? { - wsEndpoint: 'ws://' + webSocketLine.substring(WS_LINE_PREFIX.length), - vncSession: novncLine.substring(NOVNC_LINE_PREFIX.length), - } : undefined; + if (!novncLine || !webSocketLine) + return undefined; + const wsEndpoint = containerUrlToHostUrl('ws://' + webSocketLine.substring(WS_LINE_PREFIX.length)); + const vncSession = containerUrlToHostUrl(novncLine.substring(NOVNC_LINE_PREFIX.length)); + return wsEndpoint && vncSession ? { wsEndpoint, vncSession } : undefined; } export async function ensureContainerOrDie(): Promise { @@ -149,11 +162,11 @@ export async function ensureContainerOrDie(): Promise { } export async function stopContainer() { - const containerId = await findRunningDockerContainerId(); - if (!containerId) + const container = await findRunningDockerContainer(); + if (!container) return; await dockerApi.stopContainer({ - containerId, + containerId: container.containerId, waitUntil: 'removed', }); } @@ -176,10 +189,10 @@ async function findDockerImage(imageName: string): Promise image.names.includes(imageName)); } -async function findRunningDockerContainerId(): Promise { +async function findRunningDockerContainer(): Promise { const containers = await dockerApi.listContainers(); const dockerImage = await findDockerImage(VRT_IMAGE_NAME); const container = dockerImage ? containers.find(container => container.imageId === dockerImage.imageId) : undefined; - return container?.state === 'running' ? container.containerId : undefined; + return container?.state === 'running' ? container : undefined; } diff --git a/packages/playwright-test/src/docker/dockerApi.ts b/packages/playwright-test/src/docker/dockerApi.ts index eabf1b0e98..e9598d604d 100644 --- a/packages/playwright-test/src/docker/dockerApi.ts +++ b/packages/playwright-test/src/docker/dockerApi.ts @@ -25,11 +25,18 @@ export interface DockerImage { 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 { @@ -38,7 +45,12 @@ export async function listContainers(): Promise { containerId: container.Id, imageId: container.ImageID, state: container.State, - names: container.Names + names: container.Names, + portBindings: container.Ports?.map((portInfo: any) => ({ + ip: portInfo.IP, + hostPort: portInfo.PublicPort, + containerPort: portInfo.PrivatePort, + })) ?? [], })); } @@ -56,7 +68,7 @@ export async function launchContainer(options: LaunchContainerOptions): Promise< const PortBindings: any = {}; for (const port of (options.ports ?? [])) { ExposedPorts[`${port}/tcp`] = {}; - PortBindings[`${port}/tcp`] = [{ HostPort: port + '' }]; + PortBindings[`${port}/tcp`] = [{ HostPort: '0', HostIp: '127.0.0.1' }]; } const container = await postJSON(`/containers/create` + (options.name ? '?name=' + options.name : ''), { Cmd: options.command,