2022-09-09 15:25:42 -07:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
/* eslint-disable no-console */
|
|
|
|
|
|
|
|
import path from 'path';
|
2022-09-21 15:45:43 -04:00
|
|
|
import { spawnAsync } from '../utils/spawnAsync';
|
|
|
|
import * as utils from '../utils';
|
|
|
|
import { getPlaywrightVersion } from '../common/userAgent';
|
2022-09-13 10:55:11 -07:00
|
|
|
import * as dockerApi from './dockerApi';
|
2022-09-21 15:45:43 -04:00
|
|
|
import type { Command } from '../utilsBundle';
|
2022-09-09 15:25:42 -07:00
|
|
|
|
|
|
|
const VRT_IMAGE_DISTRO = 'focal';
|
|
|
|
const VRT_IMAGE_NAME = `playwright:local-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
|
|
|
const VRT_CONTAINER_NAME = `playwright-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
2022-09-13 17:08:01 -07:00
|
|
|
const VRT_CONTAINER_LABEL_NAME = 'dev.playwright.vrt-service.version';
|
|
|
|
const VRT_CONTAINER_LABEL_VALUE = '1';
|
2022-09-09 15:25:42 -07:00
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
async function startPlaywrightContainer() {
|
2022-09-13 16:14:49 -07:00
|
|
|
await checkDockerEngineIsRunningOrDie();
|
|
|
|
|
|
|
|
let info = await containerInfo();
|
|
|
|
if (!info) {
|
|
|
|
process.stdout.write(`Starting docker container... `);
|
|
|
|
const time = Date.now();
|
|
|
|
info = await ensurePlaywrightContainerOrDie();
|
|
|
|
const deltaMs = (Date.now() - time);
|
|
|
|
console.log('Done in ' + (deltaMs / 1000).toFixed(1) + 's');
|
|
|
|
}
|
|
|
|
console.log([
|
|
|
|
`- View screen:`,
|
|
|
|
` ${info.vncSession}`,
|
|
|
|
`- Run tests with browsers inside container:`,
|
|
|
|
` npx playwright docker test`,
|
|
|
|
`- Stop background container *manually* when you are done working with tests:`,
|
|
|
|
` npx playwright docker stop`,
|
|
|
|
].join('\n'));
|
|
|
|
}
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
async function stopAllPlaywrightContainers() {
|
2022-09-13 16:14:49 -07:00
|
|
|
await checkDockerEngineIsRunningOrDie();
|
|
|
|
|
2022-09-13 17:08:01 -07:00
|
|
|
const allContainers = await dockerApi.listContainers();
|
|
|
|
const vrtContainers = allContainers.filter(container => container.labels[VRT_CONTAINER_LABEL_NAME] === VRT_CONTAINER_LABEL_VALUE);
|
|
|
|
await Promise.all(vrtContainers.map(container => dockerApi.stopContainer({
|
2022-09-13 16:14:49 -07:00
|
|
|
containerId: container.containerId,
|
|
|
|
waitUntil: 'removed',
|
2022-09-13 17:08:01 -07:00
|
|
|
})));
|
2022-09-13 16:14:49 -07:00
|
|
|
}
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
async function deletePlaywrightImage() {
|
2022-09-13 16:14:49 -07:00
|
|
|
await checkDockerEngineIsRunningOrDie();
|
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
const dockerImage = await findDockerImage(VRT_IMAGE_NAME);
|
|
|
|
if (!dockerImage)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (await containerInfo())
|
2022-09-13 17:08:01 -07:00
|
|
|
await stopAllPlaywrightContainers();
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.removeImage(dockerImage.imageId);
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
async function buildPlaywrightImage() {
|
2022-09-13 16:14:49 -07:00
|
|
|
await checkDockerEngineIsRunningOrDie();
|
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
// 1. Build or pull base image.
|
2022-10-07 09:21:33 -07:00
|
|
|
let baseImageName = process.env.PWTEST_DOCKER_BASE_IMAGE || '';
|
|
|
|
if (!baseImageName) {
|
|
|
|
const isDevelopmentMode = getPlaywrightVersion().includes('next');
|
|
|
|
if (isDevelopmentMode) {
|
|
|
|
// Use our docker build scripts in development mode!
|
2022-09-09 15:25:42 -07:00
|
|
|
const arch = process.arch === 'arm64' ? '--arm64' : '--amd64';
|
2022-09-15 15:48:12 -07:00
|
|
|
throw createStacklessError(utils.wrapInASCIIBox([
|
2022-09-09 15:25:42 -07:00
|
|
|
`You are in DEVELOPMENT mode!`,
|
|
|
|
``,
|
|
|
|
`1. Build local base image`,
|
|
|
|
` ./utils/docker/build.sh ${arch} ${VRT_IMAGE_DISTRO} playwright:localbuild`,
|
|
|
|
`2. Use the local base to build VRT image:`,
|
|
|
|
` PWTEST_DOCKER_BASE_IMAGE=playwright:localbuild npx playwright docker build`,
|
|
|
|
].join('\n'), 1));
|
|
|
|
}
|
2022-10-07 09:21:33 -07:00
|
|
|
baseImageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
2022-09-09 15:25:42 -07:00
|
|
|
const { code } = await spawnAsync('docker', ['pull', baseImageName], { stdio: 'inherit' });
|
|
|
|
if (code !== 0)
|
|
|
|
throw new Error('Failed to pull docker image!');
|
|
|
|
}
|
|
|
|
// 2. Find pulled docker image
|
|
|
|
const dockerImage = await findDockerImage(baseImageName);
|
|
|
|
if (!dockerImage)
|
|
|
|
throw new Error(`Failed to pull ${baseImageName}`);
|
2022-09-22 16:38:54 -04:00
|
|
|
// 3. Delete previous build of the playwright image to avoid untagged images.
|
|
|
|
await deletePlaywrightImage();
|
|
|
|
// 4. Launch container and install VNC in it
|
2022-09-09 15:25:42 -07:00
|
|
|
console.log(`Building ${VRT_IMAGE_NAME}...`);
|
2022-09-13 10:55:11 -07:00
|
|
|
const containerId = await dockerApi.launchContainer({
|
|
|
|
imageId: dockerImage.imageId,
|
2022-09-09 15:25:42 -07:00
|
|
|
autoRemove: false,
|
2022-09-22 16:38:54 -04:00
|
|
|
workingDir: '/ms-playwright-agent',
|
|
|
|
command: ['npx', 'playwright', 'docker', 'install-server-deps'],
|
2022-09-13 10:55:11 -07:00
|
|
|
waitUntil: 'not-running',
|
2022-09-09 15:25:42 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
// 4. Commit a new image based on the launched container with installed VNC & noVNC.
|
|
|
|
const [vrtRepo, vrtTag] = VRT_IMAGE_NAME.split(':');
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.commitContainer({
|
|
|
|
containerId,
|
|
|
|
repo: vrtRepo,
|
|
|
|
tag: vrtTag,
|
2022-09-22 16:38:54 -04:00
|
|
|
workingDir: '/ms-playwright-agent',
|
|
|
|
entrypoint: ['npx', 'playwright', 'docker', 'run-server'],
|
2022-09-13 10:55:11 -07:00
|
|
|
env: {
|
|
|
|
'DISPLAY_NUM': '99',
|
|
|
|
'DISPLAY': ':99',
|
|
|
|
},
|
2022-09-09 15:25:42 -07:00
|
|
|
});
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.removeContainer(containerId);
|
2022-09-09 15:25:42 -07:00
|
|
|
console.log(`Done!`);
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ContainerInfo {
|
|
|
|
wsEndpoint: string;
|
|
|
|
vncSession: string;
|
|
|
|
}
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
async function printDockerStatus() {
|
2022-09-15 15:48:12 -07:00
|
|
|
const isDockerEngine = await dockerApi.checkEngineRunning();
|
|
|
|
const imageIsPulled = isDockerEngine && !!(await findDockerImage(VRT_IMAGE_NAME));
|
|
|
|
const info = isDockerEngine ? await containerInfo() : undefined;
|
|
|
|
console.log(JSON.stringify({
|
|
|
|
dockerEngineRunning: isDockerEngine,
|
|
|
|
imageName: VRT_IMAGE_NAME,
|
|
|
|
imageIsPulled,
|
2022-09-15 16:59:41 -07:00
|
|
|
containerWSEndpoint: info?.wsEndpoint ?? '',
|
2022-09-15 15:48:12 -07:00
|
|
|
containerVNCEndpoint: info?.vncSession ?? '',
|
|
|
|
}, null, 2));
|
|
|
|
}
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
export async function containerInfo(): Promise<ContainerInfo|undefined> {
|
2022-09-13 17:08:01 -07:00
|
|
|
const allContainers = await dockerApi.listContainers();
|
|
|
|
const pwDockerImage = await findDockerImage(VRT_IMAGE_NAME);
|
|
|
|
const container = allContainers.find(container => container.imageId === pwDockerImage?.imageId && container.state === 'running');
|
2022-09-13 13:23:04 -07:00
|
|
|
if (!container)
|
2022-09-09 15:25:42 -07:00
|
|
|
return undefined;
|
2022-09-13 13:23:04 -07:00
|
|
|
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();
|
|
|
|
};
|
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
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));
|
2022-09-13 13:23:04 -07:00
|
|
|
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;
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
export async function ensurePlaywrightContainerOrDie(): Promise<ContainerInfo> {
|
2022-09-09 15:25:42 -07:00
|
|
|
const pwImage = await findDockerImage(VRT_IMAGE_NAME);
|
|
|
|
if (!pwImage) {
|
2022-09-15 15:48:12 -07:00
|
|
|
throw createStacklessError('\n' + utils.wrapInASCIIBox([
|
2022-09-09 15:25:42 -07:00
|
|
|
`Failed to find local docker image.`,
|
|
|
|
`Please build local docker image with the following command:`,
|
|
|
|
``,
|
|
|
|
` npx playwright docker build`,
|
|
|
|
``,
|
|
|
|
`<3 Playwright Team`,
|
|
|
|
].join('\n'), 1));
|
|
|
|
}
|
|
|
|
|
|
|
|
let info = await containerInfo();
|
|
|
|
if (info)
|
|
|
|
return info;
|
|
|
|
|
2022-09-15 16:59:41 -07:00
|
|
|
// The `npx playwright docker build` command is *NOT GUARANTEED* to produce
|
|
|
|
// images with the same SHA.
|
|
|
|
//
|
|
|
|
// Consider the following sequence of actions:
|
|
|
|
// 1. Build first version of image: `npx playwright docker build`
|
|
|
|
// 2. Run container off the image: `npx playwright docker start`
|
|
|
|
// 3. Build second version of image: `npx playwright docker build`
|
|
|
|
//
|
|
|
|
// Our container auto-detection is based on the parent image SHA.
|
|
|
|
// If the image produced at Step 3 has a different SHA then the one produced on Step 1,
|
|
|
|
// then we **won't be able** to auto-detect the container from Step 2.
|
|
|
|
//
|
|
|
|
// Additionally, we won't be able to launch a new container based off image
|
|
|
|
// from Step 3, since it will have a conflicting container name.
|
|
|
|
//
|
|
|
|
// We check if there's a same-named container running to detect & handle this situation.
|
|
|
|
const hasSameNamedContainer = async () => (await dockerApi.listContainers()).some(container => container.names.includes(VRT_CONTAINER_NAME));
|
|
|
|
if (await hasSameNamedContainer()) {
|
|
|
|
// Since we mark all our containers with labels, we'll be able to stop it.
|
|
|
|
await stopAllPlaywrightContainers();
|
|
|
|
// If it wasn't our container, then it was launched manually and has to be
|
|
|
|
// stopped manually as well.
|
|
|
|
if (await hasSameNamedContainer()) {
|
|
|
|
throw createStacklessError('\n' + utils.wrapInASCIIBox([
|
|
|
|
`There is already a container with name ${VRT_CONTAINER_NAME}`,
|
|
|
|
`Please stop this container manually and rerun tests:`,
|
|
|
|
``,
|
|
|
|
` docker kill ${VRT_CONTAINER_NAME}`,
|
|
|
|
``,
|
|
|
|
`<3 Playwright Team`,
|
|
|
|
].join('\n'), 1));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.launchContainer({
|
|
|
|
imageId: pwImage.imageId,
|
2022-09-09 15:25:42 -07:00
|
|
|
name: VRT_CONTAINER_NAME,
|
|
|
|
autoRemove: true,
|
|
|
|
ports: [5400, 7900],
|
2022-09-13 17:08:01 -07:00
|
|
|
labels: {
|
|
|
|
[VRT_CONTAINER_LABEL_NAME]: VRT_CONTAINER_LABEL_VALUE,
|
|
|
|
},
|
2022-09-09 15:25:42 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
// Wait for the service to become available.
|
|
|
|
const startTime = Date.now();
|
|
|
|
const timeouts = [0, 100, 100, 200, 500, 1000];
|
|
|
|
do {
|
|
|
|
await new Promise(x => setTimeout(x, timeouts.shift() ?? 1000));
|
|
|
|
info = await containerInfo();
|
|
|
|
} while (!info && Date.now() < startTime + 60000);
|
|
|
|
|
|
|
|
if (!info)
|
|
|
|
throw new Error('Failed to launch docker container!');
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
export async function checkDockerEngineIsRunningOrDie() {
|
2022-09-13 10:55:11 -07:00
|
|
|
if (await dockerApi.checkEngineRunning())
|
|
|
|
return;
|
2022-09-15 15:48:12 -07:00
|
|
|
throw createStacklessError(utils.wrapInASCIIBox([
|
2022-09-13 10:55:11 -07:00
|
|
|
`Docker is not running!`,
|
|
|
|
`Please install and launch docker:`,
|
|
|
|
``,
|
|
|
|
` https://docs.docker.com/get-docker`,
|
|
|
|
``,
|
|
|
|
].join('\n'), 1));
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 10:55:11 -07:00
|
|
|
async function findDockerImage(imageName: string): Promise<dockerApi.DockerImage|undefined> {
|
|
|
|
const images = await dockerApi.listImages();
|
|
|
|
return images.find(image => image.names.includes(imageName));
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-15 15:48:12 -07:00
|
|
|
function createStacklessError(message: string) {
|
|
|
|
const error = new Error(message);
|
|
|
|
error.stack = '';
|
|
|
|
return error;
|
|
|
|
}
|
2022-09-21 15:45:43 -04:00
|
|
|
|
|
|
|
export function addDockerCLI(program: Command) {
|
2022-09-29 17:22:09 -04:00
|
|
|
const dockerCommand = program.command('docker', { hidden: true })
|
2022-09-21 15:45:43 -04:00
|
|
|
.description(`Manage Docker integration (EXPERIMENTAL)`);
|
|
|
|
|
|
|
|
dockerCommand.command('build')
|
|
|
|
.description('build local docker image')
|
|
|
|
.action(async function(options) {
|
|
|
|
try {
|
|
|
|
await buildPlaywrightImage();
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e.stack ? e : e.message);
|
2022-10-05 21:31:50 -07:00
|
|
|
process.exit(1);
|
2022-09-21 15:45:43 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
dockerCommand.command('start')
|
|
|
|
.description('start docker container')
|
|
|
|
.action(async function(options) {
|
|
|
|
try {
|
|
|
|
await startPlaywrightContainer();
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e.stack ? e : e.message);
|
2022-10-05 21:31:50 -07:00
|
|
|
process.exit(1);
|
2022-09-21 15:45:43 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
dockerCommand.command('stop')
|
|
|
|
.description('stop docker container')
|
|
|
|
.action(async function(options) {
|
|
|
|
try {
|
|
|
|
await stopAllPlaywrightContainers();
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e.stack ? e : e.message);
|
2022-10-05 21:31:50 -07:00
|
|
|
process.exit(1);
|
2022-09-21 15:45:43 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
dockerCommand.command('delete-image', { hidden: true })
|
|
|
|
.description('delete docker image, if any')
|
|
|
|
.action(async function(options) {
|
|
|
|
try {
|
|
|
|
await deletePlaywrightImage();
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e.stack ? e : e.message);
|
2022-10-05 21:31:50 -07:00
|
|
|
process.exit(1);
|
2022-09-21 15:45:43 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-09-22 16:38:54 -04:00
|
|
|
dockerCommand.command('install-server-deps', { hidden: true })
|
2022-10-05 21:31:50 -07:00
|
|
|
.description('install run-server dependencies')
|
2022-09-22 16:38:54 -04:00
|
|
|
.action(async function() {
|
|
|
|
const { code } = await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_install_deps.sh')], { stdio: 'inherit' });
|
|
|
|
if (code !== 0)
|
|
|
|
throw new Error('Failed to install server dependencies!');
|
|
|
|
});
|
|
|
|
|
|
|
|
dockerCommand.command('run-server', { hidden: true })
|
2022-10-05 21:31:50 -07:00
|
|
|
.description('run playwright server')
|
2022-09-22 16:38:54 -04:00
|
|
|
.action(async function() {
|
|
|
|
await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_run_server.sh')], { stdio: 'inherit' });
|
|
|
|
});
|
|
|
|
|
2022-09-21 15:45:43 -04:00
|
|
|
dockerCommand.command('print-status-json', { hidden: true })
|
|
|
|
.description('print docker status')
|
|
|
|
.action(async function(options) {
|
|
|
|
await printDockerStatus();
|
|
|
|
});
|
|
|
|
}
|