156 lines
8.6 KiB
Python
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()
|