Skip to content

Commit d8681e0

Browse files
authored
Add sonar-config JSON format conversion (#2071)
* Add JSON file conversion option * Add user JSON conversion function * Add group JSON conversion function * Add platform JSON conversion functions * Add rules JSON conversion function * Add QG JSON conversion function * Add applications export conversion * Add project export conversion * Remove useless log * Add QP JSON conversion functions * Add portfolios JSON conversion to new format * Quality pass * Quality pass
1 parent 0f46cd0 commit d8681e0

File tree

10 files changed

+187
-49
lines changed

10 files changed

+187
-49
lines changed

cli/config.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
Exports SonarQube platform configuration as JSON
2323
"""
2424

25-
from typing import TextIO
25+
from typing import TextIO, Any
2626
from threading import Thread
2727
from queue import Queue
2828

@@ -119,6 +119,16 @@ def __parse_args(desc: str) -> object:
119119
action="store_true",
120120
help="By default, sonar-config does not export empty values, setting this flag will add empty values in the export",
121121
)
122+
parser.add_argument(
123+
"--convertFrom",
124+
required=False,
125+
help="Source sonar-config old JSON format",
126+
)
127+
parser.add_argument(
128+
"--convertTo",
129+
required=False,
130+
help="Target sonar-config new JSON format",
131+
)
122132
return options.parse_and_check(parser=parser, logger_name=TOOL_NAME)
123133

124134

@@ -294,11 +304,40 @@ def __import_config(endpoint: platform.Platform, what: list[str], **kwargs) -> N
294304
log.info("Importing configuration to %s completed", kwargs[options.URL])
295305

296306

307+
def convert_json(**kwargs) -> dict[str, Any]:
308+
"""Converts a sonar-config report from the old to the new JSON format"""
309+
with open(kwargs["convertFrom"], encoding="utf-8") as fd:
310+
old_json = json.loads(fd.read())
311+
mapping = {
312+
"platform": platform.old_to_new_json,
313+
"globalSettings": platform.global_settings_old_to_new_json,
314+
"qualityProfiles": qualityprofiles.old_to_new_json,
315+
"qualityGates": qualitygates.old_to_new_json,
316+
"projects": projects.old_to_new_json,
317+
"portfolios": portfolios.old_to_new_json,
318+
"applications": applications.old_to_new_json,
319+
"users": users.old_to_new_json,
320+
"groups": groups.old_to_new_json,
321+
"rules": rules.old_to_new_json,
322+
}
323+
new_json = {}
324+
for k, func in mapping.items():
325+
if k in old_json:
326+
log.info("Converting %s", k)
327+
new_json[k] = func(old_json[k])
328+
with open(kwargs["convertTo"], mode="w", encoding="utf-8") as fd:
329+
print(utilities.json_dump(new_json), file=fd)
330+
return new_json
331+
332+
297333
def main() -> None:
298334
"""Main entry point for sonar-config"""
299335
start_time = utilities.start_clock()
300336
try:
301337
kwargs = utilities.convert_args(__parse_args("Extract SonarQube Server or Cloud platform configuration"))
338+
if kwargs["convertFrom"] is not None:
339+
convert_json(**kwargs)
340+
utilities.final_exit(errcodes.OK, "", start_time)
302341
endpoint = platform.Platform(**kwargs)
303342
endpoint.verify_connection()
304343
endpoint.set_user_agent(f"{TOOL_NAME} {version.PACKAGE_VERSION}")

sonar/applications.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"""
2424

2525
from __future__ import annotations
26-
from typing import Optional
26+
from typing import Optional, Any
2727
import re
2828
import json
2929
from datetime import datetime
@@ -340,10 +340,11 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
340340
"visibility": self.visibility(),
341341
# 'projects': self.projects(),
342342
"branches": {br.name: br.export() for br in self.branches().values()},
343-
"permissions": util.perms_to_list(self.permissions().export(export_settings=export_settings)),
343+
"permissions": self.permissions().export(export_settings=export_settings),
344344
"tags": self.get_tags(),
345345
}
346346
)
347+
json_data = old_to_new_json_one(json_data)
347348
return util.filter_export(json_data, _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False))
348349

349350
def set_permissions(self, data: types.JsonPermissions) -> application_permissions.ApplicationPermissions:
@@ -507,7 +508,7 @@ def exists(endpoint: pf.Platform, key: str) -> bool:
507508
return False
508509

509510

