Skip to content

Commit b5d0fe1

Browse files
authored
Merge pull request #451 from okorach:handle-non-existing-project-keys
Handle non existing project keys gracefully
2 parents 7547831 + 2e118fd commit b5d0fe1

File tree

12 files changed

+96
-97
lines changed

12 files changed

+96
-97
lines changed

README.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ Using login/password is not possible.
5353
The user corresponding to the token must have enough permissions to achieve the tool tasks
5454
- `-v` : Logging verbosity level (`WARN`, `ÌNFO` or `DEBUG`). The default is `INFO`.
5555
`ERROR` and above is always active.
56-
-
56+
57+
See common [error exit codes](#exit-codes) at the bottom of this page
5758

5859
# <a name="sonar-audit"></a>sonar-audit
5960

@@ -243,16 +244,19 @@ export SONAR_TOKEN=15ee09df11fb9b8234b7a1f1ac5fce2e4e93d75d
243244
sonar-projects-import -f exported_projects.csv
244245
```
245246

246-
# Tools coming soon
247-
248-
## sonar-issues-recover
247+
# <a name="exit-codes"></a>Exit codes
249248

250-
Tries to recover issues that were mistakenly closed following a scan with incorrect parameters. This tool is only useful for SonarQube instances in version 7.9.x and lower since this feature is built-in with SonarQube 8.x and higher
249+
When tools complete successfully they return exit code 0. En case of fatal error the following exit codes may be returned:
250+
- Code 1: Authentication error (Incorrect token provided)
251+
- Code 2: Authorization error (provided token has insufficient permissions)
252+
- Code 3: Other general Sonar API HTTP error
253+
- Code 4: No token provided
254+
- Code 5: Non existing project key provided
255+
- Code 6: Incorrect finding search criteria provided
256+
- Code 7: Unsupported operation requested (because of SonarQube edition or configuration)
257+
- Code 8: Audit rule loading failed (at startup)
258+
- Code 9: SIF audit error (file not found, can't open file, not a legit JSON file, ...)
251259

252-
Issue recovery means:
253-
- Reapplying all transitions to the issue to reach its final state before close (Usually *False positive* or *Won't Fix*)
254-
- Reapplying all manual comments
255-
- Reapplying all severity or issue type change
256260

257261
### :information_source: Limitations
258262
- The script has to be run before the closed issue purge period (SonarQube parameter `sonar.dbcleaner.daysBeforeDeletingClosedIssues` whose default value is **30 days**)

sonar/audit/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
#
2020
import sys
2121
from sonar.audit import rules
22+
from sonar import utilities, options
2223

2324
try:
2425
rules.load()
2526
except rules.RuleConfigError as e:
26-
print(e.message)
27-
sys.exit(3)
27+
utilities.exit_fatal(e.message, options.ERR_RULES_LOADING_FAILED)

sonar/audit/audit.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
'''
2626
import sys
2727
import json
28+
2829
import sonar.portfolios as pf
2930
import sonar.applications as apps
30-
from sonar import users, groups, version, env, qualityprofiles, qualitygates, projects, sif
31+
from sonar import users, groups, version, env, qualityprofiles, qualitygates, projects, sif, options
3132
import sonar.utilities as util
3233
from sonar.audit import problem, config
3334

@@ -94,8 +95,7 @@ def __parser_args(desc):
9495
'or outputs to stdout if it already exist')
9596
args = parser.parse_args()
9697
if args.sif is None and args.config is None and args.token is None:
97-
util.logger.critical("Token is missing (Argument -t/--token) when not analyzing local SIF")
98-
sys.exit(4)
98+
util.exit_fatal("Token is missing (Argument -t/--token) when not analyzing local SIF", options.ERR_TOKEN_MISSING)
9999
return args
100100

101101
def main():
@@ -114,17 +114,13 @@ def main():
114114
try:
115115
problems = _audit_sif(kwargs['sif'])
116116
except json.decoder.JSONDecodeError:
117-
print(f"File {kwargs['sif']} does not seem to be a legit JSON file, aborting...")
118-
sys.exit(3)
117+
util.exit_fatal(f"File {kwargs['sif']} does not seem to be a legit JSON file, aborting...", options.ERR_SIF_AUDIT_ERROR)
119118
except FileNotFoundError:
120-
print(f"File {kwargs['sif']} does not exist, aborting...")
121-
sys.exit(4)
119+
util.exit_fatal(f"File {kwargs['sif']} does not exist, aborting...", options.ERR_SIF_AUDIT_ERROR)
122120
except PermissionError:
123-
print(f"No permissiont to open file {kwargs['sif']}, aborting...")
124-
sys.exit(5)
121+
util.exit_fatal(f"No permissiont to open file {kwargs['sif']}, aborting...", options.ERR_SIF_AUDIT_ERROR)
125122
except sif.NotSystemInfo:
126-
print(f"File {kwargs['sif']} does not seem to be a system info or support info file, aborting...")
127-
sys.exit(6)
123+
util.exit_fatal(f"File {kwargs['sif']} does not seem to be a system info or support info file, aborting...", options.ERR_SIF_AUDIT_ERROR)
128124
else:
129125
problems = _audit_sq(sq, settings, args.what)
130126

sonar/env.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,14 @@
2929
import requests
3030

3131
import sonar.utilities as util
32-
32+
from sonar import options
3333
from sonar.audit import rules, config
3434
import sonar.audit.severities as sev
3535
import sonar.audit.types as typ
3636
import sonar.audit.problem as pb
3737

3838
from sonar import sif
3939

40-
AUTHENTICATION_ERROR_MSG = "Authentication error. Is token valid ?"
41-
AUTORIZATION_ERROR_MSG = "Insufficient permissions to perform operation"
42-
HTTP_FATAL_ERROR_MSG = "HTTP fatal error %d - %s"
4340
WRONG_CONFIG_MSG = "Audit config property %s has wrong value %s, skipping audit"
4441

4542
_NON_EXISTING_SETTING_SKIPPED = "Setting %s does not exist, skipping..."
@@ -129,12 +126,11 @@ def get(self, api, params=None, exit_on_error=True):
129126
else:
130127
r = requests.get(url=self.url + api, auth=self.credentials(), params=params)
131128
r.raise_for_status()
132-
except requests.exceptions.HTTPError as errh:
129+
except requests.exceptions.HTTPError:
133130
if exit_on_error:
134-
_log_and_exit(r.status_code, errh)
131+
_log_and_exit(r.status_code)
135132
except requests.RequestException as e:
136-
util.logger.error(str(e))
137-
raise SystemExit(e) from e
133+
util.exit_fatal(str(e), options.ERR_SONAR_API)
138134
return r
139135

140136
def post(self, api, params=None):
@@ -146,11 +142,10 @@ def post(self, api, params=None):
146142
else:
147143
r = requests.post(url=self.url + api, auth=self.credentials(), params=params)
148144
r.raise_for_status()
149-
except requests.exceptions.HTTPError as errh:
150-
_log_and_exit(r.status_code, errh)
145+
except requests.exceptions.HTTPError:
146+
_log_and_exit(r.status_code)
151147
except requests.RequestException as e:
152-
util.logger.error(str(e))
153-
raise SystemExit(e) from e
148+
util.exit_fatal(str(e), options.ERR_SONAR_API)
154149
return r
155150

156151
def delete(self, api, params=None):
@@ -162,11 +157,10 @@ def delete(self, api, params=None):
162157
else:
163158
r = requests.delete(url=self.url + api, auth=self.credentials(), params=params)
164159
r.raise_for_status()
165-
except requests.exceptions.HTTPError as errh:
166-
_log_and_exit(r.status_code, errh)
160+
except requests.exceptions.HTTPError:
161+
_log_and_exit(r.status_code)
167162
except requests.RequestException as e:
168-
util.logger.error(str(e))
169-
raise SystemExit(e) from e
163+
util.exit_fatal(str(e), options.ERR_SONAR_API)
170164

171165
def urlstring(self, api, params):
172166
first = True
@@ -247,9 +241,7 @@ def _audit_admin_password(self):
247241
else:
248242
util.logger.info("User 'admin' default password has been changed")
249243
except requests.RequestException as e:
250-
util.logger.error("HTTP request exception for %s/%s: %s", self.url,
251-
'api/authentication/validate', str(e))
252-
raise
244+
util.exit_fatal(str(e), options.ERR_SONAR_API)
253245
return problems
254246

255247
def __get_permissions(self, perm_type):
@@ -371,16 +363,13 @@ def _normalize_api(api):
371363
return api
372364

373365

374-
def _log_and_exit(code, err):
366+
def _log_and_exit(code):
375367
if code == 401:
376-
util.logger.fatal(AUTHENTICATION_ERROR_MSG)
377-
raise SystemExit(err)
368+
util.exit_fatal(f"HTTP error {code} - Authentication error. Is token valid ?", options.ERR_SONAR_API_AUTHENTICATION)
378369
if code == 403:
379-
util.logger.fatal(AUTORIZATION_ERROR_MSG)
380-
raise SystemExit(err)
370+
util.exit_fatal(f"HTTP error {code} - Insufficient permissions to perform operation", options.ERR_SONAR_API_AUTHORIZATION)
381371
if (code // 100) != 2:
382-
util.logger.fatal(HTTP_FATAL_ERROR_MSG, code, err)
383-
raise SystemExit(err)
372+
util.exit_fatal(f"HTTP error {code} - Exiting", options.ERR_SONAR_API)
384373

385374

386375
def get(api, params=None, ctxt=None, exit_on_error=True):

sonar/findings/export.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -160,20 +160,20 @@ def __get_list(project, list_str, list_type):
160160
def __verify_inputs(params):
161161
diff = util.difference(util.csv_to_list(params.get('resolutions', None)), issues.RESOLUTIONS + hotspots.RESOLUTIONS)
162162
if diff:
163-
util.logger.fatal("Resolutions %s are not legit status", str(diff))
164-
return False
163+
util.exit_fatal(f"Resolutions {str(diff)} are not legit resolutions", options.ERR_WRONG_SEARCH_CRITERIA)
164+
165165
diff = util.difference(util.csv_to_list(params.get('statuses', None)), issues.STATUSES + hotspots.STATUSES)
166166
if diff:
167-
util.logger.fatal("Status %s are not legit status", str(diff))
168-
return False
167+
util.exit_fatal(f"Statuses {str(diff)} are not legit statuses", options.ERR_WRONG_SEARCH_CRITERIA)
168+
169169
diff = util.difference(util.csv_to_list(params.get('severities', None)), issues.SEVERITIES + hotspots.SEVERITIES)
170170
if diff:
171-
util.logger.fatal("Severities %s are not legit severities", str(diff))
172-
return False
171+
util.exit_fatal(f"Severities {str(diff)} are not legit severities", options.ERR_WRONG_SEARCH_CRITERIA)
172+
173173
diff = util.difference(util.csv_to_list(params.get('types', None)), issues.TYPES + hotspots.TYPES)
174174
if diff:
175-
util.logger.fatal("Types %s are not legit types", str(diff))
176-
return False
175+
util.exit_fatal(f"Types {str(diff)} are not legit types", options.ERR_WRONG_SEARCH_CRITERIA)
176+
177177
return True
178178

179179

@@ -230,20 +230,15 @@ def main():
230230

231231
project_key = kwargs.get('projectKeys', None)
232232
params = util.remove_nones(kwargs.copy())
233-
if not __verify_inputs(params):
234-
sys.exit(2)
233+
__verify_inputs(params)
234+
235235
for p in ('statuses', 'createdAfter', 'createdBefore', 'resolutions', 'severities', 'types', 'tags'):
236236
if params.get(p, None) is not None:
237237
if params['useFindings']:
238238
util.logger.warning("Selected search criteria %s will disable --useFindings", params[p])
239239
params['useFindings'] = False
240240
break
241-
if project_key is None:
242-
project_list = projects.get_key_list(endpoint=sqenv)
243-
else:
244-
project_list = []
245-
for key in util.csv_to_list(project_key):
246-
project_list.append(projects.get_object(key, endpoint=sqenv).key)
241+
project_list = projects.get_projects_list(project_key, sqenv)
247242

248243
fmt = kwargs['format']
249244
file = kwargs.pop('file', None)

sonar/findings/sync.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@
2828
Only issues with a 100% match are synchronized. When there's a doubt, nothing is done
2929
'''
3030

31-
32-
import sys
33-
from sonar import env, projects, branches, version, syncer
31+
from sonar import env, projects, branches, version, syncer, options
3432
import sonar.utilities as util
3533

3634
_WITH_COMMENTS = {'additionalFields': 'comments'}
@@ -144,8 +142,7 @@ def main():
144142
counters.get('nb_tgt_has_changelog', 0))
145143

146144
except env.NonExistingObjectError as e:
147-
util.logger.critical(e.message)
148-
sys.exit(1)
145+
util.exit_fatal(e.message, options.ERR_NO_SUCH_PROJECT_KEY)
149146

150147

151148
if __name__ == '__main__':

sonar/measures_export.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,8 @@ def main():
187187
wanted_metrics = __get_wanted_metrics(args, endpoint)
188188
(fmt, file) = __get_fmt_and_file(args)
189189

190-
filters = None
191-
if args.projectKeys is not None:
192-
filters = {'projects': args.projectKeys.replace(' ', '')}
193-
util.logger.info("Getting project list")
194-
project_list = projects.search(endpoint=endpoint, params=filters).values()
190+
project_list = projects.get_projects_list(args.projectKeys, endpoint)
191+
195192
is_first = True
196193
obj_list = []
197194
if with_branches:

sonar/options.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,14 @@
3030

3131
CSV_SEPARATOR = 'csvSeparator'
3232
FORMAT = 'format'
33+
34+
ERR_SONAR_API_AUTHENTICATION = 1
35+
ERR_SONAR_API_AUTHORIZATION = 2
36+
ERR_SONAR_API = 3
37+
ERR_TOKEN_MISSING = 4
38+
39+
ERR_NO_SUCH_PROJECT_KEY = 5
40+
ERR_WRONG_SEARCH_CRITERIA = 6
41+
ERR_UNSUPPORTED_OPERATION = 7
42+
ERR_RULES_LOADING_FAILED = 8
43+
ERR_SIF_AUDIT_ERROR = 9

sonar/projects.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def __load__(self, data=None):
7070
if data is None:
7171
resp = env.get(PROJECT_SEARCH_API, ctxt=self.endpoint, params={'projects': self.key})
7272
data = json.loads(resp.text)
73+
if not data['components']:
74+
raise env.NonExistingObjectError(self.key, "Project key does not exist")
7375
data = data['components'][0]
7476
self.name = data['name']
7577
self.visibility = data['visibility']
@@ -219,8 +221,7 @@ def binding(self):
219221
self._binding['has_binding'] = True
220222
self._binding['binding'] = json.loads(resp.text)
221223
else:
222-
util.logger.fatal("alm_settings/get_binding returning status code %d, exiting", resp.status_code)
223-
raise SystemExit(1)
224+
util.exit_fatal(f"alm_settings/get_binding returning status code {resp.status_code}, exiting", options.ERR_SONAR_API)
224225
return self._binding['binding']
225226

226227
def is_part_of_monorepo(self):
@@ -442,8 +443,7 @@ def __audit_binding_valid(self, audit_settings):
442443
rule = rules.get_rule(rules.RuleId.PROJ_INVALID_BINDING)
443444
return [pb.Problem(rule.type, rule.severity, rule.msg.format(str(self)), concerned_object=self)]
444445
else:
445-
util.logger.fatal("alm_settings/validate_binding returning status code %d, exiting", resp.status_code)
446-
raise SystemExit(1)
446+
util.exit_fatal(f"alm_settings/get_binding returning status code {resp.status_code}, exiting", options.ERR_SONAR_API)
447447

448448
def audit(self, audit_settings):
449449
util.logger.debug("Auditing %s", str(self))
@@ -629,6 +629,20 @@ def get_object_list(endpoint=None, params=None):
629629
return search(endpoint, params).values()
630630

631631

632+
def get_projects_list(str_key_list, endpoint):
633+
if str_key_list is None:
634+
util.logger.info("Getting project list")
635+
project_list = search(endpoint=endpoint).values()
636+
else:
637+
project_list = []
638+
try:
639+
for key in util.csv_to_list(str_key_list):
640+
project_list.append(get_object(key, endpoint=endpoint).key)
641+
except env.NonExistingObjectError as e:
642+
util.exit_fatal(f"Project key '{e.key}' does not exist, aborting...", options.ERR_NO_SUCH_PROJECT_KEY)
643+
return project_list
644+
645+
632646
def key_obj(key_or_obj):
633647
if isinstance(key_or_obj, str):
634648
return (key_or_obj, get_object(key_or_obj))

sonar/projects_export.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
'''
2626
import sys
2727
import os
28-
from sonar import env, projects
28+
from sonar import env, projects, options
2929
import sonar.utilities as util
3030

3131

@@ -37,9 +37,7 @@ def main():
3737
sq = env.Environment(some_url=args.url, some_token=args.token)
3838

3939
if (sq.edition() in ('community', 'developer') and sq.version(digits=2) < (9, 2)):
40-
util.logger.critical("Can't export projects on Community and Developer Edition before 9.2, aborting...")
41-
print("Can't export project on Community and Developer Edition before 9.2, aborting...")
42-
sys.exit(1)
40+
util.exit_fatal("Can't export projects on Community and Developer Edition before 9.2, aborting...", options.ERR_UNSUPPORTED_OPERATION)
4341

4442
project_list = projects.search(endpoint=sq)
4543
nb_projects = len(project_list)
@@ -51,9 +49,8 @@ def main():
5149
try:
5250
dump = p.export(timeout=args.exportTimeout)
5351
except env.UnsupportedOperation as e:
54-
util.logger.critical("%s", e.message)
55-
print(f"{e.message}")
56-
sys.exit(1)
52+
util.exit_fatal(e.message, options.ERR_UNSUPPORTED_OPERATION)
53+
5754
status = dump['status']
5855
if status in statuses:
5956
statuses[status] += 1

0 commit comments

Comments
 (0)