Skip to content

Commit ca4eb8a

Browse files
authored
Stream sonar-audit output
Fixes #1395 (#1419)
1 parent 75ac53e commit ca4eb8a

File tree

2 files changed

+113
-41
lines changed

2 files changed

+113
-41
lines changed

cli/audit.py

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
"""
2626
import sys
2727
import json
28+
import csv
29+
from typing import TextIO
30+
from threading import Thread, Lock
31+
from queue import Queue
2832

2933
from cli import options
3034

31-
from sonar.util.types import ConfigSettings
3235
from sonar import errcodes, exceptions
3336
from sonar.util import types
3437
import sonar.logging as log
@@ -47,6 +50,8 @@
4750
options.WHAT_PORTFOLIOS,
4851
]
4952

53+
_WRITE_LOCK = Lock()
54+
5055

5156
def _audit_sif(sysinfo, audit_settings):
5257
log.info("Auditing SIF file '%s'", sysinfo)
@@ -67,40 +72,86 @@ def _audit_sif(sysinfo, audit_settings):
6772
return (server_id, sif_obj.audit(audit_settings))
6873

6974

75+
def write_problems(queue: Queue[list[problem.Problem]], fd: TextIO, settings: types.ConfigSettings) -> None:
76+
"""
77+
Thread to write problems in a CSV file
78+
"""
79+
csvwriter = csv.writer(fd, delimiter=settings.get("CSV_DELIMITER", ","))
80+
server_id = settings.get("SERVER_ID", None)
81+
with_url = settings.get("WITH_URL", False)
82+
while True:
83+
problems = queue.get()
84+
if problems is None:
85+
queue.task_done()
86+
break
87+
for p in problems:
88+
data = []
89+
if server_id is not None:
90+
data = [server_id]
91+
data += list(p.to_json(with_url).values())
92+
csvwriter.writerow(data)
93+
queue.task_done()
94+
log.info("Writing audit probelms complete")
95+
96+
7097
def _audit_sq(
71-
sq: platform.Platform, settings: ConfigSettings, what_to_audit: list[str] = None, key_list: types.KeyList = None
98+
sq: platform.Platform, settings: types.ConfigSettings, what_to_audit: list[str] = None, key_list: types.KeyList = None
7299
) -> list[problem.Problem]:
73100
"""Audits a SonarQube/Cloud platform"""
74101
problems = []
102+
q = Queue(maxsize=0)
75103
everything = False
76104
if not what_to_audit:
77105
everything = True
78106
what_to_audit = options.WHAT_AUDITABLE
79-
if options.WHAT_PROJECTS in what_to_audit:
80-
problems += projects.audit(endpoint=sq, audit_settings=settings, key_list=key_list)
81-
if options.WHAT_PROFILES in what_to_audit:
82-
problems += qualityprofiles.audit(endpoint=sq, audit_settings=settings)
83-
if options.WHAT_GATES in what_to_audit:
84-
problems += qualitygates.audit(endpoint=sq, audit_settings=settings)
85-
if options.WHAT_SETTINGS in what_to_audit:
86-
problems += sq.audit(audit_settings=settings)
87-
if options.WHAT_USERS in what_to_audit:
88-
problems += users.audit(endpoint=sq, audit_settings=settings)
89-
if options.WHAT_GROUPS in what_to_audit:
90-
problems += groups.audit(endpoint=sq, audit_settings=settings)
91-
if options.WHAT_PORTFOLIOS in what_to_audit:
92-
try:
93-
problems += portfolios.audit(endpoint=sq, audit_settings=settings, key_list=key_list)
94-
except exceptions.UnsupportedOperation:
95-
if not everything:
96-
log.warning("No portfolios in %s edition, audit of portfolios ignored", sq.edition())
97-
if options.WHAT_APPS in what_to_audit:
98-
try:
99-
problems += applications.audit(endpoint=sq, audit_settings=settings, key_list=key_list)
100-
except exceptions.UnsupportedOperation:
101-
if not everything:
102-
log.warning("No applications in %s edition, audit of portfolios ignored", sq.edition())
103107

108+
with util.open_file(settings.get("FILE", None), mode="w") as fd:
109+
worker = Thread(target=write_problems, args=(q, fd, settings))
110+
worker.daemon = True
111+
worker.name = "WriteProblems"
112+
worker.start()
113+
if options.WHAT_PROJECTS in what_to_audit:
114+
pbs = projects.audit(endpoint=sq, audit_settings=settings, key_list=key_list, write_q=q)
115+
q.put(pbs)
116+
problems += pbs
117+
if options.WHAT_PROFILES in what_to_audit:
118+
pbs = qualityprofiles.audit(endpoint=sq, audit_settings=settings)
119+
q.put(pbs)
120+
problems += pbs
121+
if options.WHAT_GATES in what_to_audit:
122+
pbs = qualitygates.audit(endpoint=sq, audit_settings=settings)
123+
q.put(pbs)
124+
problems += pbs
125+
if options.WHAT_SETTINGS in what_to_audit:
126+
pbs = sq.audit(audit_settings=settings)
127+
q.put(pbs)
128+
problems += pbs
129+
if options.WHAT_USERS in what_to_audit:
130+
pbs = users.audit(endpoint=sq, audit_settings=settings)
131+
q.put(pbs)
132+
problems += pbs
133+
if options.WHAT_GROUPS in what_to_audit:
134+
pbs = groups.audit(endpoint=sq, audit_settings=settings)
135+
q.put(pbs)
136+
problems += pbs
137+
if options.WHAT_PORTFOLIOS in what_to_audit:
138+
try:
139+
pbs = portfolios.audit(endpoint=sq, audit_settings=settings, key_list=key_list)
140+
q.put(pbs)
141+
problems += pbs
142+
except exceptions.UnsupportedOperation:
143+
if not everything:
144+
log.warning("No portfolios in %s edition, audit of portfolios ignored", sq.edition())
145+
if options.WHAT_APPS in what_to_audit:
146+
try:
147+
pbs = applications.audit(endpoint=sq, audit_settings=settings, key_list=key_list)
148+
q.put(pbs)
149+
problems += pbs
150+
except exceptions.UnsupportedOperation:
151+
if not everything:
152+
log.warning("No applications in %s edition, audit of portfolios ignored", sq.edition())
153+
q.put(None)
154+
q.join()
104155
return problems
105156

106157

@@ -138,16 +189,22 @@ def main():
138189
config.configure()
139190
sys.exit(0)
140191

192+
ofile = kwargs.pop(options.REPORT_FILE)
193+
settings["FILE"] = ofile
194+
settings["CSV_DELIMITER"] = kwargs[options.CSV_SEPARATOR]
195+
settings["WITH_URL"] = kwargs[options.WITH_URL]
196+
141197
if kwargs.get("sif", None) is not None:
142198
err = errcodes.SIF_AUDIT_ERROR
143199
try:
144200
(server_id, problems) = _audit_sif(kwargs["sif"], settings)
201+
settings["SERVER_ID"] = server_id
145202
except json.decoder.JSONDecodeError:
146203
util.exit_fatal(f"File {kwargs['sif']} does not seem to be a legit JSON file, aborting...", err)
147204
except FileNotFoundError:
148205
util.exit_fatal(f"File {kwargs['sif']} does not exist, aborting...", err)
149206
except PermissionError:
150-
util.exit_fatal(f"No permissiont to open file {kwargs['sif']}, aborting...", err)
207+
util.exit_fatal(f"No permission to open file {kwargs['sif']}, aborting...", err)
151208
except sif.NotSystemInfo:
152209
util.exit_fatal(f"File {kwargs['sif']} does not seem to be a system info or support info file, aborting...", err)
153210
else:
@@ -157,6 +214,7 @@ def main():
157214
except exceptions.ConnectionError as e:
158215
util.exit_fatal(e.message, e.errcode)
159216
server_id = sq.server_id()
217+
settings["SERVER_ID"] = server_id
160218
util.check_token(kwargs[options.TOKEN])
161219
key_list = kwargs[options.KEYS]
162220
if key_list is not None and len(key_list) > 0 and "projects" in util.csv_to_list(kwargs[options.WHAT]):
@@ -168,7 +226,6 @@ def main():
168226
except exceptions.ObjectNotFound as e:
169227
util.exit_fatal(e.message, errcodes.NO_SUCH_KEY)
170228

171-
ofile = kwargs.pop(options.REPORT_FILE)
172229
if problems:
173230
log.warning("%d issues found during audit", len(problems))
174231
else:

sonar/projects.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ def __audit_scanner(self, audit_settings: types.ConfigSettings) -> list[Problem]
643643
return []
644644
return [Problem(get_rule(RuleId.PROJ_WRONG_SCANNER), self, str(self), proj_type, scanner)]
645645

646-
def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
646+
def audit(self, audit_settings: types.ConfigSettings, write_q: Queue[list[Problem]]) -> list[Problem]:
647647
"""Audits a project and returns the list of problems found
648648
649649
:param dict audit_settings: Options of what to audit and thresholds to raise problems
@@ -666,6 +666,8 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
666666
problems += self.__audit_scanner(audit_settings)
667667
except (ConnectionError, RequestException) as e:
668668
log.error("%s while auditing %s", util.error_msg(e), str(self))
669+
if write_q:
670+
write_q.put(problems)
669671
return problems
670672