510-
def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwargs) -> types.ObjectJsonRepr:
511+
def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwargs) -> list[dict[str, Any]]:
511512
"""Exports applications as JSON
512513
513514
:param endpoint: Reference to the Sonar platform
@@ -520,14 +521,13 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwarg
520521
key_regexp = kwargs.get("key_list", ".*")
521522

522523
app_list = {k: v for k, v in get_list(endpoint).items() if not key_regexp or re.match(key_regexp, k)}
523-
apps_settings = {}
524+
apps_settings = []
524525
for k, app in app_list.items():
525526
app_json = app.export(export_settings)
526527
if write_q:
527528
write_q.put(app_json)
528529
else:
529-
app_json.pop("key")
530-
apps_settings[k] = app_json
530+
apps_settings.append(app_json)
531531
write_q and write_q.put(util.WRITE_END)
532532
return apps_settings
533533

@@ -598,3 +598,21 @@ def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, Application]:
598598
data[app.key] = app
599599
# return {app.key: app for app in Application.CACHE.values() if app.name == name}
600600
return data
601+
602+
603+
def old_to_new_json_one(old_app_json: dict[str, Any]) -> dict[str, Any]:
604+
"""Converts sonar-config old JSON report format to new format for a single application"""
605+
new_json = old_app_json.copy()
606+
if "permissions" in old_app_json:
607+
new_json["permissions"] = util.perms_to_list(old_app_json["permissions"])
608+
if "branches" in old_app_json:
609+
new_json["branches"] = util.dict_to_list(old_app_json["branches"], "name")
610+
return new_json
611+
612+
613+
def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]:
614+
"""Converts sonar-config old JSON report format to new format"""
615+
new_json = old_json.copy()
616+
for k, v in new_json.items():
617+
new_json[k] = old_to_new_json_one(v)
618+
return util.dict_to_list(new_json, "key")

sonar/groups.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from __future__ import annotations
2525
import json
2626

27-
from typing import Optional
27+
from typing import Optional, Any
2828

2929
import sonar.logging as log
3030
import sonar.platform as pf
@@ -428,7 +428,12 @@ def import_config(endpoint: pf.Platform, config_data: types.ObjectJsonRepr, key_
428428
def exists(endpoint: pf.Platform, name: str) -> bool:
429429
"""
430430
:param endpoint: reference to the SonarQube platform
431-
:param group_name: group name to check
431+
:param name: group name to check
432432
:return: whether the group exists
433433
"""
434434
return Group.get_object(endpoint=endpoint, name=name) is not None
435+
436+
437+
def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]:
438+
"""Converts sonar-config old groups JSON report format to new format"""
439+
return util.dict_to_list(old_json, "name", "description")

sonar/platform.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,7 @@ def __urlstring(self, api: str, params: types.ApiParams, data: Optional[str] = N
465465
return url
466466

467467
def webhooks(self) -> dict[str, webhooks.WebHook]:
468-
"""
469-
:return: the list of global webhooks
470-
"""
468+
"""Returns the list of global webhooks"""
471469
return webhooks.get_list(self)
472470

473471
def export(self, export_settings: types.ConfigSettings, full: bool = False) -> types.ObjectJsonRepr:
@@ -476,19 +474,16 @@ def export(self, export_settings: types.ConfigSettings, full: bool = False) -> t
476474
:param full: Whether to also export properties that cannot be set, defaults to False
477475
:type full: bool, optional
478476
:return: dict of all properties with their values
479-
:rtype: dict
480477
"""
481478
log.info("Exporting platform global settings")
482479
json_data = {}
480+
settings_list = self.__settings(include_not_set=export_settings.get("EXPORT_DEFAULTS", False)).values()
481+
settings_list = [s for s in settings_list if s.is_global() and not s.is_internal()]
483482
for s in self.__settings(include_not_set=export_settings.get("EXPORT_DEFAULTS", False)).values():
484-
if s.is_internal():
485-
continue
486483
(categ, subcateg) = s.category()
487484
if self.is_sonarcloud() and categ == settings.THIRD_PARTY_SETTINGS:
488485
# What is reported as 3rd part are SonarQube Cloud internal settings
489486
continue
490-
if not s.is_global():
491-
continue
492487
util.update_json(json_data, categ, subcateg, s.to_json(export_settings.get("INLINE_LISTS", True)))
493488

494489
hooks = {}
@@ -650,8 +645,7 @@ def _audit_logs(self, audit_settings: types.ConfigSettings) -> list[Problem]:
650645
if rule is not None:
651646
problems.append(Problem(rule, f"{self.local_url}/admin/system", logfile, line))
652647
logs = self.get("system/logs", params={"name": "deprecation"}).text
653-
nb_deprecation = len(logs.splitlines())
654-
if nb_deprecation > 0:
648+
if (nb_deprecation := len(logs.splitlines())) > 0:
655649
rule = get_rule(RuleId.DEPRECATION_WARNINGS)
656650
problems.append(Problem(rule, f"{self.local_url}/admin/system", nb_deprecation))
657651
return problems
@@ -810,10 +804,9 @@ def _normalize_api(api: str) -> str:
810804
return api
811805

