"""Exercise a real previous installation inside a disposable rootless container. Requires podman, the old tarball argument and the newly built amd64 release. """ import http.cookies import json from pathlib import Path import secrets import subprocess import sys import tarfile import tempfile import time import urllib.request project = Path(__file__).resolve().parent.parent name = 'bluebird-upgrade-' + secrets.token_hex(4) password = 'upgrade-preserved-password' base = 'http://127.0.0.1:18996' origin = 'https://files.example.test' def run(*args, **kw): return subprocess.run(args, check=True, capture_output=True, text=True, **kw).stdout def inside(*args, **kw): return run('podman', 'exec', name, *args, **kw) with tempfile.TemporaryDirectory(prefix='bluebird-upgrade-old-') as old: with tarfile.open(sys.argv[1]) as archive: archive.extractall(old, filter='data') try: run('podman', 'run', '--rm', '-d', '--security-opt', 'label=disable', '--name', name, '-p', '127.0.0.1:18996:18996', '-v', old+':/oldpkg:ro', '-v', str(project/'build/linux-amd64')+':/pkg:ro', 'docker.io/library/debian:bookworm-slim', 'sleep', '300') inside('env', 'BLUEBIRD_ADMIN_PASSWORD='+password, '/oldpkg/install.sh', '--url', origin, '--listen', '0.0.0.0:18996', '--encrypt', '--no-start') inside('sed','-i','s/min_free_bytes = 1073741824/min_free_bytes = 0/', '/opt/bluebird/config.toml') key_before = inside('sha256sum', '/opt/bluebird/var/secrets/age.key') cookies = {} def call(method, path, body=None): data = body if isinstance(body, bytes) else json.dumps(body).encode() if body is not None else None req = urllib.request.Request(base+'/api/v1'+path, data=data, method=method, headers={ 'Origin': origin, 'X-Bluebird-Origin': origin, 'Cookie': '; '.join(k+'='+v for k,v in cookies.items()), 'Content-Type': 'application/octet-stream' if isinstance(body,bytes) else 'application/json'}) with urllib.request.urlopen(req, timeout=10) as response: for line in response.headers.get_all('Set-Cookie',[]): parsed = http.cookies.SimpleCookie(); parsed.load(line) cookies.update({k:v.value for k,v in parsed.items()}) result=response.read() return result if path.endswith('/download') else json.loads(result) def start(): subprocess.run(['podman','exec','-d',name,'runuser','-u','bluebird','--','/opt/bluebird/bluebird','serve','--config','/opt/bluebird/config.toml'],check=True) for _ in range(50): try: if call('GET','/health')['ready']: return except OSError: time.sleep(.1) raise AssertionError('Container service not ready') start() call('GET','/policy') parcel=call('POST','/uploads',{'files':[{'name':'preserved.txt','size':21}],'days':7,'downloads':5,'renew':True}) call('PUT',f"/uploads/{parcel['id']}/files/{parcel['files'][0]['id']}", b'old encrypted payload') share=call('POST',f"/uploads/{parcel['id']}/publish",{}) call('POST','/admin/login',{'username':'admin','password':password}) before=inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird') # Invalid candidates must not stop a running service or change files. bad=subprocess.run(['podman','exec',name,'/pkg/upgrade.sh','--access-url','http://bad.test/path','--no-start'],capture_output=True) assert bad.returncode != 0 assert before == inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird') assert call('GET','/health')['ready'] # No pgrep dependency: /proc/comm identifies only the native service. inside('sh','-c','for p in /proc/[0-9]*; do [ "$(cat "$p/comm" 2>/dev/null)" != bluebird ] || kill -TERM "${p##*/}"; done') for _ in range(50): try: call('GET','/health') except OSError: break time.sleep(.1) # Simulate a new-version integrity-check failure after files were replaced. inside('sh','-c','mkdir /failpkg; cp /pkg/bluebird /failpkg/bluebird.real; cp /pkg/ARCH /pkg/upgrade.sh /failpkg/') wrapper='#!/bin/sh\nif [ "$1" = check ]; then exit 1; fi\nexec "$(dirname "$0")/bluebird.real" "$@"\n' inside('sh','-c','printf "%s" "$1" > /failpkg/bluebird; chmod 755 /failpkg/bluebird','wrapper',wrapper) failed=subprocess.run(['podman','exec',name,'/failpkg/upgrade.sh','--lan-url',base,'--listen','0.0.0.0:18996','--no-start'],capture_output=True) assert failed.returncode != 0 and '已恢复旧程序和配置' in failed.stderr.decode() assert before == inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird') result=inside('/pkg/upgrade.sh','--lan-url',base,'--listen','0.0.0.0:18996', '--access-url','http://files.example.test','--no-start') assert '升级完成' in result assert key_before == inside('sha256sum','/opt/bluebird/var/secrets/age.key') assert inside('stat','-c','%U:%G:%a','/opt/bluebird/config.toml').strip() == 'root:bluebird:640' assert 'data.tar.gz' in inside('find','/opt/bluebird/backups','-type','f') start();cookies.clear() call('POST','/admin/login',{'username':'admin','password':password}) assert call('GET','/admin/stats')['objects'] == 1 policy=call('GET','/policy');assert set(policy['access_urls']) == {origin,base,'http://files.example.test'} for entrance in [origin,base,'http://files.example.test']: origin=entrance;cookies.clear() call('POST','/pickup/resolve',{'code':share['code']}) assert call('GET',f"/pickup/files/{parcel['files'][0]['id']}/download") == b'old encrypted payload' print('升级验证通过:旧管理员密码、加密文件、密钥及取件码保留;三入口下载、权限、备份、非法配置保护及检查失败回滚通过。') except subprocess.CalledProcessError as error: print(error.stdout, error.stderr, file=sys.stderr) raise finally: subprocess.run(['podman','rm','-f',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)