Files
FEDEO/scripts/update/tests/host_storage_integration.py

156 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""Real migration -> adoption -> two updates, including a backup restore, on synthetic data.
Requires local storage test images and registry:2; never contacts a FEDEO instance.
Release HTTP/health responses are fixtures; Compose, registry, SDK, PostgreSQL,
backup, restore and provider validation use real processes and containers.
"""
import copy
import importlib.util
import json
from pathlib import Path
import subprocess
import tarfile
import tempfile
ROOT = Path(__file__).resolve().parents[3]
spec = importlib.util.spec_from_file_location('storage_fixture', ROOT / 'scripts/storage/tests/host_integration.py')
fixture = importlib.util.module_from_spec(spec)
spec.loader.exec_module(fixture)
m, docker = fixture.m, fixture.docker
u = m.u
class Http:
version = '3.4.0'
def json(self, url):
return {'status': 'ok', 'product': 'FEDEO', 'version': self.version, 'revision': 'a' * 40}
class Releases:
def __init__(self, original, images):
self.original, self.images = original, images
def download(self, version, destination):
config = copy.deepcopy(self.original)
for name, image in self.images.items():
config['services'][name]['image'] = image['repository'] + ':fixture'
config['services']['frontend']['entrypoint'] = ['node']
config['services']['frontend']['command'] = ['-e', 'setInterval(()=>{},1000)']
for name in ('traefik', 'node-exporter'):
config['services'][name]['image'] = json.loads(docker('image', 'inspect', 'node:22-alpine'))[0]['Id']
config['services']['backend']['environment']['FEDEO_VERSION'] = version
path = destination / 'fixture.json'
u.write_json(path, m.literal(config))
rendered = docker('compose', '-f', path, 'config', '--format', 'yaml')
(destination / 'docker-compose.selfhost.yml').write_text(rendered)
for name in ('.env.example', 'selfhost-setup.sh', 'CHANGELOG.md'):
(destination / name).write_text('# Synthetic update fixture\n')
manifest = {'schemaVersion': 1, 'version': version, 'commit': 'a' * 40,
'images': list(self.images.values()), 'migrations': []}
u.write_json(destination / 'release-manifest.json', manifest)
return manifest
def run():
root, project = fixture.fixture()
registry = project + '-registry'
registry_id = None
work = Path(tempfile.mkdtemp(prefix='fedeo-storage-update-images-'))
try:
config = m.resolved(u.read_json(root / 'docker-compose.yml'))
config['services']['backend']['environment'].update(ENCRYPTION_KEY='fixture-key', COOKIE_SECRET='fixture-$-cookie', JWT_SECRET='fixture-jwt')
config['services']['frontend']['environment'] = {'NUXT_PUBLIC_API_BASE': 'https://fixture.example.test/backend'}
u.write_json(root / 'docker-compose.yml', m.literal(config))
docker('compose', '-p', project, '-f', root / 'docker-compose.yml', 'up', '-d', '--pull', 'never', '--wait', '--wait-timeout', '90')
fixture.seed(root, project)
# Include the stopped Matrix orphan seen on the first real instance.
(root / 'matrix/postgres').mkdir(parents=True)
(root / 'matrix/postgres/retained.txt').write_text('retired Matrix')
retained = docker('create', '--pull', 'never', '--restart=no', '--network', project + '-internal',
'--label', 'com.docker.compose.project=' + project,
'--label', 'com.docker.compose.service=matrix-db',
'-v', str(root / 'matrix/postgres') + ':/var/lib/postgresql/data', 'node:22-alpine')
migration = m.Migration(root, 'fedeo-storage-tools:pilot')
with migration.engine.lock():
migration.plan()
migration.migrate(True)
migration.activate()
print(json.dumps({'phase': 'migration_complete', 'evidence': str(root)}), flush=True)
for name in u.CORE_SERVICES:
migration.wait_ready(docker('compose', '-p', project, '-f', root / 'docker-compose.yml', 'ps', '-q', name))
http = Http()
updater = u.Updater(root, http=http)
assert updater.adopt_storage()['status'] == 'adopted'
print(json.dumps({'phase': 'storage_adopted'}), flush=True)
registry_id = docker('run', '-d', '--pull', 'never', '--name', registry, '-p', '127.0.0.1::5000', 'registry:2')
port = json.loads(docker('inspect', registry_id))[0]['NetworkSettings']['Ports']['5000/tcp'][0]['HostPort']
repository = 'localhost:' + port + '/fedeo-update-fixture'
(work / 'Dockerfile').write_text('FROM fedeo-storage-tools:pilot\n'
'RUN node -e "const fs=require(\'fs\');const p=JSON.parse(fs.readFileSync(\'package.json\'));'
'p.scripts={migrate:\'node --version\'};fs.writeFileSync(\'package.json\',JSON.stringify(p))"\n'
'ENTRYPOINT ["node"]\nCMD ["-e", "setInterval(()=>{},1000)"]\n')
docker('build', '-t', repository + ':fixture', work)
docker('push', repository + ':fixture')
digest = json.loads(docker('image', 'inspect', repository + ':fixture'))[0]['RepoDigests'][0].split('@')[1]
images = {name: {'name': name, 'repository': repository + '-' + name, 'digest': digest} for name in ('backend', 'frontend')}
for image in images.values():
docker('tag', repository + ':fixture', image['repository'] + ':fixture')
docker('push', image['repository'] + ':fixture')
# Copy health, networking and SDK settings from the original fixture, but start the new image's migration script.
updater.releases = Releases(config, images)
for version in ('3.4.0', '3.5.0'):
http.version = version
checked = updater.execute('check', version)
assert checked['plan']['storageProvider'] == 'seaweedfs'
assert not checked['plan']['requiresComposeAdoption']
result = updater.execute('apply', version, expected_fingerprint=checked['plan']['approvalFingerprint'])
assert result['status'] == 'succeeded'
assert not migration.inspect(migration.state['containers']['minio']['id'])['State']['Running']
old_matrix = migration.inspect(retained)
assert not old_matrix['State']['Running'] and old_matrix['HostConfig']['RestartPolicy']['Name'] == 'no'
backup = Path(result['backup'])
with tarfile.open(backup / 'files.tar') as archive:
assert archive.extractfile('matrix/postgres/retained.txt').read() == b'retired Matrix'
assert 'storage/config/s3.json' in archive.getnames()
assert 'minio' not in archive.getnames()
metadata = u.read_json(backup / 'complete.json')
for name, expected in metadata['sha256'].items():
assert u.file_sha(backup / name) == expected
print(json.dumps({'phase': 'update_complete', 'version': version, 'backup': str(backup)}), flush=True)
# Restore the *updater's* cold archive, not the original migration backup.
restore = root / '.updates/update-restore-test'
restore.mkdir()
subprocess.run(['tar', '-xpf', str(backup / 'files.tar'), '-C', str(restore)], check=True)
store = updater.storage['service']
restore_name = project + '-update-restore'
try:
docker('run', '-d', '--pull', 'never', '--name', restore_name, '--network', project + '-internal',
'--label', 'com.docker.compose.project=' + project,
'-v', str(restore / 'storage/seaweedfs') + ':/data', '-v', str(restore / 'storage/config') + ':/etc/fedeo:ro',
'-e', 'FEDEO_S3_CONFIG_SHA256=' + updater.storage['configSha256']['s3.json'],
'--entrypoint', store['entrypoint'][0], store['image'], *store['entrypoint'][1:], *store['command'])
migration.helper('restore-verify', endpoint='http://' + restore_name + ':9000')
finally:
docker('rm', '-f', restore_name)
print(json.dumps({'result': 'passed', 'restoredUpdaterBackup': str(backup), 'evidence': str(root)}), flush=True)
except BaseException:
print('Failure evidence: ' + str(root), flush=True)
for path in sorted((root / '.updates').glob('*/commands.log')):
print(str(path) + '\n' + path.read_text()[-5000:], flush=True)
raise
finally:
ids = docker('ps', '-aq', '--filter', 'label=com.docker.compose.project=' + project).split()
if ids:
docker('rm', '-f', '-v', *ids)
if registry_id:
docker('rm', '-f', '-v', registry_id)
docker('network', 'rm', project + '-internal')
if __name__ == '__main__':
run()