107 lines
7.7 KiB
PHP
107 lines
7.7 KiB
PHP
<?php
|
|
|
|
use Dotenv\Dotenv;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
require dirname(__DIR__).'/vendor/autoload.php';
|
|
$app = require dirname(__DIR__).'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
$root = dirname(__DIR__);
|
|
$host = realpath($root.'/../sgf-tempo');
|
|
$config = DB::connection('pgsql')->getConfig();
|
|
if ($config['host'] !== '127.0.0.1') {
|
|
throw new RuntimeException('Local isolated cluster required.');
|
|
}
|
|
config(['database.connections.fixed_admin' => [...$config, 'url' => null, 'database' => 'postgres']]);
|
|
$admin = DB::connection('fixed_admin');
|
|
$suffix = bin2hex(random_bytes(6));
|
|
$owner = 'fix_owner_'.$suffix;
|
|
$runtime = 'fix_app_'.$suffix;
|
|
$database = 'sgf_reproduction_'.$suffix;
|
|
$ownerPassword = bin2hex(random_bytes(32));
|
|
$runtimePassword = bin2hex(random_bytes(32));
|
|
$roles = [];
|
|
$created = false;
|
|
$restoreCreated = false;
|
|
$restoreDatabase = 'sgf_reproduction_restore_'.$suffix;
|
|
$exit = 1;
|
|
$work = sys_get_temp_dir().'/sgf-corrected-'.$suffix;
|
|
mkdir($work, 0700);
|
|
$pointer = file_get_contents($host.'/packages/purchasing/current.json');
|
|
try {
|
|
$sourceSettings = Dotenv::parse(file_get_contents($host.'/.env'));
|
|
$sourceEnvironment = array_intersect_key($sourceSettings, array_flip(['DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'APP_ENV']));
|
|
$sourceEnvironment += ['APP_BASE_PATH' => $host, 'DB_URL' => $sourceSettings['DB_URL'] ?? '', 'APP_CONFIG_CACHE' => $host.'/bootstrap/cache/config.php'];
|
|
$export = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-export.php'], $host, [...$sourceEnvironment, 'FIX_SOURCE_HOST' => $host, 'FIX_SCHEMA_FILE' => $work.'/source-schema.dump'], null, 90);
|
|
$export->mustRun();
|
|
$inventory = json_decode($export->getOutput(), true, 64, JSON_THROW_ON_ERROR);
|
|
if ($inventory['database'] !== $sourceSettings['DB_DATABASE']) {
|
|
throw new RuntimeException('Wrong reproduction source.');
|
|
}
|
|
file_put_contents($work.'/baseline.json', json_encode($inventory, JSON_THROW_ON_ERROR));
|
|
$prepare = new Process([PHP_BINARY, $root.'/scripts/prepare-sgf-operational.php', $host], $root);
|
|
$prepare->mustRun();
|
|
$artifact = json_decode($prepare->getOutput(), true, 16, JSON_THROW_ON_ERROR);
|
|
require $root.'/tests/Support/sgf-update-fixture.php';
|
|
$manifest = json_decode(file_get_contents($artifact['directory'].'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
|
|
$clone = updateFixture($host, $manifest, true);
|
|
foreach ([$owner => $ownerPassword, $runtime => $runtimePassword] as $role => $password) {
|
|
$admin->statement('CREATE ROLE "'.$role.'" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS PASSWORD '.$admin->getPdo()->quote($password));
|
|
$roles[] = $role;
|
|
}
|
|
$admin->statement('CREATE DATABASE "'.$database.'" OWNER "'.$owner.'"');
|
|
$created = true;
|
|
$env = ['APP_ENV' => 'production', 'APP_DEBUG' => 'false', 'APP_BASE_PATH' => $clone, 'APP_KEY' => 'base64:'.base64_encode(random_bytes(32)), 'DB_CONNECTION' => 'pgsql', 'DB_URL' => '', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => (string) $config['port'], 'DB_DATABASE' => $database, 'DB_USERNAME' => $owner, 'DB_PASSWORD' => $ownerPassword, 'SESSION_DRIVER' => 'file', 'SESSION_FILES' => $clone.'/storage/framework/sessions', 'SESSION_COOKIE' => 'corrected_'.$suffix, 'CACHE_STORE' => 'array', 'QUEUE_CONNECTION' => 'sync',
|
|
'FIX_SOURCE_HOST' => $host, 'FIX_CLONE' => $clone, 'FIX_BUNDLE' => $artifact['directory'], 'FIX_ROOT' => $root, 'FIX_WORK' => $work, 'FIX_RUNTIME' => $runtime, 'FIX_RUNTIME_PASSWORD' => $runtimePassword];
|
|
$restore = new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe', '--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$database, $work.'/source-schema.dump'], null, ['PGHOST' => '127.0.0.1', 'PGPORT' => (string) $config['port'], 'PGUSER' => $owner, 'PGPASSWORD' => $ownerPassword], null, 60);
|
|
$restore->mustRun();
|
|
$proofProcess = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-proof.php'], $host, $env, null, 180);
|
|
$proofProcess->mustRun();
|
|
try {
|
|
$proof = json_decode($proofProcess->getOutput(), true, 64, JSON_THROW_ON_ERROR);
|
|
} catch (JsonException) {
|
|
file_put_contents($work.'/failure.txt', $proofProcess->getOutput().$proofProcess->getErrorOutput());
|
|
throw new RuntimeException('Private diagnostic '.$work.'/failure.txt');
|
|
}
|
|
file_put_contents($work.'/expected.json', json_encode($proof['data_fingerprints'], JSON_THROW_ON_ERROR));
|
|
$admin->statement('CREATE DATABASE "'.$restoreDatabase.'" OWNER "'.$owner.'"');
|
|
$restoreCreated = true;
|
|
$restoreData = new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe', '--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restoreDatabase, $work.'/installed.dump'], null, ['PGHOST' => '127.0.0.1', 'PGPORT' => (string) $config['port'], 'PGUSER' => $owner, 'PGPASSWORD' => $ownerPassword], null, 90);
|
|
$restoreData->mustRun();
|
|
$readRestore = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-restored.php'], $host, [...$env, 'DB_DATABASE' => $restoreDatabase], null, 60);
|
|
$readRestore->mustRun();
|
|
$proof['data_recovery'] = json_decode($readRestore->getOutput(), true, 32, JSON_THROW_ON_ERROR);
|
|
$after = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $root.'/tests/Support/sgf-corrected-export.php'], $host, [...$sourceEnvironment, 'FIX_SOURCE_HOST' => $host, 'FIX_CHECK_ONLY' => '1'], null, 60);
|
|
$after->mustRun();
|
|
$afterState = json_decode($after->getOutput(), true, 64, JSON_THROW_ON_ERROR);
|
|
if ($afterState['source_security_fingerprints'] !== $inventory['source_security_fingerprints'] || $afterState['tables'] !== $inventory['tables']) {
|
|
throw new RuntimeException('Destination changed during isolated proof; reconcile.');
|
|
}
|
|
$proof['destination_security_and_schema_preserved'] = true;
|
|
if (! $proof['passed'] || file_get_contents($host.'/packages/purchasing/current.json') !== $pointer) {
|
|
throw new RuntimeException('Proof or destination preservation failed.');
|
|
}
|
|
$proof += ['artifact' => $artifact, 'source_schema_tables' => count($inventory['tables']), 'source_rows_exported' => 0, 'source_schema_sha256' => $inventory['schema_sha256'], 'runtime_and_migrator_separate' => true, 'destination_version' => '0.1.1', 'destination_changed' => false, 'prior_proofs_reused' => true, 'checked_at' => gmdate('c')];
|
|
$exit = 0;
|
|
} catch (Throwable $error) {
|
|
fwrite(STDERR, $error->getMessage()."\n");
|
|
} finally {
|
|
if ($restoreCreated) {
|
|
$admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname=? and pid<>pg_backend_pid()', [$restoreDatabase]);
|
|
$admin->statement('DROP DATABASE "'.$restoreDatabase.'"');
|
|
}
|
|
if ($created) {
|
|
$admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname=? and pid<>pg_backend_pid()', [$database]);
|
|
$admin->statement('DROP DATABASE "'.$database.'"');
|
|
}foreach (array_reverse($roles) as $role) {
|
|
$admin->statement('DROP ROLE "'.$role.'"');
|
|
}
|
|
}
|
|
if ($exit === 0) {
|
|
$proof['test_database_and_roles_removed'] = true;
|
|
file_put_contents($root.'/docs/proofs/sgf-corrected-delivery.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
echo json_encode($proof,JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
|
|
}exit($exit);
|