| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 |
- """Switch only the app after verified preparation; retain an immediate image rollback."""
- import json
- import os
- import subprocess
- import time
- from pathlib import Path
- os.umask(0o077)
- root=Path('/opt/chengchuan-relay')
- release=root/'releases/optional-controls-20260915'
- deploy=release/'source/deploy'
- evidence=root/'evidence/optional-controls-20260915'
- backup=root/'backups/before-optional-controls-20260915'
- assert (evidence/'build.exit').read_text().strip()=='0'
- assert json.loads((evidence/'stage-http.json').read_text())['passed']
- assert json.loads((evidence/'stage-migration.json').read_text())['historical_tests_preserved']
- def sql(statement):
- return subprocess.check_output(['docker','exec','chengchuan-relay-postgres-1','psql','-U','relay','-d','relay','-At','-v','ON_ERROR_STOP=1','-c',statement]).decode().strip()
- def configuration_hash():
- return sql("SELECT md5(COALESCE(string_agg(jsonb_build_array(id,concurrency,proxy_id,extra)::text,E'\\n' ORDER BY id),'')) FROM accounts")
- def inspect(name):
- return json.loads(subprocess.check_output(['docker','inspect',name]))[0]
- old=inspect('chengchuan-relay-app-1')
- dependencies={name:inspect(name)['Id'] for name in ['chengchuan-relay-postgres-1','chengchuan-relay-redis-1']}
- assert old['Image']=='sha256:3140765d60ec62efad35a922cbc5a1fdc4de85ea77e47b8b6ebd217dc07c50cd'
- config_before=configuration_hash()
- started=time.time()
- subprocess.run(['docker','stop','--time','60','chengchuan-relay-app-1'],check=True,stdout=subprocess.DEVNULL)
- try:
- with (backup/'database-at-cutover.dump').open('xb') as output:
- subprocess.run(['docker','exec','chengchuan-relay-postgres-1','pg_dump','-U','relay','-d','relay','-Fc'],stdout=output,check=True)
- command=['docker','compose','--env-file','.env','-f','compose.relay.yml','up','-d','--no-deps','--no-build','--wait','--wait-timeout','180','app']
- with (evidence/'cutover.log').open('w') as log:
- subprocess.run(command,cwd=deploy,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=220)
- with (evidence/'production-http.log').open('w') as log:
- subprocess.run(['python3',str(release/'check-release.py'),'--base-url','https://juhe.pub','--env',str(deploy/'.env'),'--output',str(evidence/'production-http.json'),'--db-container','chengchuan-relay-postgres-1'],stdout=log,stderr=subprocess.STDOUT,check=True,timeout=180)
- current=inspect('chengchuan-relay-app-1')
- config_after=configuration_hash()
- assert config_before==config_after,'account configuration changed during cutover; investigate before accepting'
- assert all(inspect(name)['Id']==cid for name,cid in dependencies.items())
- result={'version':'optional-controls-20260915','image':current['Image'],'healthy':current['State']['Health']['Status']=='healthy',
- 'account_configuration_preserved':config_before==config_after,'postgres_and_redis_not_recreated':True,
- 'migration_applied':sql("SELECT count(*) FROM schema_migrations WHERE filename='248_intelligent_assessment_v2.sql'")=='1',
- 'strict_rpm_enabled_accounts':int(sql("SELECT count(*) FROM accounts WHERE extra->'account_traffic_control'->>'strict_rpm_enabled'='true'")),
- 'adaptive_enabled_accounts':int(sql("SELECT count(*) FROM accounts WHERE extra->'account_traffic_control'->>'adaptive_enabled'='true'")),
- 'elapsed_seconds':round(time.time()-started,1),'url':'https://juhe.pub'}
- assert result['healthy'] and result['migration_applied']
- (evidence/'activation.json').write_text(json.dumps(result,indent=2))
- print(json.dumps(result,indent=2))
- except Exception as error:
- rollback_env=backup/'rollback.env'
- lines=(backup/'app.env').read_text().splitlines()
- lines=[line for line in lines if not line.startswith('RELAY_VERSION=')]
- lines.append('RELAY_VERSION=rollback-optional-controls-20260915')
- rollback_env.write_text('\n'.join(lines)+'\n')
- with (evidence/'rollback.log').open('w') as log:
- subprocess.run(['docker','compose','--env-file',str(rollback_env),'-f',str(backup/'compose.relay.yml'),'up','-d','--no-deps','--no-build','--wait','--wait-timeout','180','app'],cwd=backup,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=220)
- print('APPLICATION_ROLLED_BACK: '+type(error).__name__)
- raise
|