Skip to content

Commit 6bd63ae

Browse files
authored
Mutualize migration and config (#1423)
* Mutualize config and migration * Rename tests * Add constants for cli options * Fix type of parse_and_check * Add type hints
1 parent b6223a0 commit 6bd63ae

File tree

4 files changed

+51
-200
lines changed

4 files changed

+51
-200
lines changed

cli/config.py

Lines changed: 45 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,11 @@
3636
from sonar import platform, rules, qualityprofiles, qualitygates, users, groups
3737
from sonar import projects, portfolios, applications
3838

39-
WRITE_FILE = None
40-
41-
_EVERYTHING = [
42-
options.WHAT_SETTINGS,
43-
options.WHAT_USERS,
44-
options.WHAT_GROUPS,
45-
options.WHAT_GATES,
46-
options.WHAT_RULES,
47-
options.WHAT_PROFILES,
48-
options.WHAT_PROJECTS,
49-
options.WHAT_APPS,
50-
options.WHAT_PORTFOLIOS,
51-
]
52-
5339
TOOL_NAME = "sonar-config"
5440

41+
DONT_INLINE_LISTS = "dontInlineLists"
42+
FULL_EXPORT = "fullExport"
43+
5544
__JSON_KEY_PLATFORM = "platform"
5645
__JSON_KEY_SETTINGS = "globalSettings"
5746
__JSON_KEY_USERS = "users"
@@ -63,21 +52,8 @@
6352
__JSON_KEY_APPS = "applications"
6453
__JSON_KEY_PORTFOLIOS = "portfolios"
6554

66-
__MAP = {
67-
options.WHAT_SETTINGS: __JSON_KEY_SETTINGS,
68-
options.WHAT_USERS: __JSON_KEY_USERS,
69-
options.WHAT_GROUPS: __JSON_KEY_GROUPS,
70-
options.WHAT_GATES: __JSON_KEY_GATES,
71-
options.WHAT_RULES: __JSON_KEY_RULES,
72-
options.WHAT_PROFILES: __JSON_KEY_PROFILES,
73-
options.WHAT_PROJECTS: __JSON_KEY_PROJECTS,
74-
options.WHAT_APPS: __JSON_KEY_APPS,
75-
options.WHAT_PORTFOLIOS: __JSON_KEY_PORTFOLIOS,
76-
}
77-
78-
7955
_EXPORT_CALLS = {
80-
"platform": [__JSON_KEY_PLATFORM, platform.basics],
56+
__JSON_KEY_PLATFORM: [__JSON_KEY_PLATFORM, platform.basics],
8157
options.WHAT_SETTINGS: [__JSON_KEY_SETTINGS, platform.export],
8258
options.WHAT_RULES: [__JSON_KEY_RULES, rules.export],
8359
options.WHAT_PROFILES: [__JSON_KEY_PROFILES, qualityprofiles.export],
@@ -89,16 +65,19 @@
8965
options.WHAT_GROUPS: [__JSON_KEY_GROUPS, groups.export],
9066
}
9167

68+
_EVERYTHING = list(_EXPORT_CALLS.keys())[1:]
69+
9270

93-
def __parse_args(desc):
71+
def __parse_args(desc: str) -> object:
72+
"""Sets and parses all sonar-config options"""
9473
parser = options.set_common_args(desc)
9574
parser = options.set_key_arg(parser)
9675
parser = options.set_output_file_args(parser, allowed_formats=("json", "yaml"))
9776
parser = options.add_thread_arg(parser, "project export")
9877
parser = options.set_what(parser, what_list=_EVERYTHING, operation="export or import")
9978
parser = options.add_import_export_arg(parser, "configuration")
10079
parser.add_argument(
101-
"--fullExport",
80+
f"--{FULL_EXPORT}",
10281
required=False,
10382
default=False,
10483
action="store_true",
@@ -115,7 +94,7 @@ def __parse_args(desc):
11594
"and the setting will not be imported at import time",
11695
)
11796
parser.add_argument(
118-
"--dontInlineLists",
97+
f"--{DONT_INLINE_LISTS}",
11998
required=False,
12099
default=False,
121100
action="store_true",
@@ -157,96 +136,55 @@ def __convert_for_yaml(json_export: dict[str, any]) -> dict[str, any]:
157136
return json_export
158137

159138

160-
def __export_config_sync(endpoint: platform.Platform, what: list[str], **kwargs) -> None:
161-
"""Exports config in a synchronous way"""
162-
export_settings = {
163-
"INLINE_LISTS": not kwargs["dontInlineLists"],
164-
"EXPORT_DEFAULTS": kwargs["exportDefaults"],
165-
"FULL_EXPORT": kwargs["fullExport"],
166-
"THREADS": kwargs[options.NBR_THREADS],
167-
options.REPORT_FILE: kwargs[options.REPORT_FILE],
168-
}
169-
if "projects" in what and kwargs[options.KEYS]:
170-
non_existing_projects = [key for key in kwargs[options.KEYS] if not projects.exists(key, endpoint)]
171-
if len(non_existing_projects) > 0:
172-
utilities.exit_fatal(f"Project key(s) '{','.join(non_existing_projects)}' do(es) not exist", errcodes.NO_SUCH_KEY)
173-
log.info("Exporting configuration synchronously from %s", kwargs[options.URL])
174-
key_list = kwargs[options.KEYS]
175-
what.append("platform")
176-
sq_settings = {}
177-
for what_item, call_data in _EXPORT_CALLS.items():
178-
if what_item not in what:
179-
continue
180-
ndx, func = call_data
181-
try:
182-
sq_settings[ndx] = func(endpoint, export_settings=export_settings, key_list=key_list)
183-
except exceptions.UnsupportedOperation as e:
184-
log.warning(e.message)
185-
except exceptions.ObjectNotFound as e:
186-
log.error(e.message)
187-
sq_settings = __prep_json_for_write(sq_settings, export_settings)
188-
__write_export(sq_settings, kwargs[options.REPORT_FILE], kwargs[options.FORMAT])
189-
log.info("Synchronous export of configuration from %s completed", kwargs["url"])
190-
191-
192-
def __prep_json_for_write(json_data: types.ObjectJsonRepr, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
193-
"""Cleans up the JSON before writing"""
194-
json_data = utilities.sort_lists(json_data)
195-
if not export_settings.get("FULL_EXPORT", False):
196-
json_data = utilities.remove_empties(utilities.remove_nones(json_data))
197-
if export_settings.get("INLINE_LISTS", True):
198-
json_data = utilities.inline_lists(json_data, exceptions=("conditions",))
199-
return json_data
200-
201-
202139
def write_objects(queue: Queue[types.ObjectJsonRepr], fd: TextIO, object_type: str, export_settings: types.ConfigSettings) -> None:
203140
"""
204141
Thread to write projects in the JSON file
205142
"""
143+
done = False
206144
prefix = ""
207145
log.info("Waiting %s to write...", object_type)
208146
print(f'"{object_type}": ' + "{", file=fd)
209-
while True:
147+
while not done:
210148
obj_json = queue.get()
211-
if obj_json is None:
212-
queue.task_done()
213-
break
214-
obj_json = __prep_json_for_write(obj_json, export_settings)
215-
if object_type in ("projects", "applications", "portfolios", "users"):
216-
if object_type == "users":
217-
key = obj_json.pop("login", None)
149+
done = obj_json is None
150+
if not done:
151+
obj_json = __prep_json_for_write(obj_json, export_settings)
152+
if object_type in ("projects", "applications", "portfolios", "users"):
153+
if object_type == "users":
154+
key = obj_json.pop("login", None)
155+
else:
156+
key = obj_json.pop("key", None)
157+
log.debug("Writing %s key '%s'", object_type[:-1], key)
158+
print(f'{prefix}"{key}": {utilities.json_dump(obj_json)}', end="", file=fd)
218159
else:
219-
key = obj_json.pop("key", None)
220-
log.debug("Writing %s key '%s'", object_type[:-1], key)
221-
print(f'{prefix}"{key}": {utilities.json_dump(obj_json)}', end="", file=fd)
222-
else:
223-
log.debug("Writing %s", object_type)
224-
print(f"{prefix}{utilities.json_dump(obj_json)[2:-1]}", end="", file=fd)
225-
prefix = ",\n"
160+
log.debug("Writing %s", object_type)
161+
print(f"{prefix}{utilities.json_dump(obj_json)[2:-1]}", end="", file=fd)
162+
prefix = ",\n"
226163
queue.task_done()
227164
print("\n}", file=fd, end="")
228165
log.info("Writing %s complete", object_type)
229166

230167

231-
def __export_config_async(endpoint: platform.Platform, what: list[str], **kwargs) -> None:
168+
def export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> None:
232169
"""Exports a platform configuration in a JSON file"""
233170
file = kwargs[options.REPORT_FILE]
171+
mode = kwargs.get("mode", "CONFIG")
234172
export_settings = {
235-
"INLINE_LISTS": not kwargs["dontInlineLists"],
236-
"EXPORT_DEFAULTS": kwargs["exportDefaults"],
237-
"FULL_EXPORT": kwargs["fullExport"],
238-
"MODE": "CONFIG",
173+
"INLINE_LISTS": False if mode == "MIGRATION" else not kwargs.get(DONT_INLINE_LISTS, False),
174+
"EXPORT_DEFAULTS": True,
175+
"FULL_EXPORT": False if mode == "MIGRATION" else kwargs.get(FULL_EXPORT, False),
176+
"MODE": mode,
239177
"THREADS": kwargs[options.NBR_THREADS],
240-
"SKIP_ISSUES": True,
178+
"SKIP_ISSUES": kwargs.get("skipIssues", False),
241179
}
242180
if "projects" in what and kwargs[options.KEYS]:
243181
non_existing_projects = [key for key in kwargs[options.KEYS] if not projects.exists(key, endpoint)]
244182
if len(non_existing_projects) > 0:
245183
utilities.exit_fatal(f"Project key(s) '{','.join(non_existing_projects)}' do(es) not exist", errcodes.NO_SUCH_KEY)
246184

185+
what.append(__JSON_KEY_PLATFORM)
247186
log.info("Exporting configuration from %s", kwargs[options.URL])
248187
key_list = kwargs[options.KEYS]
249-
what.append("platform")
250188
is_first = True
251189
q = Queue(maxsize=0)
252190
with utilities.open_file(file, mode="w") as fd:
@@ -268,16 +206,20 @@ def __export_config_async(endpoint: platform.Platform, what: list[str], **kwargs
268206
except exceptions.UnsupportedOperation as e:
269207
log.warning(e.message)
270208
print("\n}", file=fd)
271-
utilities.normalize_json_file(file, remove_empty=True, remove_none=True)
209+
utilities.normalize_json_file(file, remove_empty=False, remove_none=True)
272210
log.info("Exporting migration data from %s completed", kwargs["url"])
273211

274212

275-
def __export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> None:
276-
"""Exports the configuration of the SonarQube platform"""
277-
if kwargs[options.KEYS] or options.WHAT_PROJECTS not in what or kwargs[options.FORMAT] != "json":
278-
__export_config_sync(endpoint=endpoint, what=what, **kwargs)
279-
else:
280-
__export_config_async(endpoint=endpoint, what=what, **kwargs)
213+
def __prep_json_for_write(json_data: types.ObjectJsonRepr, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
214+
"""Cleans up the JSON before writing"""
215+
json_data = utilities.sort_lists(json_data)
216+
if export_settings.get("MODE", "CONFIG") == "MIGRATION":
217+
return json_data
218+
if not export_settings.get("FULL_EXPORT", False):
219+
json_data = utilities.remove_empties(utilities.remove_nones(json_data))
220+
if export_settings.get("INLINE_LISTS", True):
221+
json_data = utilities.inline_lists(json_data, exceptions=("conditions",))
222+
return json_data
281223

282224

283225
def __import_config(endpoint: platform.Platform, what: list[str], **kwargs) -> None:
@@ -330,7 +272,7 @@ def main() -> None:
330272
kwargs[options.FORMAT] = utilities.deduct_format(kwargs[options.FORMAT], kwargs[options.REPORT_FILE], allowed_formats=("json", "yaml"))
331273
if kwargs[options.EXPORT]:
332274
try:
333-
__export_config(endpoint, what, **kwargs)
275+
export_config(endpoint, what, **kwargs)
334276
except exceptions.ObjectNotFound as e:
335277
utilities.exit_fatal(e.message, errcodes.NO_SUCH_KEY)
336278
except (PermissionError, FileNotFoundError) as e:

cli/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def __check_file_writeable(file: str) -> None:
176176
os.remove(file)
177177

178178

179-
def parse_and_check(parser: ArgumentParser, logger_name: str = None, verify_token: bool = True, is_migration: bool = False) -> ArgumentParser:
179+
def parse_and_check(parser: ArgumentParser, logger_name: str = None, verify_token: bool = True, is_migration: bool = False) -> object:
180180
"""Parses arguments, applies default settings and perform common environment checks"""
181181
try:
182182
args = parser.parse_args()

migration/migration.py

Lines changed: 3 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@
2222
Exports SonarQube platform configuration as JSON
2323
"""
2424
import sys
25-
from threading import Thread
26-
from queue import Queue
2725

28-
from cli import options
26+
from cli import options, config
2927
from sonar import exceptions, errcodes, utilities, version
3028
import sonar.logging as log
31-
from sonar import platform, rules, qualityprofiles, qualitygates, users, groups
32-
from sonar import projects, portfolios, applications
29+
from sonar import platform
3330

3431
TOOL_NAME = "sonar-migration"
3532

@@ -97,94 +94,6 @@ def __parse_args(desc):
9794
return args
9895

9996

100-
def write_objects(queue: Queue, fd, object_type: str) -> None:
101-
"""
102-
Thread to write projects in the JSON file
103-
"""
104-
done = False
105-
prefix = ""
106-
log.info("Waiting %s to write...", object_type)
107-
print(f'"{object_type}": ' + "{", file=fd)
108-
while not done:
109-
obj_json = queue.get()
110-
done = obj_json is None
111-
if not done:
112-
obj_json = utilities.remove_nones(obj_json)
113-
if object_type in ("projects", "applications", "portfolios", "users"):
114-
if object_type == "users":
115-
key = obj_json.pop("login", None)
116-
else:
117-
key = obj_json.pop("key", None)
118-
log.debug("Writing %s key '%s'", object_type[:-1], key)
119-
print(f'{prefix}"{key}": {utilities.json_dump(obj_json)}', end="", file=fd)
120-
else:
121-
log.debug("Writing %s", object_type)
122-
print(f"{prefix}{utilities.json_dump(obj_json)[2:-1]}", end="", file=fd)
123-
prefix = ",\n"
124-
queue.task_done()
125-
print("\n}", file=fd, end="")
126-
log.info("Writing %s complete", object_type)
127-
128-
129-
def __export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> None:
130-
"""Exports a platform configuration in a JSON file"""
131-
file = kwargs[options.REPORT_FILE]
132-
export_settings = {
133-
"INLINE_LISTS": False,
134-
"EXPORT_DEFAULTS": True,
135-
# "FULL_EXPORT": kwargs["fullExport"],
136-
"FULL_EXPORT": False,
137-
"MODE": "MIGRATION",
138-
"THREADS": kwargs[options.NBR_THREADS],
139-
"SKIP_ISSUES": kwargs["skipIssues"],
140-
}
141-
if "projects" in what and kwargs[options.KEYS]:
142-
non_existing_projects = [key for key in kwargs[options.KEYS] if not projects.exists(key, endpoint)]
143-
if len(non_existing_projects) > 0:
144-
utilities.exit_fatal(f"Project key(s) '{','.join(non_existing_projects)}' do(es) not exist", errcodes.NO_SUCH_KEY)
145-
146-
calls = {
147-
"platform": [__JSON_KEY_PLATFORM, platform.basics],
148-
options.WHAT_SETTINGS: [__JSON_KEY_SETTINGS, platform.export],
149-
options.WHAT_RULES: [__JSON_KEY_RULES, rules.export],
150-
options.WHAT_PROFILES: [__JSON_KEY_PROFILES, qualityprofiles.export],
151-
options.WHAT_GATES: [__JSON_KEY_GATES, qualitygates.export],
152-
options.WHAT_PROJECTS: [__JSON_KEY_PROJECTS, projects.export],
153-
options.WHAT_APPS: [__JSON_KEY_APPS, applications.export],
154-
options.WHAT_PORTFOLIOS: [__JSON_KEY_PORTFOLIOS, portfolios.export],
155-
options.WHAT_USERS: [__JSON_KEY_USERS, users.export],
156-
options.WHAT_GROUPS: [__JSON_KEY_GROUPS, groups.export],
157-
}
158-
what.append("platform")
159-
log.info("Exporting configuration from %s", kwargs[options.URL])
160-
key_list = kwargs[options.KEYS]
161-
is_first = True
162-
q = Queue(maxsize=0)
163-
with utilities.open_file(file, mode="w") as fd:
164-
print("{", file=fd)
165-
for what_item, call_data in calls.items():
166-
if what_item not in what:
167-
continue
168-
ndx, func = call_data
169-
try:
170-
if not is_first:
171-
print(",", file=fd)
172-
is_first = False
173-
worker = Thread(target=write_objects, args=(q, fd, ndx))
174-
worker.daemon = True
175-
worker.name = f"Write{ndx[:1].upper()}{ndx[1:10]}"
176-
worker.start()
177-
func(endpoint, export_settings=export_settings, key_list=key_list, write_q=q)
178-
q.join()
179-
except exceptions.UnsupportedOperation as e:
180-
log.warning(e.message)
181-
# if not kwargs.get("dontInlineLists", False):
182-
# sq_settings = utilities.inline_lists(sq_settings, exceptions=("conditions",))
183-
print("\n}", file=fd)
184-
utilities.normalize_json_file(file, remove_empty=False, remove_none=True)
185-
log.info("Exporting migration data from %s completed", kwargs["url"])
186-
187-
18897
def main() -> None:
18998
"""Main entry point for sonar-config"""
19099
start_time = utilities.start_clock()
@@ -203,7 +112,7 @@ def main() -> None:
203112
if kwargs[options.REPORT_FILE] is None:
204113
kwargs[options.REPORT_FILE] = f"sonar-migration.{endpoint.server_id()}.json"
205114
try:
206-
__export_config(endpoint, what, **kwargs)
115+
config.export_config(endpoint, what, mode="MIGRATION", **kwargs)
207116
except exceptions.ObjectNotFound as e:
208117
utilities.exit_fatal(e.message, errcodes.NO_SUCH_KEY)
209118
except (PermissionError, FileNotFoundError) as e:

test/test_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def test_config_non_existing_project() -> None:
102102
util.clean(util.JSON_FILE)
103103

104104

105-
def test_config_inline_commas() -> None:
105+
def test_config_inline_lists() -> None:
106106
"""test_config_inline_commas"""
107107
util.clean(util.JSON_FILE)
108108
with pytest.raises(SystemExit):
@@ -124,7 +124,7 @@ def test_config_inline_commas() -> None:
124124
util.clean(util.JSON_FILE)
125125

126126

127-
def test_config_no_inline_commas() -> None:
127+
def test_config_dont_inline_lists() -> None:
128128
"""test_config_no_inline_commas"""
129129
util.clean(util.JSON_FILE)
130130
with pytest.raises(SystemExit):

0 commit comments

Comments
 (0)