Skip to content

Commit ebf0911

Browse files
authored
Fix-0.3-problems (#1394)
* Improve check on log level * Fixes #1393 * Add test for #1393 * Don't remove empty fields * Add test to verify every migration field is present * Fixes #1392 - Much simpler algorithm for hierarchization of QP * Fix debug level * Fixes #1391 * Improve URL construction * Fix hotspot count * Cleanup paginated search * Quality pass * Quality pass
1 parent f160eaa commit ebf0911

File tree

8 files changed

+159
-39
lines changed

8 files changed

+159
-39
lines changed

cli/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def parse_and_check(parser: ArgumentParser, logger_name: str = None, verify_toke
193193
log.info("sonar-tools version %s", version.PACKAGE_VERSION)
194194
if os.getenv("IN_DOCKER", "No") == "Yes":
195195
kwargs[URL] = kwargs[URL].replace("http://localhost", "http://host.docker.internal")
196-
if log.get_level() == log.DEBUG:
196+
if log.get_level() <= log.DEBUG:
197197
sanitized_args = kwargs.copy()
198198
sanitized_args[TOKEN] = utilities.redacted_token(sanitized_args[TOKEN])
199199
if "tokenTarget" in sanitized_args:

migration/migration.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def write_objects(queue: Queue, fd, object_type: str) -> None:
109109
obj_json = queue.get()
110110
done = obj_json is None
111111
if not done:
112+
obj_json = utilities.remove_nones(obj_json)
112113
if object_type in ("projects", "applications", "portfolios", "users"):
113114
if object_type == "users":
114115
key = obj_json.pop("login", None)
@@ -143,6 +144,7 @@ def __export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> N
143144
utilities.exit_fatal(f"Project key(s) '{','.join(non_existing_projects)}' do(es) not exist", errcodes.NO_SUCH_KEY)
144145

145146
calls = {
147+
"platform": [__JSON_KEY_PLATFORM, platform.basics],
146148
options.WHAT_SETTINGS: [__JSON_KEY_SETTINGS, platform.export],
147149
options.WHAT_RULES: [__JSON_KEY_RULES, rules.export],
148150
options.WHAT_PROFILES: [__JSON_KEY_PROFILES, qualityprofiles.export],
@@ -153,10 +155,9 @@ def __export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> N
153155
options.WHAT_USERS: [__JSON_KEY_USERS, users.export],
154156
options.WHAT_GROUPS: [__JSON_KEY_GROUPS, groups.export],
155157
}
156-
158+
what.append("platform")
157159
log.info("Exporting configuration from %s", kwargs[options.URL])
158160
key_list = kwargs[options.KEYS]
159-
sq_settings = {__JSON_KEY_PLATFORM: endpoint.basics()}
160161
is_first = True
161162
q = Queue(maxsize=0)
162163
with utilities.open_file(file, mode="w") as fd:
@@ -173,11 +174,10 @@ def __export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> N
173174
worker.daemon = True
174175
worker.name = f"Write{ndx[:1].upper()}{ndx[1:10]}"
175176
worker.start()
176-
sq_settings[ndx] = func(endpoint, export_settings=export_settings, key_list=key_list, write_q=q)
177+
func(endpoint, export_settings=export_settings, key_list=key_list, write_q=q)
177178
q.join()
178179
except exceptions.UnsupportedOperation as e:
179180
log.warning(e.message)
180-
sq_settings = utilities.remove_empties(sq_settings)
181181
# if not kwargs.get("dontInlineLists", False):
182182
# sq_settings = utilities.inline_lists(sq_settings, exceptions=("conditions",))
183183
print("\n}", file=fd)

sonar/hotspots.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
from __future__ import annotations
2323

24-
import math
2524
import json
2625
import re
2726
from http import HTTPStatus
@@ -85,6 +84,12 @@ def __init__(self, nbr_issues, message):
8584

8685

8786
class Hotspot(findings.Finding):
87+
"""Abstraction of the Sonar hotspot concept"""
88+
89+
SEARCH_API = "hotspots/search"
90+
MAX_PAGE_SIZE = 500
91+
MAX_SEARCH = 10000
92+
8893
def __init__(self, endpoint: pf.Platform, key: str, data: types.ApiPayload = None, from_export: bool = False) -> None:
8994
"""Constructor"""
9095
super().__init__(endpoint=endpoint, key=key, data=data, from_export=from_export)
@@ -385,16 +390,17 @@ def search(endpoint: pf.Platform, filters: types.ApiParams = None) -> dict[str,
385390
"""
386391
hotspots_list = {}
387392
new_params = sanitize_search_filters(endpoint=endpoint, params=filters)
393+
log.debug("Search hotspots with params %s", str(new_params))
388394
filters_iterations = split_search_filters(new_params)
389-
ps = 500 if "ps" not in new_params else new_params["ps"]
395+
ps = Hotspot.MAX_PAGE_SIZE if "ps" not in new_params else new_params["ps"]
390396
for inline_filters in filters_iterations:
391397
p = 1
392398
inline_filters["ps"] = ps
393399
log.debug("Searching hotspots with sanitized filters %s", str(inline_filters))
394400
while True:
395401
inline_filters["p"] = p
396402
try:
397-
data = json.loads(endpoint.get("hotspots/search", params=inline_filters, mute=(HTTPStatus.NOT_FOUND,)).text)
403+
data = json.loads(endpoint.get(Hotspot.SEARCH_API, params=inline_filters, mute=(HTTPStatus.NOT_FOUND,)).text)
398404
nbr_hotspots = util.nbr_total_elements(data)
399405
except HTTPError as e:
400406
if e.response.status_code == HTTPStatus.NOT_FOUND:
@@ -404,10 +410,10 @@ def search(endpoint: pf.Platform, filters: types.ApiParams = None) -> dict[str,
404410
raise e
405411
nbr_pages = util.nbr_pages(data)
406412
log.debug("Number of hotspots: %d - Page: %d/%d", nbr_hotspots, inline_filters["p"], nbr_pages)
407-
if nbr_hotspots > 10000:
413+
if nbr_hotspots > Hotspot.MAX_SEARCH:
408414
raise TooManyHotspotsError(
409415
nbr_hotspots,
410-
f"{nbr_hotspots} hotpots returned by api/hotspots/search, " "this is more than the max 10000 possible",
416+
f"{nbr_hotspots} hotpots returned by api/{Hotspot.SEARCH_API}, this is more than the max {Hotspot.MAX_SEARCH} possible",
411417
)
412418

413419
for i in data["hotspots"]:
@@ -500,6 +506,7 @@ def count(endpoint: pf.Platform, **kwargs) -> int:
500506
"""Returns number of hotspots of a search"""
501507
params = {} if not kwargs else kwargs.copy()
502508
params["ps"] = 1
503-
nbr_hotspots = len(search(endpoint=endpoint, filters=params))
509+
params = sanitize_search_filters(endpoint, params)
510+
nbr_hotspots = util.nbr_total_elements(json.loads(endpoint.get(Hotspot.SEARCH_API, params=params, mute=(HTTPStatus.NOT_FOUND,)).text))
504511
log.debug("Hotspot counts with filters %s returned %d hotspots", str(kwargs), nbr_hotspots)
505512
return nbr_hotspots

sonar/platform.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import datetime
3434
import json
3535
import tempfile
36-
import logging
3736
import requests
3837
import jprops
3938
from requests.exceptions import HTTPError
@@ -226,7 +225,7 @@ def __run_request(
226225
if kwargs.get("with_organization", True):
227226
params["organization"] = self.organization
228227
req_type, url = "", ""
229-
if log.get_level() >= logging.DEBUG:
228+
if log.get_level() <= log.DEBUG:
230229
req_type = getattr(request, "__name__", repr(request)).upper()
231230
url = self.__urlstring(api, params)
232231
log.debug("%s: %s", req_type, url)
@@ -380,14 +379,17 @@ def __urlstring(self, api: str, params: types.ApiParams) -> str:
380379
url_prefix = f"{str(self)}{api}"
381380
if params is None:
382381
return url_prefix
382+
temp_params = params.copy()
383383
for p in params:
384384
if params[p] is None:
385385
continue
386386
sep = "?" if first else "&"
387387
first = False
388-
if isinstance(params[p], datetime.date):
389-
params[p] = util.format_date(params[p])
390-
url_prefix += f"{sep}{p}={requests.utils.quote(str(params[p]))}"
388+
if isinstance(temp_params[p], datetime.date):
389+
temp_params[p] = util.format_date(temp_params[p])
390+
elif isinstance(temp_params[p], (list, tuple, set)):
391+
temp_params[p] = ",".join(temp_params[p])
392+
url_prefix += f"{sep}{p}={requests.utils.quote(str(temp_params[p]))}"
391393
return url_prefix
392394

393395
def webhooks(self) -> dict[str, object]:
@@ -886,3 +888,14 @@ def export(
886888
write_q.put(exp)
887889
write_q.put(None)
888890
return exp
891+
892+
893+
def basics(
894+
endpoint: Platform, export_settings: types.ConfigSettings, key_list: Optional[types.KeyList] = None, write_q: Optional[Queue] = None
895+
) -> types.ObjectJsonRepr:
896+
"""Returns an endpooint basic info (license, edition, version etc..)"""
897+
exp = endpoint.basics()
898+
if write_q:
899+
write_q.put(exp)
900+
write_q.put(None)
901+
return exp

sonar/portfolios.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
_CREATE_API = "views/create"
5151
_GET_API = "views/show"
5252

53-
MAX_PAGE_SIZE = 500
5453
_PORTFOLIO_QUALIFIER = "VW"
5554
_SUBPORTFOLIO_QUALIFIER = "SVW"
5655

@@ -74,6 +73,7 @@
7473
"visibility",
7574
"permissions",
7675
"projects",
76+
"projectsList",
7777
"portfolios",
7878
"subPortfolios",
7979
"applications",
@@ -88,6 +88,8 @@ class Portfolio(aggregations.Aggregation):
8888
SEARCH_API = "views/search"
8989
SEARCH_KEY_FIELD = "key"
9090
SEARCH_RETURN_FIELD = "components"
91+
MAX_PAGE_SIZE = 500
92+
MAX_SEARCH = 10000
9193

9294
_OBJECTS = {}
9395

@@ -301,7 +303,7 @@ def get_components(self) -> types.ApiPayload:
301303
"component": self.key,
302304
"metricKeys": "ncloc",
303305
"strategy": "children",
304-
"ps": 500,
306+
"ps": Portfolio.MAX_PAGE_SIZE,
305307
},
306308
).text
307309
)
@@ -359,6 +361,8 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr
359361
if mode and "none" not in mode:
360362
json_data["projects"] = mode
361363
json_data["applications"] = self._applications
364+
if export_settings.get("MODE", "") == "MIGRATION":
365+
json_data["projectsList"] = self.get_project_list()
362366
return json_data
363367

364368
def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
@@ -563,6 +567,33 @@ def recompute(self) -> bool:
563567
key = self._root_portfolio.key if self._root_portfolio else self.key
564568
return self.post("views/refresh", params={"key": key}).ok
565569

570+
def get_project_list(self) -> list[str]:
571+
log.debug("Search %s projects list", str(self))
572+
proj_key_list = []
573+
page = 0
574+
params = {"component": self.key, "ps": Portfolio.MAX_PAGE_SIZE, "qualifiers": "TRK", "strategy": "leaves", "metricKeys": "ncloc"}
575+
while True:
576+
page += 1
577+
params["p"] = page
578+
try:
579+
data = json.loads(self.get("api/measures/component_tree", params=params).text)
580+
nbr_projects = util.nbr_total_elements(data)
581+
proj_key_list += [c["refKey"] for c in data["components"]]
582+
except HTTPError as e:
583+
if e.response.status_code in (HTTPStatus.BAD_REQUEST, HTTPStatus.NOT_FOUND):
584+
log.warning("HTTP Error %s while collecting projects from %s, stopping collection", str(e), str(self))
585+
else:
586+
log.critical("HTTP Error %s while collecting projects from %s, proceeding anyway", str(e), str(self))
587+
break
588+
nbr_pages = util.nbr_pages(data)
589+
log.debug("Number of projects: %d - Page: %d/%d", nbr_projects, page, nbr_pages)
590+
if nbr_projects > Portfolio.MAX_SEARCH:
591+
log.warning("Can't collect more than %d projects from %s", Portfolio.MAX_SEARCH, str(self))
592+
if page >= nbr_pages or page >= Portfolio.MAX_SEARCH / Portfolio.MAX_PAGE_SIZE:
593+
break
594+
log.debug("%s projects list = %s", str(self), str(proj_key_list))
595+
return proj_key_list
596+
566597
def update(self, data: dict[str, str], recurse: bool) -> None:
567598
"""Updates a portfolio with sonar-config JSON data, if recurse is true, this recurses in sub portfolios"""
568599
log.debug("Updating %s with %s", str(self), util.json_dump(data))

sonar/projects.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1486,7 +1486,8 @@ def __export_thread(queue: Queue[Project], results: dict[str, str], export_setti
14861486
with _CLASS_LOCK:
14871487
export_settings["EXPORTED"] += 1
14881488
nb, tot = export_settings["EXPORTED"], export_settings["NBR_PROJECTS"]
1489-
if nb % 10 == 0 or nb == tot:
1489+
log.debug("%d/%d projects exported (%d%%)", nb, tot, (nb * 100) // tot)
1490+
if nb % 10 == 0 or tot - nb < 10:
14901491
log.info("%d/%d projects exported (%d%%)", nb, tot, (nb * 100) // tot)
14911492
queue.task_done()
14921493

sonar/qualityprofiles.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,27 @@ def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings = None) ->
550550
return problems
551551

552552

553-
def hierarchize(qp_list: dict[str, str], endpoint: pf.Platform) -> types.ObjectJsonRepr:
553+
def hierarchize_language(qp_list: dict[str, str]) -> types.ObjectJsonRepr:
554+
"""Organizes a flat list of quality profiles in inheritance hierarchy"""
555+
log.debug("Organizing QP list %s in hierarchy", str(qp_list.keys()))
556+
hierarchy = qp_list.copy()
557+
to_remove = []
558+
for qp_name, qp_json_data in hierarchy.items():
559+
if "parentName" in qp_json_data:
560+
if qp_json_data["parentName"] not in hierarchy:
561+
log.critical("Can't find parent %s in quality profiles", qp_json_data["parentName"])
562+
continue
563+
parent_qp = hierarchy[qp_json_data.pop("parentName")]
564+
if _CHILDREN_KEY not in parent_qp:
565+
parent_qp[_CHILDREN_KEY] = {}
566+
parent_qp[_CHILDREN_KEY][qp_name] = qp_json_data
567+
to_remove.append(qp_name)
568+
for qp_name in to_remove:
569+
hierarchy.pop(qp_name)
570+
return hierarchy
571+
572+
573+
def hierarchize(qp_list: types.ObjectJsonRepr) -> types.ObjectJsonRepr:
554574
"""Organize a flat list of QP in hierarchical (inheritance) fashion
555575
556576
:param qp_list: List of quality profiles
@@ -559,31 +579,17 @@ def hierarchize(qp_list: dict[str, str], endpoint: pf.Platform) -> types.ObjectJ
559579
:rtype: {<language>: {<qp_name>: {"children": <qp_list>; <qp_data>}}}
560580
"""
561581
log.info("Organizing quality profiles in hierarchy")
562-
for lang, qpl in qp_list.copy().items():
563-
for qp_name, qp_json_data in qpl.copy().items():
564-
log.debug("Treating %s:%s", lang, qp_name)
565-
if "parentName" not in qp_json_data:
566-
continue
567-
parent_qp_name = qp_json_data["parentName"]
568-
qp_json_data.pop("rules", None)
569-
log.debug("QP name '%s:%s' has parent '%s'", lang, qp_name, qp_json_data["parentName"])
570-
if _CHILDREN_KEY not in qp_list[lang][qp_json_data["parentName"]]:
571-
qp_list[lang][qp_json_data["parentName"]][_CHILDREN_KEY] = {}
572-
573-
this_qp = get_object(endpoint=endpoint, name=qp_name, language=lang)
574-
(_, qp_json_data) = this_qp.diff(get_object(endpoint=endpoint, name=parent_qp_name, language=lang), qp_json_data)
575-
qp_list[lang][parent_qp_name][_CHILDREN_KEY][qp_name] = qp_json_data
576-
qp_list[lang].pop(qp_name)
577-
qp_json_data.pop("parentName")
578-
return qp_list
582+
hierarchy = {}
583+
for lang, lang_qp_list in qp_list.items():
584+
hierarchy[lang] = hierarchize_language(lang_qp_list)
585+
return hierarchy
579586

580587

581588
def export(
582589
endpoint: pf.Platform, export_settings: types.ConfigSettings, key_list: Optional[types.KeyList] = None, write_q: Optional[Queue] = None
583590
) -> types.ObjectJsonRepr:
584591
"""Exports all or a list of quality profiles configuration as dict
585592
586-
:param Platform endpoint: reference to the SonarQube platform
587593
:param ConfigSettings export_settings: Export parameters
588594
:param KeyList key_list: Unused
589595
:return: Dict of quality profiles JSON representation
@@ -599,7 +605,7 @@ def export(
599605
if lang not in qp_list:
600606
qp_list[lang] = {}
601607
qp_list[lang][name] = json_data
602-
qp_list = hierarchize(qp_list, endpoint)
608+
qp_list = hierarchize(qp_list)
603609
if write_q:
604610
write_q.put(qp_list)
605611
write_q.put(None)

test/test_migration.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,36 @@ def test_migration() -> None:
5757
with open(file=util.JSON_FILE, mode="r", encoding="utf-8") as fh:
5858
json_config = json.loads(fh.read())
5959

60+
for item in (
61+
"platform",
62+
"globalSettings",
63+
"rules",
64+
"qualityProfiles",
65+
"qualityGates",
66+
"projects",
67+
"applications",
68+
"portfolios",
69+
"users",
70+
"groups",
71+
):
72+
assert item in json_config
73+
74+
for p in json_config["projects"].values():
75+
for item in (
76+
"backgroundTasks",
77+
"branches",
78+
"detectedCi",
79+
"lastAnalysis",
80+
"issues",
81+
"hotspots",
82+
"name",
83+
"ncloc",
84+
"permissions",
85+
"revision",
86+
"visibility",
87+
):
88+
assert item in p
89+
6090
u = json_config["users"]["admin"]
6191
assert "sonar-users" in u["groups"]
6292
assert u["local"] and u["active"]
@@ -104,3 +134,35 @@ def test_migration() -> None:
104134
assert json_config["projects"]["demo:github-actions-cli"]["detectedCi"] == "Github Actions"
105135

106136
util.clean(util.JSON_FILE)
137+
138+
139+
def test_migration_skip_issues() -> None:
140+
"""test_config_export"""
141+
util.clean(util.JSON_FILE)
142+
with pytest.raises(SystemExit) as e:
143+
with patch.object(sys, "argv", OPTS + ["--skipIssues"]):
144+
migration.main()
145+
assert int(str(e.value)) == errcodes.OK
146+
assert util.file_not_empty(util.JSON_FILE)
147+
with open(file=util.JSON_FILE, mode="r", encoding="utf-8") as fh:
148+
json_config = json.loads(fh.read())
149+
150+
for item in (
151+
"platform",
152+
"globalSettings",
153+
"rules",
154+
"qualityProfiles",
155+
"qualityGates",
156+
"projects",
157+
"applications",
158+
"portfolios",
159+
"users",
160+
"groups",
161+
):
162+
assert item in json_config
163+
164+
for p in json_config["projects"].values():
165+
assert "issues" not in p
166+
assert "hotspots" not in p
167+
168+
util.clean(util.JSON_FILE)

0 commit comments

Comments
 (0)