feat(containers): introduce separate container commands (#17541)

This patch introduces hidden commands to control container
lifecycle:
- `npx playwright docker install-server-deps` to install fluxbox,
  vnc, novnc & to configure them.
- `npx playwright docker run-server` to run a server inside the
  container.

Drive-by: remove old version of container image when building a new
version with the same name. This way we won't pile up untagged
container images.
This commit is contained in:
Andrey Lushnikov 2022-09-22 16:38:54 -04:00 committed by GitHub
parent 6b4afbb8df
commit 4cd2176155
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 43 deletions

View file

@ -88,38 +88,3 @@ $center $full|/ms-playwright-agent/node_modules/playwright-core/lib/server/chrom
$center $full|/ms-playwright-agent/node_modules/playwright-core/lib/server/chromium/appIcon.png||:99.0 $center $full|/ms-playwright-agent/node_modules/playwright-core/lib/server/chromium/appIcon.png||:99.0
EOF EOF
# Create entrypoint.sh
cat <<'EOF' > /entrypoint.sh
#!/bin/bash
set -e
SCREEN_WIDTH=1360
SCREEN_HEIGHT=1020
SCREEN_DEPTH=24
SCREEN_DPI=96
GEOMETRY="$SCREEN_WIDTH""x""$SCREEN_HEIGHT""x""$SCREEN_DEPTH"
nohup /usr/bin/xvfb-run --server-num=$DISPLAY_NUM \
--listen-tcp \
--server-args="-screen 0 "$GEOMETRY" -fbdir /var/tmp -dpi "$SCREEN_DPI" -listen tcp -noreset -ac +extension RANDR" \
/usr/bin/fluxbox -display "$DISPLAY" >/dev/null 2>&1 &
for i in $(seq 1 500); do
if xdpyinfo -display $DISPLAY >/dev/null 2>&1; then
break
fi
echo "Waiting for Xvfb..."
sleep 0.2
done
nohup x11vnc -noprimary -nosetprimary -forever -shared -rfbport 5900 -rfbportv6 5900 -display "$DISPLAY" >/dev/null 2>&1 &
nohup /opt/bin/noVNC/utils/novnc_proxy --listen 7900 --vnc localhost:5900 >/dev/null 2>&1 &
cd /ms-playwright-agent
NOVNC_UUID=$(cat /proc/sys/kernel/random/uuid)
echo "novnc is listening on http://127.0.0.1:7900?path=$NOVNC_UUID&resize=scale&autoconnect=1"
PW_UUID=$(cat /proc/sys/kernel/random/uuid)
npx playwright run-server --port=5400 --path=/$PW_UUID
EOF
chmod 755 /entrypoint.sh

View file

@ -0,0 +1,31 @@
#!/bin/bash
set -e
SCREEN_WIDTH=1360
SCREEN_HEIGHT=1020
SCREEN_DEPTH=24
SCREEN_DPI=96
GEOMETRY="$SCREEN_WIDTH""x""$SCREEN_HEIGHT""x""$SCREEN_DEPTH"
nohup /usr/bin/xvfb-run --server-num=$DISPLAY_NUM \
--listen-tcp \
--server-args="-screen 0 "$GEOMETRY" -fbdir /var/tmp -dpi "$SCREEN_DPI" -listen tcp -noreset -ac +extension RANDR" \
/usr/bin/fluxbox -display "$DISPLAY" >/dev/null 2>&1 &
for i in $(seq 1 500); do
if xdpyinfo -display $DISPLAY >/dev/null 2>&1; then
break
fi
echo "Waiting for Xvfb..."
sleep 0.2
done
nohup x11vnc -noprimary -nosetprimary -forever -shared -rfbport 5900 -rfbportv6 5900 -display "$DISPLAY" >/dev/null 2>&1 &
nohup /opt/bin/noVNC/utils/novnc_proxy --listen 7900 --vnc localhost:5900 >/dev/null 2>&1 &
cd /ms-playwright-agent
NOVNC_UUID=$(cat /proc/sys/kernel/random/uuid)
echo "novnc is listening on http://127.0.0.1:7900?path=$NOVNC_UUID&resize=scale&autoconnect=1"
PW_UUID=$(cat /proc/sys/kernel/random/uuid)
npx playwright run-server --port=5400 --path=/$PW_UUID

View file

@ -16,7 +16,6 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import path from 'path'; import path from 'path';
import fs from 'fs';
import { spawnAsync } from '../utils/spawnAsync'; import { spawnAsync } from '../utils/spawnAsync';
import * as utils from '../utils'; import * as utils from '../utils';
import { getPlaywrightVersion } from '../common/userAgent'; import { getPlaywrightVersion } from '../common/userAgent';
@ -102,13 +101,15 @@ async function buildPlaywrightImage() {
const dockerImage = await findDockerImage(baseImageName); const dockerImage = await findDockerImage(baseImageName);
if (!dockerImage) if (!dockerImage)
throw new Error(`Failed to pull ${baseImageName}`); throw new Error(`Failed to pull ${baseImageName}`);
// 3. Launch container and install VNC in it // 3. Delete previous build of the playwright image to avoid untagged images.
await deletePlaywrightImage();
// 4. Launch container and install VNC in it
console.log(`Building ${VRT_IMAGE_NAME}...`); console.log(`Building ${VRT_IMAGE_NAME}...`);
const buildScriptText = await fs.promises.readFile(path.join(__dirname, 'build_docker_image.sh'), 'utf8');
const containerId = await dockerApi.launchContainer({ const containerId = await dockerApi.launchContainer({
imageId: dockerImage.imageId, imageId: dockerImage.imageId,
autoRemove: false, autoRemove: false,
command: ['/bin/bash', '-c', buildScriptText], workingDir: '/ms-playwright-agent',
command: ['npx', 'playwright', 'docker', 'install-server-deps'],
waitUntil: 'not-running', waitUntil: 'not-running',
}); });
@ -118,7 +119,8 @@ async function buildPlaywrightImage() {
containerId, containerId,
repo: vrtRepo, repo: vrtRepo,
tag: vrtTag, tag: vrtTag,
entrypoint: '/entrypoint.sh', workingDir: '/ms-playwright-agent',
entrypoint: ['npx', 'playwright', 'docker', 'run-server'],
env: { env: {
'DISPLAY_NUM': '99', 'DISPLAY_NUM': '99',
'DISPLAY': ':99', 'DISPLAY': ':99',
@ -317,6 +319,20 @@ export function addDockerCLI(program: Command) {
} }
}); });
dockerCommand.command('install-server-deps', { hidden: true })
.description('delete docker image, if any')
.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 })
.description('delete docker image, if any')
.action(async function() {
await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_run_server.sh')], { stdio: 'inherit' });
});
dockerCommand.command('print-status-json', { hidden: true }) dockerCommand.command('print-status-json', { hidden: true })
.description('print docker status') .description('print docker status')
.action(async function(options) { .action(async function(options) {

View file

@ -65,6 +65,7 @@ interface LaunchContainerOptions {
labels?: Record<string, string>; labels?: Record<string, string>;
ports?: Number[]; ports?: Number[];
name?: string; name?: string;
workingDir?: string;
waitUntil?: 'not-running' | 'next-exit' | 'removed'; waitUntil?: 'not-running' | 'next-exit' | 'removed';
} }
@ -77,6 +78,7 @@ export async function launchContainer(options: LaunchContainerOptions): Promise<
} }
const container = await postJSON(`/containers/create` + (options.name ? '?name=' + options.name : ''), { const container = await postJSON(`/containers/create` + (options.name ? '?name=' + options.name : ''), {
Cmd: options.command, Cmd: options.command,
WorkingDir: options.workingDir,
Labels: options.labels ?? {}, Labels: options.labels ?? {},
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
@ -134,7 +136,8 @@ interface CommitContainerOptions {
containerId: string, containerId: string,
repo: string, repo: string,
tag: string, tag: string,
entrypoint?: string, entrypoint?: string[],
workingDir?: string,
env?: {[key: string]: string | number | boolean | undefined}, env?: {[key: string]: string | number | boolean | undefined},
} }
@ -143,7 +146,8 @@ export async function commitContainer(options: CommitContainerOptions) {
for (const [key, value] of Object.entries(options.env ?? {})) for (const [key, value] of Object.entries(options.env ?? {}))
Env.push(`${key}=${value}`); Env.push(`${key}=${value}`);
await postJSON(`/commit?container=${options.containerId}&repo=${options.repo}&tag=${options.tag}`, { await postJSON(`/commit?container=${options.containerId}&repo=${options.repo}&tag=${options.tag}`, {
Entrypoint: options.entrypoint ?? '', Entrypoint: options.entrypoint,
WorkingDir: options.workingDir,
Env, Env,
}); });
} }

View file

@ -301,7 +301,7 @@ copyFiles.push({
// Babel doesn't touch JS files, so copy them manually. // Babel doesn't touch JS files, so copy them manually.
// For example: diff_match_patch.js // For example: diff_match_patch.js
copyFiles.push({ copyFiles.push({
files: 'packages/playwright-core/src/**/*.(js|sh)', files: 'packages/playwright-core/src/**/*.js',
from: 'packages/playwright-core/src', from: 'packages/playwright-core/src',
to: 'packages/playwright-core/lib', to: 'packages/playwright-core/lib',
ignored: ['**/.eslintrc.js', '**/webpack*.config.js', '**/injected/**/*'] ignored: ['**/.eslintrc.js', '**/webpack*.config.js', '**/injected/**/*']