812806

813-
def _audit_setting_value(key: str, platform_settings: dict[str, any], audit_settings: types.ConfigSettings, url: str) -> list[Problem]:
807+
def _audit_setting_value(key: str, platform_settings: dict[str, Any], audit_settings: types.ConfigSettings, url: str) -> list[Problem]:
814808
"""Audits a particular platform setting is set to expected value"""
815-
v = _get_multiple_values(4, audit_settings[key], "MEDIUM", "CONFIGURATION")
816-
if v is None:
809+
if (v := _get_multiple_values(4, audit_settings[key], "MEDIUM", "CONFIGURATION")) is None:
817810
log.error(WRONG_CONFIG_MSG, key, audit_settings[key])
818811
return []
819812
if v[0] not in platform_settings:
@@ -830,11 +823,10 @@ def _audit_setting_value(key: str, platform_settings: dict[str, any], audit_sett
830823

831824

832825
def _audit_setting_in_range(
833-
key: str, platform_settings: dict[str, any], audit_settings: types.ConfigSettings, sq_version: tuple[int, int, int], url: str
826+
key: str, platform_settings: dict[str, Any], audit_settings: types.ConfigSettings, sq_version: tuple[int, int, int], url: str
834827
) -> list[Problem]:
835828
"""Audits a particular platform setting is within expected range of values"""
836-
v = _get_multiple_values(5, audit_settings[key], "MEDIUM", "CONFIGURATION")
837-
if v is None:
829+
if (v := _get_multiple_values(5, audit_settings[key], "MEDIUM", "CONFIGURATION")) is None:
838830
log.error(WRONG_CONFIG_MSG, key, audit_settings[key])
839831
return []
840832
if v[0] not in platform_settings:
@@ -852,11 +844,10 @@ def _audit_setting_in_range(
852844

853845

854846
def _audit_setting_set(
855-
key: str, check_is_set: bool, platform_settings: dict[str, any], audit_settings: types.ConfigSettings, url: str
847+
key: str, check_is_set: bool, platform_settings: dict[str, Any], audit_settings: types.ConfigSettings, url: str
856848
) -> list[Problem]:
857849
"""Audits that a setting is set or not set"""
858-
v = _get_multiple_values(3, audit_settings[key], "MEDIUM", "CONFIGURATION")
859-
if v is None:
850+
if (v := _get_multiple_values(3, audit_settings[key], "MEDIUM", "CONFIGURATION")) is None:
860851
log.error(WRONG_CONFIG_MSG, key, audit_settings[key])
861852
return []
862853
log.info("Auditing whether setting %s is set or not", v[0])
@@ -928,7 +919,6 @@ def import_config(endpoint: Platform, config_data: types.ObjectJsonRepr, key_lis
928919
:param Platform endpoint: reference to the SonarQube platform
929920
:param ObjectJsonRepr config_data: the configuration to import
930921
:param KeyList key_list: Unused
931-
:return: Nothing
932922
"""
933923
return endpoint.import_config(config_data)
934924

@@ -948,7 +938,6 @@ def export(endpoint: Platform, export_settings: types.ConfigSettings, **kwargs)
948938
:param Platform endpoint: reference to the SonarQube platform
949939
:param ConfigSettings export_settings: Export parameters
950940
:return: Platform settings
951-
:rtype: ObjectJsonRepr
952941
"""
953942
exp = endpoint.export(export_settings)
954943
if write_q := kwargs.get("write_q", None):
@@ -974,3 +963,29 @@ def audit(endpoint: Platform, audit_settings: types.ConfigSettings, **kwargs) ->
974963
pbs = endpoint.audit(audit_settings)
975964
"write_q" in kwargs and kwargs["write_q"].put(pbs)
976965
return pbs
966+
967+
968+
def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]:
969+
"""Converts sonar-config "plaform" section old JSON report format to new format"""
970+
if "plugins" in old_json:
971+
old_json["plugins"] = util.dict_to_list(old_json["plugins"], "key")
972+
return old_json
973+
974+
975+
def global_settings_old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]:
976+
"""Converts sonar-config "globalSettings" section old JSON report format to new format"""
977+
new_json = {}
978+
special_categories = (settings.LANGUAGES_SETTINGS, settings.DEVOPS_INTEGRATION, "permissions", "permissionTemplates")
979+
for categ in [cat for cat in settings.CATEGORIES if cat not in special_categories]:
980+
new_json[categ] = util.sort_list_by_key(util.dict_to_list(old_json[categ], "key"), "key")
981+
for k, v in old_json[settings.LANGUAGES_SETTINGS].items():
982+
new_json[settings.LANGUAGES_SETTINGS] = new_json.get(settings.LANGUAGES_SETTINGS, None) or {}
983+
new_json[settings.LANGUAGES_SETTINGS][k] = util.sort_list_by_key(util.dict_to_list(v, "key"), "key")
984+
new_json[settings.LANGUAGES_SETTINGS] = util.dict_to_list(new_json[settings.LANGUAGES_SETTINGS], "language", "settings")
985+
new_json[settings.DEVOPS_INTEGRATION] = util.dict_to_list(old_json[settings.DEVOPS_INTEGRATION], "key")
986+
new_json["permissions"] = util.perms_to_list(old_json["permissions"])
987+
for v in old_json["permissionTemplates"].values():
988+
if "permissions" in v:
989+
v["permissions"] = util.perms_to_list(v["permissions"])
990+
new_json["permissionTemplates"] = util.dict_to_list(old_json["permissionTemplates"], "key")
991+
return new_json

sonar/portfolios.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,3 +850,24 @@ def get_api_branch(branch: str) -> str:
850850
def clear_cache(endpoint: pf.Platform) -> None:
851851
"""Clears the cache of an endpoint"""
852852
Portfolio.clear_cache(endpoint)
853+
854+
855+
def old_to_new_json_one(old_json: dict[str, Any]) -> dict[str, Any]:
856+
"""Converts the sonar-config old JSON report format for a single portfolio to the new one"""
857+
new_json = old_json.copy()
858+
for key in "children", "portfolios":
859+
if key in new_json:
860+
new_json[key] = old_to_new_json(new_json[key])
861+
if "permissions" in old_json:
862+
new_json["permissions"] = util.perms_to_list(old_json["permissions"])
863+
if "branches" in old_json:
864+
new_json["branches"] = util.dict_to_list(old_json["branches"], "name")
865+
return new_json
866+
867+
868+
def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]:
869+
"""Converts the sonar-config portfolios old JSON report format to the new one"""
870+
new_json = old_json.copy()
871+
for k, v in new_json.items():
872+
new_json[k] = old_to_new_json_one(v)
873+
return util.dict_to_list(new_json, "key")