671673
def export_zip(self, timeout: int = 180) -> dict[str, str]:
@@ -1406,32 +1408,43 @@ def get_list(endpoint: pf.Platform, key_list: types.KeyList = None, use_cache: b
14061408
return {key: Project.get_object(endpoint, key) for key in util.csv_to_list(key_list)}
14071409

14081410

1409-
def __audit_thread(queue: Queue[Project], results: list[Problem], audit_settings: types.ConfigSettings, bindings: dict[str, str]) -> None:
1411+
def __audit_thread(
1412+
queue: Queue[Project],
1413+
results: list[Problem],
1414+
audit_settings: types.ConfigSettings,
1415+
bindings: dict[str, str],
1416+
write_q: Optional[Queue[list[Problem]]],
1417+
) -> None:
14101418
"""Audit callback function for multitheaded audit"""
14111419
audit_bindings = audit_settings.get("audit.projects.bindings", True)
14121420
while not queue.empty():
14131421
log.debug("Picking from the queue")
14141422
project = queue.get()
1415-
results += project.audit(audit_settings)
1423+
problems = project.audit(audit_settings, write_q)
14161424
try:
14171425
if project.endpoint.edition() == "community" or not audit_bindings or project.is_part_of_monorepo():
14181426
queue.task_done()
14191427
log.debug("%s audit done", str(project))
14201428
continue
14211429
bindkey = project.binding_key()
14221430
if bindkey and bindkey in bindings:
1423-
results.append(Problem(get_rule(RuleId.PROJ_DUPLICATE_BINDING), project, str(project), str(bindings[bindkey])))
1431+
problems.append(Problem(get_rule(RuleId.PROJ_DUPLICATE_BINDING), project, str(project), str(bindings[bindkey])))
14241432
else:
14251433
bindings[bindkey] = project
14261434
except (ConnectionError, RequestException) as e:
14271435
log.error("%s while auditing %s", util.error_msg(e), str(project))
14281436
__increment_processed(audit_settings)
1437+
if write_q:
1438+
write_q.put(problems)
1439+
results += problems
14291440
queue.task_done()
14301441

14311442
log.debug("Audit of projects completed")
14321443

14331444

1434-
def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, key_list: types.KeyList = None) -> list[Problem]:
1445+
def audit(
1446+
endpoint: pf.Platform, audit_settings: types.ConfigSettings, key_list: types.KeyList = None, write_q: Optional[Queue[list[Problem]]] = None
1447+
) -> list[Problem]:
14351448
"""Audits all or a list of projects
14361449
14371450
:param Platform endpoint: reference to the SonarQube platform
@@ -1451,19 +1464,21 @@ def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, key_list:
14511464
bindings = {}
14521465
for i in range(audit_settings.get("threads", 1)):
14531466
log.debug("Starting project audit thread %d", i)
1454-
worker = Thread(target=__audit_thread, args=(q, problems, audit_settings, bindings))
1467+
worker = Thread(target=__audit_thread, args=(q, problems, audit_settings, bindings, write_q))
14551468
worker.setDaemon(True)
14561469
worker.setName(f"ProjectAudit{i}")
14571470
worker.start()
14581471
q.join()
14591472
if not audit_settings.get("audit.projects.duplicates", True):
14601473
log.info("Project duplicates auditing was disabled by configuration")
1461-
return problems
1462-
for key, p in plist.items():
1463-
log.debug("Auditing for potential duplicate projects")
1464-
for key2 in plist:
1465-
if key2 != key and re.match(key2, key):
1466-
problems.append(Problem(get_rule(RuleId.PROJ_DUPLICATE), p, str(p), key2))
1474+
else:
1475+
log.info("Auditing for potential duplicate projects")
1476+
for key, p in plist.items():
1477+
for key2 in plist:
1478+
if key2 != key and re.match(key2, key):
1479+
problems.append(Problem(get_rule(RuleId.PROJ_DUPLICATE), p, str(p), key2))
1480+
if write_q:
1481+
write_q.put(problems)
14671482
return problems
14681483

14691484

@@ -1508,7 +1523,7 @@ def export(
15081523
q = Queue(maxsize=0)
15091524
proj_list = get_list(endpoint=endpoint, key_list=key_list)
15101525
export_settings["NBR_PROJECTS"] = len(proj_list)
1511-
export_settings["EXPORTED"] = 0
1526+
export_settings["PROCESSED"] = 0
15121527
log.info("Exporting %d projects", export_settings["NBR_PROJECTS"])
15131528
for p in proj_list.values():
15141529
q.put(p)

0 commit comments

Comments
 (0)