Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/sandbox-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ When creation finishes, QuickInstall asks whether to start the board.
Open:

```text
http://localhost:8081/
http://localhost:8081/demo/
```

Admin login:
Expand Down
2 changes: 1 addition & 1 deletion src/QuickInstall/Sandbox/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ private function boardCreate(array $args): int
echo "Created board scaffold: $name\n";
echo "Compose: {$paths['compose']}\n";
echo "Install config: {$paths['install_config']}\n";
echo "URL after start: http://localhost:$port/\n";
echo "URL after start: {$created['board']['url']}\n";
if ($populate !== 'none')
{
echo "Populate preset: $populate (runs on board:start)\n";
Expand Down
2 changes: 1 addition & 1 deletion src/QuickInstall/Sandbox/BoardRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function start(string $name): void
throw new RuntimeException('SQLite boards support populate:none, tiny, or development only. Use mariadb, mysql, or postgres for heavier fixture presets.');
}
$this->seedIfNeeded($name, $board['populate'] ?? 'none');
$this->waitUntilHttpReady($name, $board['url'] ?? '');
$this->waitUntilHttpReady($name, $board['health_url'] ?? ($board['url'] ?? ''));
}

public function stop(string $name): void
Expand Down
14 changes: 12 additions & 2 deletions src/QuickInstall/Sandbox/BoardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,17 @@ public function create(string $name, string $version = 'latest', string $db = 'm
{
throw new RuntimeException("Unable to determine PHP runtime for source: {$source['source_key']}");
}

$config = [
'phpbb' => $source['version'],
'phpbb_source' => $source['source_key'],
'php' => $php,
'db' => $db,
'port' => $port,
'server_name' => 'localhost',
'script_path' => '/' . $name,
'cookie_path' => '/' . $name . '/',
'scoped_path' => true,
'postgres_data_subdir' => $db === 'postgres',
'populate' => $populate,
'debug' => $debug,
'admin_name' => 'admin',
Expand Down Expand Up @@ -121,7 +125,13 @@ public function create(string $name, string $version = 'latest', string $db = 'm
'php' => $php,
'db' => $db,
'port' => $port,
'url' => "http://localhost:$port/",
'server_name' => $config['server_name'],
'script_path' => $config['script_path'],
'cookie_path' => $config['cookie_path'],
'scoped_path' => $config['scoped_path'],
'postgres_data_subdir' => $config['postgres_data_subdir'],
'url' => "http://localhost:$port/$name/",
'health_url' => "http://127.0.0.1:$port/$name/",
'path' => $boardDir,
'populate' => $populate,
'debug' => $debug,
Expand Down
51 changes: 44 additions & 7 deletions src/QuickInstall/Sandbox/DockerComposeWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ public function write(string $name, array $config): array
$compose = $runtimeDir . '/compose.yml';
$dockerfile = $runtimeDir . '/Dockerfile';
$entrypoint = $runtimeDir . '/entrypoint.sh';
$apacheConfig = $runtimeDir . '/apache.conf';

$this->writeFile($installConfig, $this->installConfig($name, $config));
$this->writeFile($compose, $this->compose($name, $config));
$this->writeFile($dockerfile, $this->dockerfile($config));
$this->writeFile($entrypoint, $this->entrypoint($config));
$this->writeFile($apacheConfig, $this->apacheConfig($name, $config));
if ((PHP_OS_FAMILY !== 'Windows') && !chmod($entrypoint, 0755))
{
throw new RuntimeException("Unable to make entrypoint executable: $entrypoint");
Expand All @@ -50,6 +52,7 @@ public function write(string $name, array $config): array
'install_config' => $installConfig,
'dockerfile' => $dockerfile,
'entrypoint' => $entrypoint,
'apache_config' => $apacheConfig,
];
}

Expand All @@ -72,6 +75,8 @@ private function installConfig(string $name, array $config): string
$adminPass = $this->yamlString($config['admin_pass']);
$adminEmail = $this->yamlString($config['admin_email']);
$boardName = $this->yamlString($name);
$serverName = $this->yamlString($config['server_name'] ?? 'localhost');
$scriptPath = $this->yamlString($config['script_path'] ?? '/');

return <<<YAML
installer:
Expand Down Expand Up @@ -102,21 +107,23 @@ private function installConfig(string $name, array $config): string
cookie_secure: false
server_protocol: http://
force_server_vars: true
server_name: localhost
server_name: $serverName
server_port: {$config['port']}
script_path: /
script_path: $scriptPath
extensions: []

YAML;
}

private function compose(string $name, array $config): string
{
$dbService = $this->databaseService($config['db'], $name, $config['php']);
$dbService = $this->databaseService($config['db'], $name, $config['php'], !empty($config['postgres_data_subdir']));
$sourcePath = $this->project->sourcePath($config['phpbb_source'] ?? $config['phpbb']);
$boardPath = $this->project->boardPath($name);
$extensionVolumes = $this->extensionVolumes($config['extensions'] ?? []);
$styleVolumes = $this->styleVolumes($config['styles'] ?? []);
$cookiePath = $config['cookie_path'] ?? '/';
$rewriteBase = rtrim($config['script_path'] ?? '/', '/') . '/';
$dbPath = $this->project->workspacePath('db/' . $name);
if (!is_dir($dbPath) && !mkdir($dbPath, 0775, true) && !is_dir($dbPath))
{
Expand All @@ -135,7 +142,7 @@ private function compose(string $name, array $config): string
# Boards are intentionally unavailable to other network devices.
- "127.0.0.1:{$config['port']}:80"
volumes:
{$this->bindVolume($sourcePath, '/opt/phpbb-source', true)}{$this->bindVolume($boardPath, '/var/www/html')}{$extensionVolumes}{$styleVolumes}{$this->bindVolume('./install-config.yml', '/opt/quickinstall/install-config.yml', true)}{$this->bindVolume('./entrypoint.sh', '/opt/quickinstall/entrypoint.sh', true)}
{$this->bindVolume($sourcePath, '/opt/phpbb-source', true)}{$this->bindVolume($boardPath, '/var/www/html')}{$extensionVolumes}{$styleVolumes}{$this->bindVolume('./install-config.yml', '/opt/quickinstall/install-config.yml', true)}{$this->bindVolume('./entrypoint.sh', '/opt/quickinstall/entrypoint.sh', true)}{$this->bindVolume('./apache.conf', '/etc/apache2/conf-enabled/quickinstall.conf', true)}
entrypoint: ["/bin/sh", "/opt/quickinstall/entrypoint.sh"]
depends_on:
db:
Expand All @@ -144,6 +151,8 @@ private function compose(string $name, array $config): string
QUICKINSTALL_PHPBB_VERSION: "{$config['phpbb']}"
QUICKINSTALL_POPULATE: "{$config['populate']}"
QUICKINSTALL_BOARD_TIMEZONE: "{$config['board_timezone']}"
QUICKINSTALL_COOKIE_PATH: "$cookiePath"
QUICKINSTALL_REWRITE_BASE: "$rewriteBase"

$dbService

Expand Down Expand Up @@ -173,6 +182,27 @@ private function extensionVolumes(array $extensions): string
return $volumes;
}

/** Routes a scoped board URL to phpBB's unchanged document root. */
private function apacheConfig(string $name, array $config): string
{
$serverName = $config['server_name'] ?? 'localhost';
if (empty($config['scoped_path']))
{
return "ServerName $serverName\nLimitRequestFieldSize 65536\n";
}

$path = '/' . $name;
$pattern = preg_quote($path, '#');
return <<<APACHE
ServerName $serverName
LimitRequestFieldSize 65536
RedirectMatch 302 "^/$" "$path/"
RedirectMatch 302 "^$pattern$" "$path/"
Alias "$path/" "/var/www/html/"

APACHE;
}

private function styleVolumes(array $styles): string
{
$volumes = '';
Expand Down Expand Up @@ -234,6 +264,7 @@ private function dockerfile(array $config): string
&& apt-get install -y --no-install-recommends $packages \\
&& docker-php-ext-install mbstring zip \\
{$sodiumInstall} && $extensionInstall \\
&& a2enmod rewrite \\
&& for extension in $requiredExtensions; do php -m | grep -qi "^\${extension}$"; done \\
&& rm -rf /var/lib/apt/lists/*

Expand Down Expand Up @@ -274,9 +305,14 @@ private function entrypoint(array $config): string
rm -f /var/www/html/config.php
fi

if [ "${QUICKINSTALL_REWRITE_BASE:-/}" != "/" ] && [ -f /var/www/html/.htaccess ]; then
sed -i "s|^#RewriteBase /$|RewriteBase ${QUICKINSTALL_REWRITE_BASE}|" /var/www/html/.htaccess
fi

if [ ! -s /var/www/html/config.php ] && [ -f /var/www/html/install/phpbbcli.php ]; then
php /var/www/html/install/phpbbcli.php install /opt/quickinstall/install-config.yml
rm -rf /var/www/html/install
php /var/www/html/bin/phpbbcli.php config:set cookie_path "${QUICKINSTALL_COOKIE_PATH:-/}"
if php -r 'try { new DateTimeZone((string) getenv("QUICKINSTALL_BOARD_TIMEZONE")); } catch (Exception $e) { exit(1); }'; then
php /var/www/html/bin/phpbbcli.php config:set board_timezone "$QUICKINSTALL_BOARD_TIMEZONE"
else
Expand Down Expand Up @@ -327,7 +363,7 @@ private function shellString(string $value): string
return "'" . str_replace("'", "'\"'\"'", $value) . "'";
}

private function databaseService(string $db, string $name, string $phpVersion): string
private function databaseService(string $db, string $name, string $phpVersion, bool $postgresDataSubdir): string
{
$dbPath = '../../db/' . $name;
switch ($db)
Expand All @@ -337,17 +373,18 @@ private function databaseService(string $db, string $name, string $phpVersion):
$command = version_compare($phpVersion, '7.4.4', '<') ? " command: [\"--default-authentication-plugin=mysql_native_password\"]\n" : '';
break;
case 'postgres':
$pgdata = $postgresDataSubdir ? " PGDATA: \"/var/lib/postgresql/data/pgdata\"\n" : '';
return <<<YAML
db:
image: postgres:16
environment:
POSTGRES_DB: phpbb
POSTGRES_USER: phpbb
POSTGRES_PASSWORD: phpbb
volumes:
{$pgdata} volumes:
{$this->bindVolume($dbPath, '/var/lib/postgresql/data')}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U phpbb -d phpbb"]
test: ["CMD-SHELL", "psql -U phpbb -d phpbb -tAc 'SELECT 1' 2>/dev/null | grep -qx 1"]
interval: 5s
timeout: 5s
retries: 20
Expand Down
1 change: 1 addition & 0 deletions tests/Integration/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ public function testBoardCreateRepeatsStartPromptUntilYesOrNo(): void
$result = $this->runApplication($root, ['qi', 'board:create', 'demo', '--phpbb', '3.3.14', '--port', (string) $this->availablePort()], "maybe\nn\n");

self::assertSame(0, $result['exit_code']);
self::assertMatchesRegularExpression('#URL after start: http://localhost:\d+/demo/#', $result['output']);
self::assertStringContainsString('Please answer y or n.', $result['output']);
self::assertSame(2, substr_count($result['output'], 'Run this command now? [Y/n]: '));
}
Expand Down
13 changes: 13 additions & 0 deletions tests/Unit/BoardRunnerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ public function testStartAllowsSqliteDevelopmentSeededBoards(): void
self::assertSame([['demo', 'development']], $runner->seedIfNeededRuns);
}

public function testStartUsesLoopbackHealthUrlWhenAvailable(): void
{
[$project] = $this->projectWithBoard([
'url' => 'http://demo.localhost:8080/',
'health_url' => 'http://127.0.0.1:8080/',
]);
$runner = new TestBoardRunner($project);

$runner->start('demo');

self::assertSame([['demo', 'http://127.0.0.1:8080/']], $runner->httpWaits);
}

public function testStartRejectsHeavySqliteSeededBoards(): void
{
[$project] = $this->projectWithBoard([
Expand Down
7 changes: 7 additions & 0 deletions tests/Unit/BoardServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ public function testCreateWritesBoardAndRuntimeConfig(): void
self::assertSame('demo', $result['board']['name']);
self::assertSame('postgres', $result['board']['db']);
self::assertSame(8090, $result['board']['port']);
self::assertSame('localhost', $result['board']['server_name']);
self::assertSame('/demo', $result['board']['script_path']);
self::assertSame('/demo/', $result['board']['cookie_path']);
self::assertTrue($result['board']['scoped_path']);
self::assertTrue($result['board']['postgres_data_subdir']);
self::assertSame('http://localhost:8090/demo/', $result['board']['url']);
self::assertSame('http://127.0.0.1:8090/demo/', $result['board']['health_url']);
self::assertSame('tiny', $result['board']['populate']);
self::assertTrue($result['board']['debug']);
self::assertSame('America/Los_Angeles', $result['board']['board_timezone']);
Expand Down
61 changes: 61 additions & 0 deletions tests/Unit/DockerComposeWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@ public function testWritesDatabaseRuntimeFiles(string $board, string $db, array
self::assertFileExists($paths['install_config']);
self::assertFileExists($paths['dockerfile']);
self::assertFileExists($paths['entrypoint']);
self::assertFileExists($paths['apache_config']);
$entrypoint = file_get_contents($paths['entrypoint']);
self::assertStringContainsString('apache2-foreground', $entrypoint);
self::assertStringContainsString('new DateTimeZone((string) getenv("QUICKINSTALL_BOARD_TIMEZONE"))', $entrypoint);
self::assertStringContainsString('config:set board_timezone "$QUICKINSTALL_BOARD_TIMEZONE"', $entrypoint);
self::assertStringContainsString('config:set board_timezone UTC', $entrypoint);
self::assertStringContainsString('config:set cookie_path "${QUICKINSTALL_COOKIE_PATH:-/}"', $entrypoint);
self::assertStringContainsString('RewriteBase ${QUICKINSTALL_REWRITE_BASE}', $entrypoint);
self::assertStringContainsString("is unsupported by this PHP runtime; using UTC.", $entrypoint);
self::assertStringContainsString('QUICKINSTALL_BOARD_TIMEZONE: "America/Los_Angeles"', file_get_contents($paths['compose']));
self::assertStringContainsString('QUICKINSTALL_COOKIE_PATH: "/demo/"', file_get_contents($paths['compose']));
self::assertStringContainsString('QUICKINSTALL_REWRITE_BASE: "/demo/"', file_get_contents($paths['compose']));
self::assertStringNotContainsString("\t", file_get_contents($paths['compose']));
self::assertStringContainsString('a2enmod rewrite', file_get_contents($paths['dockerfile']));

$output = file_get_contents($paths['compose']) . "\n" . file_get_contents($paths['install_config']) . "\n" . file_get_contents($paths['dockerfile']);
foreach ($expectedContains as $expected)
Expand Down Expand Up @@ -68,6 +75,8 @@ public function databaseRuntimeProvider(): array
'mysql',
[
'image: mysql:8.0',
'server_name: "localhost"',
'script_path: "/demo"',
'server_port: 8081',
'dbms: mysqli',
'docker-php-ext-install mysqli pdo_mysql',
Expand All @@ -89,6 +98,8 @@ public function databaseRuntimeProvider(): array
'postgres',
[
'image: postgres:16',
'PGDATA: "/var/lib/postgresql/data/pgdata"',
"psql -U phpbb -d phpbb -tAc 'SELECT 1'",
'dbms: postgres',
'docker-php-ext-install pgsql pdo_pgsql',
],
Expand Down Expand Up @@ -157,6 +168,51 @@ public function testQuotesYamlSignificantInstallerValues(): void
self::assertStringContainsString(' name: "demo"', $installConfig);
}

public function testExistingBoardConfigWithoutServerNameKeepsLocalhost(): void
{
$config = $this->config();
unset($config['server_name']);
unset($config['script_path'], $config['scoped_path']);

$root = $this->createTempProjectRoot();
$project = new Project($root);
$project->init();
mkdir($project->boardPath('demo'), 0775, true);
$paths = (new DockerComposeWriter($project))->write('demo', $config);

self::assertStringContainsString('server_name: "localhost"', file_get_contents($paths['install_config']));
self::assertStringContainsString('script_path: "/"', file_get_contents($paths['install_config']));
self::assertStringNotContainsString('Alias ', file_get_contents($paths['apache_config']));
}

public function testExistingPostgresConfigKeepsRootDataDirectory(): void
{
$config = $this->config(['db' => 'postgres']);
unset($config['postgres_data_subdir']);

$root = $this->createTempProjectRoot();
$project = new Project($root);
$project->init();
mkdir($project->boardPath('demo'), 0775, true);
$paths = (new DockerComposeWriter($project))->write('demo', $config);
$compose = file_get_contents($paths['compose']);

self::assertStringNotContainsString('PGDATA:', $compose);
self::assertStringContainsString("psql -U phpbb -d phpbb -tAc 'SELECT 1'", $compose);
}

public function testScopedBoardWritesApacheRoute(): void
{
[, $paths] = $this->writeBoard('demo');
$apacheConfig = file_get_contents($paths['apache_config']);

self::assertStringContainsString('RedirectMatch 302 "^/$" "/demo/"', $apacheConfig);
self::assertStringContainsString('RedirectMatch 302 "^/demo$" "/demo/"', $apacheConfig);
self::assertStringContainsString('Alias "/demo/" "/var/www/html/"', $apacheConfig);
self::assertStringContainsString('LimitRequestFieldSize 65536', $apacheConfig);
self::assertStringContainsString('target: "/etc/apache2/conf-enabled/quickinstall.conf"', file_get_contents($paths['compose']));
}

public function testEntrypointExcludesBindMountsFromOwnershipChanges(): void
{
[, $paths] = $this->writeBoard('demo', [
Expand Down Expand Up @@ -204,6 +260,11 @@ private function config(array $overrides = []): array
'php' => '8.1',
'db' => 'mariadb',
'port' => 8081,
'server_name' => 'localhost',
'script_path' => '/demo',
'cookie_path' => '/demo/',
'scoped_path' => true,
'postgres_data_subdir' => true,
'populate' => 'none',
'admin_name' => 'admin',
'admin_pass' => 'password',
Expand Down