sonar/projects.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from datetime import datetime
3333
import traceback
3434

35-
from typing import Optional, Union
35+
from typing import Optional, Union, Any
3636
from http import HTTPStatus
3737
from threading import Lock
3838
from requests import HTTPError, RequestException
@@ -1696,3 +1696,23 @@ def import_zips(endpoint: pf.Platform, project_list: list[str], threads: int = 2
16961696
log.info("%d/%d imports (%d%%) - Latest: %s - %s", i, nb_projects, int(i * 100 / nb_projects), proj_key, status)
16971697
log.info("%s", ", ".join([f"{k}:{v}" for k, v in statuses_count.items()]))
16981698
return statuses
1699+
1700+
1701+
def old_to_new_json_one(old_json: dict[str, Any]) -> dict[str, Any]:
1702+
"""Converts the sonar-config projects old JSON report format for a single project to the new one"""
1703+
new_json = old_json.copy()
1704+
if "permissions" in old_json:
1705+
new_json["permissions"] = util.perms_to_list(old_json["permissions"])
1706+
if "branches" in old_json:
1707+
new_json["branches"] = util.dict_to_list(old_json["branches"], "name")
1708+
if "settings" in old_json:
1709+
new_json["settings"] = util.dict_to_list(old_json["settings"], "key")
1710+
return new_json
1711+
1712+
1713+
def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]:
1714+
"""Converts the sonar-config projects old JSON report format to the new one"""
1715+
new_json = old_json.copy()
1716+
for k, v in new_json.items():
1717+
new_json[k] = old_to_new_json_one(v)
1718+
return util.dict_to_list(new_json, "key")

sonar/qualitygates.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"""
2525

2626
from __future__ import annotations
27-
from typing import Union, Optional
27+
from typing import Union, Optional, Any
2828

2929
import json
3030

@@ -561,3 +561,8 @@ def _decode_condition(cond: str) -> tuple[str, str, str]:
561561
def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, QualityGate]:
562562
"""Searches quality gates matching name"""
563563
return util.search_by_name(endpoint, name, QualityGate.API[c.LIST], "qualitygates")
564+
565+
566+
def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]:
567+
"""Converts the sonar-config quality gates old JSON report format to the new one"""
568+
return util.dict_to_list(old_json, "name")

0 commit comments

Comments
 (0)