76 lines
6.9 KiB
PHP
76 lines
6.9 KiB
PHP
<?php
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\Process\Process;
|
|
use Tempo\Purchasing\Delivery\CodeDeployment;
|
|
|
|
$root=dirname(__DIR__);
|
|
$host='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/development-app';
|
|
$recovery=dirname($host).'/private/read-correction-recovery';
|
|
$candidate=json_decode(file_get_contents($root.'/docs/proofs/sgf-read-candidate.json'),true,32,JSON_THROW_ON_ERROR);
|
|
require $host.'/vendor/autoload.php';
|
|
$app=require $host.'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
$db=DB::connection();
|
|
$identity=$db->selectOne("select current_database() as db,current_user as role,inet_server_port() as port,current_setting('cluster_name') as cluster");
|
|
if(str_replace('\\','/',$app->basePath())!==$host||$identity->db!=='sgf_modules_dev_2576427d1939'||$identity->role!=='sgf_modules_qa_2576427d1939'||(int)$identity->port!==55439||$identity->cluster!=='sgf_modular_regression_2576427d1939'||config('app.url')!=='http://127.0.0.1:8039')throw new RuntimeException('Wrong demonstration');
|
|
$record=$db->table('ir_requests')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1])->first();
|
|
if(!$record||$record->status!=='approved'||$record->total_minor!==10000||$record->version!==3||$db->table('cb_budget_entries')->where('kind','reserve')->count()!==1)throw new RuntimeException('Wrong existing request');
|
|
require_once $root.'/integrations/sgf/operational/CodeDeployment.php';
|
|
$deploy=new CodeDeployment($host);
|
|
$fingerprints=function()use($db){
|
|
return $db->transaction(function()use($db){
|
|
$db->statement('SET TRANSACTION READ ONLY');$hashes=[];
|
|
foreach($db->select("select tablename from pg_tables where schemaname='public' order by tablename") as $t){
|
|
$rows=$db->select('select to_jsonb(t)::text as data from "'.$t->tablename.'" t order by to_jsonb(t)::text');
|
|
$hashes[$t->tablename]=hash('sha256',json_encode(array_column($rows,'data'),JSON_THROW_ON_ERROR));
|
|
}return $hashes;
|
|
});
|
|
};
|
|
$action=$argv[1]??'';
|
|
if($action==='prepare'){
|
|
if(is_dir($recovery))throw new RuntimeException('Recovery exists; no overwrite');
|
|
$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);
|
|
mkdir($recovery,0700,true);
|
|
$cfg=$db->getConfig();
|
|
(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','--format=custom','--file='.$recovery.'/before.dump','--dbname='.$identity->db],null,['PGHOST'=>'127.0.0.1','PGPORT'=>'55439','PGUSER'=>$identity->role,'PGPASSWORD'=>$cfg['password']],null,90))->mustRun();
|
|
(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','--list',$recovery.'/before.dump']))->mustRun();
|
|
$baseline=['identity'=>$identity,'plan'=>$plan,'data_fingerprints'=>$fingerprints(),'backup_sha256'=>hash_file('sha256',$recovery.'/before.dump'),'pointer_sha256'=>hash_file('sha256',$host.'/packages/purchasing/current.json'),'config_hashes'=>[]];
|
|
foreach(['.env','config/modules.php','config/purchasing-operational.php','config/purchasing-internal.php','config/purchasing-access.php'] as $file)$baseline['config_hashes'][$file]=hash_file('sha256',$host.'/'.$file);
|
|
file_put_contents($recovery.'/baseline.json',json_encode($baseline,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
|
echo json_encode(['prepared'=>true,'tables'=>count($baseline['data_fingerprints']),'backup_sha256'=>$baseline['backup_sha256'],'plan'=>$plan],JSON_PRETTY_PRINT);
|
|
}else{
|
|
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);
|
|
$currentFingerprints=$fingerprints();
|
|
$changedTables=array_keys(array_filter($currentFingerprints,fn($hash,$table)=>$hash!==($baseline['data_fingerprints'][$table]??null),ARRAY_FILTER_USE_BOTH));
|
|
$auditAppended=false;
|
|
if(in_array('identity_audit',$changedTables,true)){
|
|
$extract=new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','--data-only','--table=identity_audit','--file=-',$recovery.'/before.dump']);$extract->mustRun();
|
|
if(!preg_match('~COPY public\.identity_audit \(id,[^)]*\) FROM stdin;\r?\n(.*?)\r?\n\\\\\.~s',$extract->getOutput(),$match))throw new RuntimeException('Audit baseline cannot be reconciled');
|
|
$ids=array_map(fn($line)=>(int)explode("\t",$line)[0],preg_split('/\r?\n/',$match[1]));
|
|
$rows=$db->select('select to_jsonb(t)::text as data from identity_audit t where id<=? order by to_jsonb(t)::text',[max($ids)]);
|
|
if(hash('sha256',json_encode(array_column($rows,'data'),JSON_THROW_ON_ERROR))!==$baseline['data_fingerprints']['identity_audit'])throw new RuntimeException('Existing audit modified');
|
|
$auditAppended=true;$changedTables=array_values(array_diff($changedTables,['identity_audit']));
|
|
}
|
|
if($changedTables!==[])throw new RuntimeException('Database rows changed: '.implode(', ',$changedTables));
|
|
if(hash_file('sha256',$host.'/packages/purchasing/current.json')!==$baseline['pointer_sha256'])throw new RuntimeException('Pointer changed');
|
|
foreach($baseline['config_hashes'] as $file=>$hash)if(hash_file('sha256',$host.'/'.$file)!==$hash)throw new RuntimeException('Configuration changed');
|
|
if($action==='verify'){
|
|
$report=['passed'=>true,'identity'=>$identity,'data_tables_reconciled'=>count($baseline['data_fingerprints']),'audit_appended_only'=>$auditAppended,'request_id'=>1,'request_version'=>3,'amount_minor'=>10000,'reservation_count'=>1,'rights_and_accounts_unchanged'=>true,'config_unchanged'=>true,'pointer_unchanged'=>true,'candidate_manifest_sha256'=>hash_file('sha256',$host.'/packages/internal-requisitions/manifest.json'),'backup_sha256'=>$baseline['backup_sha256'],'checked_at'=>gmdate('c')];
|
|
file_put_contents($root.'/docs/proofs/sgf-read-8039-data.json',json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($report,JSON_PRETTY_PRINT);
|
|
}else{
|
|
if(is_file($host.'/storage/framework/down'))throw new RuntimeException('Unexpected maintenance');
|
|
file_put_contents($host.'/storage/framework/down',json_encode(['time'=>time(),'retry'=>60]));
|
|
if($action==='rollback'){
|
|
$applied=json_decode(file_get_contents($recovery.'/applied.json'),true);
|
|
$result=$deploy->rollback($applied['deployment_id'],'Authorised read fix recovery proof 8039',true);
|
|
file_put_contents($recovery.'/rollback.json',json_encode($result,JSON_PRETTY_PRINT));
|
|
}elseif(in_array($action,['apply','reapply'],true)){
|
|
$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);
|
|
$result=$deploy->apply($candidate['directory'],$candidate['archive']['manifest_sha256'],$plan['target_sha256'],'Authorised read-only visibility correction 8039',true);
|
|
file_put_contents($recovery.'/'.($action==='apply'?'applied':'reapplied').'.json',json_encode($result,JSON_PRETTY_PRINT));
|
|
}else throw new RuntimeException('Use prepare/apply/rollback/reapply/verify');
|
|
unlink($host.'/storage/framework/down');echo json_encode($result,JSON_PRETTY_PRINT);
|
|
}
|
|
}
|