Merge pull request #12341 from ninjadq/support_multi_down_version

Enhance: Support multi downversion in migration
This commit is contained in:
Qian Deng 2020-07-15 23:39:11 +08:00 committed by GitHub
commit bd26c294e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 52 deletions

View File

@ -14,6 +14,9 @@ from migrations import accept_versions
def migrate(input_, output, target): def migrate(input_, output, target):
""" """
migrate command will migrate config file style to specific version migrate command will migrate config file style to specific version
:input_: is the path of the original config file
:output: is the destination path of config file, the generated configs will storage in it
:target: is the the target version of config file will upgrade to
""" """
if target not in accept_versions: if target not in accept_versions:
click.echo('target version {} not supported'.format(target)) click.echo('target version {} not supported'.format(target))

View File

@ -3,7 +3,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined
from utils.migration import read_conf from utils.migration import read_conf
revision = '1.10.0' revision = '1.10.0'
down_revision = '1.9.0' down_revisions = ['1.9.0']
def migrate(input_cfg, output_cfg): def migrate(input_cfg, output_cfg):
config_dict = read_conf(input_cfg) config_dict = read_conf(input_cfg)

View File

@ -3,14 +3,14 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined
from utils.migration import read_conf from utils.migration import read_conf
revision = '1.9.0' revision = '1.9.0'
down_revision = None down_revisions = []
def migrate(input_cfg, output_cfg): def migrate(input_cfg, output_cfg):
config_dict = read_conf(input_cfg) config_dict = read_conf(input_cfg)
this_dir = os.path.dirname(__file__) current_dir = os.path.dirname(__file__)
tpl = Environment( tpl = Environment(
loader=FileSystemLoader(this_dir), loader=FileSystemLoader(current_dir),
undefined=StrictUndefined, undefined=StrictUndefined,
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True lstrip_blocks=True

View File

@ -48,7 +48,7 @@ harbor_admin_password: {{ harbor_admin_password }}
# Harbor DB configuration # Harbor DB configuration
database: database:
# The password for the root user of Harbor DB. Change this before any production use. # The password for the root user of Harbor DB. Change this before any production use.
password: {{ database.password}} password: {{ database.password }}
# The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained. # The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained.
max_idle_conns: 50 max_idle_conns: 50
# The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections. # The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections.

View File

@ -3,7 +3,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined
from utils.migration import read_conf from utils.migration import read_conf
revision = '2.0.0' revision = '2.0.0'
down_revision = '1.10.0' down_revisions = ['1.10.0']
def migrate(input_cfg, output_cfg): def migrate(input_cfg, output_cfg):
config_dict = read_conf(input_cfg) config_dict = read_conf(input_cfg)

View File

@ -1,36 +1,46 @@
import pytest import pytest
import importlib import importlib
from utils.migration import search from utils.migration import search, MigratioNotFound
class mockModule: class mockModule:
def __init__(self, revision, down_revision): def __init__(self, revision: str, down_revisions: list):
self.revision = revision self.revision = revision
self.down_revision = down_revision self.down_revisions = down_revisions
def mock_import_module_loop(module_path: str): def mock_import_module_loop(module_path: str):
loop_modules = { modules = {
'migration.versions.1_9_0': mockModule('1.9.0', None), 'migrations.version_1_9_0': mockModule('1.9.0', []),
'migration.versions.1_10_0': mockModule('1.10.0', '2.0.0'), 'migrations.version_1_10_0': mockModule('1.10.0', ['2.0.0']),
'migration.versions.2_0_0': mockModule('2.0.0', '1.10.0') 'migrations.version_2_0_0': mockModule('2.0.0', ['1.10.0'])
} }
return loop_modules[module_path] return modules[module_path]
def mock_import_module_mission(module_path: str): def mock_import_module_mission(module_path: str):
loop_modules = { modules = {
'migration.versions.1_9_0': mockModule('1.9.0', None), 'migrations.version_1_9_0': mockModule('1.9.0', []),
'migration.versions.1_10_0': mockModule('1.10.0', None), 'migrations.version_1_10_0': mockModule('1.10.0', []),
'migration.versions.2_0_0': mockModule('2.0.0', '1.10.0') 'migrations.version_2_0_0': mockModule('2.0.0', ['1.10.0'])
} }
return loop_modules[module_path] return modules[module_path]
def mock_import_module_success(module_path: str): def mock_import_module_success(module_path: str):
loop_modules = { modules = {
'migration.versions.1_9_0': mockModule('1.9.0', None), 'migrations.version_1_9_0': mockModule('1.9.0', []),
'migration.versions.1_10_0': mockModule('1.10.0', '1.9.0'), 'migrations.version_1_10_0': mockModule('1.10.0', ['1.9.0']),
'migration.versions.2_0_0': mockModule('2.0.0', '1.10.0') 'migrations.version_2_0_0': mockModule('2.0.0', ['1.10.0'])
} }
return loop_modules[module_path] return modules[module_path]
def mock_import_module_success_multi_downversion(module_path: str):
modules = {
'migrations.version_1_9_0': mockModule('1.9.0', []),
'migrations.version_1_10_0': mockModule('1.10.0', ['1.9.0']),
'migrations.version_1_10_1': mockModule('1.10.1', ['1.9.0']),
'migrations.version_1_10_2': mockModule('1.10.2', ['1.9.0']),
'migrations.version_2_0_0': mockModule('2.0.0', ['1.10.0', '1.10.1', '1.10.2'])
}
return modules[module_path]
@pytest.fixture @pytest.fixture
def mock_import_module_with_loop(monkeypatch): def mock_import_module_with_loop(monkeypatch):
@ -44,15 +54,25 @@ def mock_import_module_with_mission(monkeypatch):
def mock_import_module_with_success(monkeypatch): def mock_import_module_with_success(monkeypatch):
monkeypatch.setattr(importlib, "import_module", mock_import_module_success) monkeypatch.setattr(importlib, "import_module", mock_import_module_success)
@pytest.fixture
def mock_import_module_with_success_multi_downversion(monkeypatch):
monkeypatch.setattr(importlib, "import_module", mock_import_module_success_multi_downversion)
def test_search_loop(mock_import_module_with_loop): def test_search_loop(mock_import_module_with_loop):
with pytest.raises(Exception): with pytest.raises(Exception):
search('1.9.0', '2.0.0') search('1.9.0', '2.0.0')
def test_search_mission(mock_import_module_with_mission): def test_search_mission(mock_import_module_with_mission):
with pytest.raises(Exception): with pytest.raises(MigratioNotFound):
search('1.9.0', '2.0.0') search('1.9.0', '2.0.0')
def test_search_success(): def test_search_success(mock_import_module_with_success):
migration_path = search('1.9.0', '2.0.0') migration_path = search('1.9.0', '2.0.0')
assert migration_path[0].revision == '1.10.0' assert migration_path[0].revision == '1.10.0'
assert migration_path[1].revision == '2.0.0' assert migration_path[1].revision == '2.0.0'
def test_search_success_multi_downversion(mock_import_module_with_success_multi_downversion):
migration_path = search('1.9.0', '2.0.0')
print(migration_path)
assert migration_path[0].revision == '1.10.2'
assert migration_path[1].revision == '2.0.0'

View File

@ -4,7 +4,24 @@ import importlib
import os import os
from collections import deque from collections import deque
from migrations import MIGRATION_BASE_DIR class MigratioNotFound(Exception): ...
class MigrationVersion:
'''
The version used to migration
Arttribute:
name(str): version name like `1.0.0`
module: the python module object for a specific migration which contains migrate info, codes and templates
down_versions(list): previous versions that can migrated to this version
'''
def __init__(self, version: str):
self.name = version
self.module = importlib.import_module("migrations.version_{}".format(version.replace(".","_")))
@property
def down_versions(self):
return self.module.down_revisions
def read_conf(path): def read_conf(path):
with open(path) as f: with open(path) as f:
@ -15,29 +32,35 @@ def read_conf(path):
exit(-1) exit(-1)
return d return d
def _to_module_path(ver): def search(input_version: str, target_version: str) -> list :
return "migrations.version_{}".format(ver.replace(".","_")) """
Find the migration path by BFS
Args:
input_version(str): The version migration start from
target_version(str): The target version migrated to
Returns:
list: the module of migrations in the upgrade path
"""
upgrade_path = []
next_version, visited, q = {}, set(), deque()
q.append(target_version)
found = False
while q: # BFS to find a valid path
version = MigrationVersion(q.popleft())
visited.add(version.name)
if version.name == input_version:
found = True
break # break loop cause migration path found
for v in version.down_versions:
next_version[v] = version.name
if v not in (visited.union(q)):
q.append(v)
def search(input_ver: str, target_ver: str) -> deque : if not found:
""" raise MigratioNotFound('no migration path found to target version')
Search accept a input version and the target version.
Returns the module of migrations in the upgrade path current_version = MigrationVersion(input_version)
""" while current_version.name != target_version:
upgrade_path, visited = deque(), set() current_version = MigrationVersion(next_version[current_version.name])
while True: upgrade_path.append(current_version)
module_path = _to_module_path(target_ver) return list(map(lambda x: x.module, upgrade_path))
visited.add(target_ver) # mark current version for loop finding
if os.path.isdir(os.path.join(MIGRATION_BASE_DIR, 'version_{}'.format(target_ver.replace(".","_")))):
module = importlib.import_module(module_path)
if module.revision == input_ver: # migration path found
break
elif module.down_revision is None: # migration path not found
raise Exception('no migration path found')
else:
upgrade_path.appendleft(module)
target_ver = module.down_revision
if target_ver in visited: # version visited before, loop found
raise Exception('find a loop caused by {} on migration path'.format(target_ver))
else:
raise Exception('{} not dir'.format(os.path.join(MIGRATION_BASE_DIR, 'versions', target_ver.replace(".","_"))))
return upgrade_path