Skip to content

Commit d72b02e

Browse files
authored
Housekeeper-delete-tokens-projects-and-branches-separately (#2018)
* Fixes #2014 * Filter for housekeeper mode * Quality pass
1 parent 4c16b5f commit d72b02e

File tree

8 files changed

+61
-54
lines changed

8 files changed

+61
-54
lines changed

cli/housekeeper.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
from requests import RequestException
3232
from cli import options
3333
import sonar.logging as log
34-
from sonar import platform, tokens, users, projects, branches, version, errcodes
34+
from sonar import platform, tokens, users, projects, branches, pull_requests, version, errcodes
35+
from sonar.util import types
3536
import sonar.util.constants as c
3637
import sonar.utilities as util
3738
import sonar.exceptions as ex
@@ -44,7 +45,7 @@
4445
def get_project_problems(settings: dict[str, str], endpoint: object) -> list[problem.Problem]:
4546
"""Returns the list of problems that would require housekeeping for a given project"""
4647
problems = []
47-
if settings[PROJ_MAX_AGE] < 90:
48+
if settings[PROJ_MAX_AGE] != 0 and settings[PROJ_MAX_AGE] < 90:
4849
log.error("As a safety measure, can't delete projects more recent than 90 days")
4950
return problems
5051

@@ -90,10 +91,6 @@ def get_user_problems(settings: dict[str, str], endpoint: platform.Platform) ->
9091

9192
def _parse_arguments() -> object:
9293
"""Parses CLI arguments"""
93-
_DEFAULT_PROJECT_OBSOLESCENCE = 365
94-
_DEFAULT_BRANCH_OBSOLESCENCE = 90
95-
_DEFAULT_PR_OBSOLESCENCE = 30
96-
_DEFAULT_TOKEN_OBSOLESCENCE = 365
9794
parser = options.set_common_args("Deletes projects, branches, PR, user tokens not used since a given number of days")
9895
parser = options.set_output_file_args(parser, allowed_formats=("csv",))
9996
parser = options.add_thread_arg(parser, "auditing before housekeeping")
@@ -112,16 +109,16 @@ def _parse_arguments() -> object:
112109
"--projectsMaxAge",
113110
required=False,
114111
type=int,
115-
default=_DEFAULT_PROJECT_OBSOLESCENCE,
116-
help=f"Deletes projects not analyzed since a given number of days, by default {_DEFAULT_PROJECT_OBSOLESCENCE} days",
112+
default=0,
113+
help="Deletes projects not analyzed since a given number of days",
117114
)
118115
parser.add_argument(
119116
"-B",
120117
"--branchesMaxAge",
121118
required=False,
122119
type=int,
123-
default=_DEFAULT_BRANCH_OBSOLESCENCE,
124-
help=f"Deletes branches not to be kept and not analyzed since a given number of days, by default {_DEFAULT_BRANCH_OBSOLESCENCE} days",
120+
default=0,
121+
help="Deletes branches not to be kept and not analyzed since a given number of days",
125122
)
126123
parser.add_argument(
127124
"--keepWhenInactive",
@@ -134,21 +131,21 @@ def _parse_arguments() -> object:
134131
"--pullrequestsMaxAge",
135132
required=False,
136133
type=int,
137-
default=_DEFAULT_BRANCH_OBSOLESCENCE,
138-
help=f"Deletes pull requests not analyzed since a given number of days, by default {_DEFAULT_PR_OBSOLESCENCE} days",
134+
default=0,
135+
help="Deletes pull requests not analyzed since a given number of days",
139136
)
140137
parser.add_argument(
141138
"-T",
142139
"--tokensMaxAge",
143140
required=False,
144141
type=int,
145-
default=_DEFAULT_TOKEN_OBSOLESCENCE,
146-
help=f"Deletes user tokens older than a certain number of days, by default {_DEFAULT_TOKEN_OBSOLESCENCE} days",
142+
default=0,
143+
help="Deletes user tokens older than a certain number of days",
147144
)
148145
return options.parse_and_check(parser=parser, logger_name=TOOL_NAME)
149146

150147

151-
def _delete_objects(problems: problem.Problem, mode: str) -> tuple[int, int, int, int, int]:
148+
def _delete_objects(problems: problem.Problem, mode: str, settings: types.ConfigSettings) -> tuple[int, int, int, int, int]:
152149
"""Deletes objects (that should be housekept)"""
153150
revoked_token_count = 0
154151
deleted_projects = {}
@@ -171,12 +168,13 @@ def _delete_objects(problems: problem.Problem, mode: str) -> tuple[int, int, int
171168
deleted_loc += loc
172169
if isinstance(obj, (tokens.UserToken, users.User)) and (mode != "delete" or obj.revoke()):
173170
revoked_token_count += 1
174-
elif obj.project().key in deleted_projects:
171+
elif settings[PROJ_MAX_AGE] > 0 and obj.project().key in deleted_projects:
175172
log.info("%s deleted, so no need to delete %s", str(obj.project()), str(obj))
176173
elif mode != "delete" or obj.delete():
174+
log.info("%s to delete", str(obj))
177175
if isinstance(obj, branches.Branch):
178176
deleted_branch_count += 1
179-
else:
177+
elif isinstance(obj, pull_requests.PullRequest):
180178
deleted_pr_count += 1
181179

182180
except ex.ObjectNotFound:
@@ -211,7 +209,6 @@ def main() -> None:
211209
settings = {
212210
"audit.tokens.maxAge": token_age,
213211
"audit.tokens.maxUnusedAge": 90,
214-
# "audit.groups.empty": True,
215212
PROJ_MAX_AGE: proj_age,
216213
"audit.projects.branches.maxLastAnalysisAge": branch_age,
217214
"audit.projects.pullRequests.maxLastAnalysisAge": pr_age,
@@ -232,12 +229,16 @@ def main() -> None:
232229
op = "to delete"
233230
if mode == "delete":
234231
op = "deleted"
235-
(deleted_proj, deleted_loc, deleted_branches, deleted_prs, revoked_tokens) = _delete_objects(problems, mode)
236-
237-
log.info("%d projects older than %d days (%d LoCs) %s", deleted_proj, proj_age, deleted_loc, op)
238-
log.info("%d branches older than %d days %s", deleted_branches, branch_age, op)
239-
log.info("%d pull requests older than %d days %s", deleted_prs, pr_age, op)
240-
log.info("%d tokens older than %d days %s", revoked_tokens, token_age, "revoked" if mode == "deleted" else "to revoke")
232+
(deleted_proj, deleted_loc, deleted_branches, deleted_prs, revoked_tokens) = _delete_objects(problems, mode, settings=settings)
233+
234+
if proj_age > 0:
235+
log.info("%d projects older than %d days (%d LoCs) %s", deleted_proj, proj_age, deleted_loc, op)
236+
if branch_age > 0:
237+
log.info("%d branches older than %d days %s", deleted_branches, branch_age, op)
238+
if pr_age > 0:
239+
log.info("%d pull requests older than %d days %s", deleted_prs, pr_age, op)
240+
if token_age > 0:
241+
log.info("%d tokens older than %d days %s", revoked_tokens, token_age, "revoked" if mode == "deleted" else "to revoke")
241242

242243
except (PermissionError, FileNotFoundError) as e:
243244
util.final_exit(errcodes.OS_ERROR, f"OS error while housekeeping: {str(e)}")

sonar/audit/sonar-audit.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,11 @@ audit.qualityProfiles.maxPerLanguage = 5
323323
audit.users.maxLoginAge = 180
324324

325325
# Audit for days after which a token should be revoked (and potentially renewed)
326+
# Set to 0 to turn off this audit check
326327
audit.tokens.maxAge = 90
327328

328329
# Audit for days after which an unused token should be revoked (and potentially renewed)
330+
# Set to 0 to turn off this audit check
329331
audit.tokens.maxUnusedAge = 30
330332

331333
# Comma separated list of SonarQube users whose tokens are not considered for expiration

sonar/branches.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -363,13 +363,14 @@ def __audit_last_analysis(self, audit_settings: types.ConfigSettings) -> list[Pr
363363
if self.is_main():
364364
log.info("%s is main (not purgeable)", str(self))
365365
return []
366-
if (age := util.age(self.last_analysis())) is None:
366+
if (max_age := audit_settings.get("audit.projects.branches.maxLastAnalysisAge", 30)) == 0:
367367
log.debug("%s last analysis audit is disabled, skipped...", str(self))
368368
return []
369-
max_age = audit_settings.get("audit.projects.branches.maxLastAnalysisAge", 30)
369+
if (age := util.age(self.last_analysis())) is None:
370+
log.warning("%s: Can't get last analysis date for audit, skipped", str(self))
371+
return []
370372
preserved = audit_settings.get("audit.projects.branches.keepWhenInactive", None)
371373
problems = []
372-
log.info("EVALUATING %s age %d greater than %d days and not matches '%s'", str(self), age, max_age, preserved)
373374
if preserved is not None and age > max_age and not re.match(rf"^{preserved}$", self.name):
374375
log.info("%s age %d greater than %d days and not matches '%s'", str(self), age, max_age, preserved)
375376
problems.append(Problem(get_rule(RuleId.BRANCH_LAST_ANALYSIS), self, str(self), age))
@@ -392,7 +393,10 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
392393
return []
393394
log.debug("Auditing %s", str(self))
394395
try:
395-
return self.__audit_last_analysis(audit_settings) + self.__audit_never_analyzed() + self._audit_component(audit_settings)
396+
if audit_settings.get(c.AUDIT_MODE_PARAM, "") == "housekeeper":
397+
return self.__audit_last_analysis(audit_settings)
398+
else:
399+
return self.__audit_last_analysis(audit_settings) + self.__audit_never_analyzed() + self._audit_component(audit_settings)
396400
except Exception as e:
397401
log.error("%s while auditing %s, audit skipped", util.error_msg(e), str(self))
398402
return []

sonar/components.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def get_ai_code_assurance(self) -> Optional[str]:
288288

289289
def _audit_bg_task(self, audit_settings: types.ConfigSettings) -> list[Problem]:
290290
"""Audits project background tasks"""
291-
if audit_settings.get("audit.mode", "") == "housekeeper":
291+
if audit_settings.get(c.AUDIT_MODE_PARAM, "") == "housekeeper":
292292
return []
293293
# Cutting short if background task audit is disabled because getting last task is costly
294294
if (

sonar/projects.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,8 @@ def __audit_last_analysis(self, audit_settings: types.ConfigSettings) -> list[Pr
417417
problems = []
418418
age = util.age(self.last_analysis(include_branches=True), True)
419419
if age is None:
420+
if audit_settings.get(c.AUDIT_MODE_PARAM, "") == "housekeeper":
421+
return problems
420422
if not audit_settings.get("audit.projects.neverAnalyzed", True):
421423
log.debug("Auditing of never analyzed projects is disabled, skipping")
422424
else:
@@ -457,19 +459,11 @@ def __audit_branches(self, audit_settings: types.ConfigSettings) -> list[Problem
457459
return problems
458460

459461
def __audit_pull_requests(self, audit_settings: types.ConfigSettings) -> list[Problem]:
460-
"""Audits project pul requests
462+
"""Audits project pull requests
461463
462-
:param audit_settings: Settings (thresholds) to raise problems
463-
:type audit_settings: dict
464-
:return: List of problems found, or empty list
465-
:rtype: list[Problem]
464+
:param ConfigSettings audit_settings: Settings (thresholds) to raise problems
465+
:return: List of problems found
466466
"""
467-
if audit_settings.get(c.AUDIT_MODE_PARAM, "") == "housekeeper":
468-
return []
469-
max_age = audit_settings.get("audit.projects.pullRequests.maxLastAnalysisAge", 30)
470-
if max_age == 0:
471-
log.debug("Auditing of pull request last analysis age is disabled, skipping...")
472-
return []
473467
problems = []
474468
for pr in self.pull_requests().values():
475469
problems += pr.audit(audit_settings)
@@ -650,16 +644,16 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
650644
problems = []
651645
try:
652646
problems = self.__audit_last_analysis(audit_settings)
653-
problems += self.audit_visibility(audit_settings)
654-
problems += self.__audit_binding_valid(audit_settings)
655-
# Skip language audit, as this can be problematic
656-
# problems += self.__audit_languages(audit_settings)
657647
if audit_settings.get(c.AUDIT_MODE_PARAM, "") != "housekeeper":
648+
problems += self.audit_visibility(audit_settings)
649+
problems += self.__audit_binding_valid(audit_settings)
650+
# Skip language audit, as this can be problematic
651+
# problems += self.__audit_languages(audit_settings)
658652
problems += self.permissions().audit(audit_settings)
659653

660-
problems += self.__audit_scanner(audit_settings)
661-
problems += self._audit_component(audit_settings)
662-
problems += self.__audit_key_pattern(audit_settings)
654+
problems += self.__audit_scanner(audit_settings)
655+
problems += self._audit_component(audit_settings)
656+
problems += self.__audit_key_pattern(audit_settings)
663657
if self.endpoint.edition() != c.CE and audit_settings.get("audit.project.branches", True):
664658
problems += self.__audit_branches(audit_settings)
665659
problems += self.__audit_pull_requests(audit_settings)

sonar/pull_requests.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,13 @@ def last_analysis(self) -> datetime:
8787

8888
def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
8989
"""Audits the pull request according to the audit settings"""
90-
problems = self._audit_component(audit_settings)
91-
if (age := util.age(self.last_analysis())) is None: # Main branch not analyzed yet
90+
problems = [] if audit_settings.get(c.AUDIT_MODE_PARAM, "") == "housekeeper" else self._audit_component(audit_settings)
91+
if (age := util.age(self.last_analysis())) is None:
92+
log.warning("%s: Can't get last analysis date for audit, skipped")
93+
return problems
94+
if (max_age := audit_settings.get("audit.projects.pullRequests.maxLastAnalysisAge", 30)) == 0:
95+
log.info("%s: Audit of last analysis date is disabled", self)
9296
return problems
93-
max_age = audit_settings.get("audit.projects.pullRequests.maxLastAnalysisAge", 30)
9497
if age > max_age:
9598
problems.append(Problem(get_rule(RuleId.PULL_REQUEST_LAST_ANALYSIS), self, str(self), age))
9699
else:

sonar/tokens.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,18 @@ def audit(self, settings: types.ConfigSettings, today: Optional[datetime.datetim
9494
if self.sq_json.get("isExpired", False):
9595
return [Problem(get_rule(RuleId.TOKEN_EXPIRED), self, str(self))]
9696
problems = []
97-
mode = settings.get("audit.mode", "")
97+
mode = settings.get(c.AUDIT_MODE_PARAM, "")
98+
max_age = settings.get("audit.tokens.maxAge", 90)
9899
if not today:
99100
today = datetime.datetime.now(datetime.timezone.utc).astimezone()
100101
age = util.age(self.created_at, now=today)
101102
if mode != "housekeeper" and not self.expiration_date:
102103
problems.append(Problem(get_rule(RuleId.TOKEN_WITHOUT_EXPIRATION), self, str(self), age))
103-
if age > settings.get("audit.tokens.maxAge", 90):
104+
if max_age == 0:
105+
log.info("%s: Audit of token max age is disabled, skipped")
106+
elif age > max_age:
104107
problems.append(Problem(get_rule(RuleId.TOKEN_TOO_OLD), self, str(self), age))
105-
if self.last_connection_date:
108+
if self.last_connection_date and mode != "housekeeper":
106109
last_cnx_age = util.age(self.last_connection_date, now=today)
107110
if last_cnx_age > settings.get("audit.tokens.maxUnusedAge", 30):
108111
problems.append(Problem(get_rule(RuleId.TOKEN_UNUSED), self, str(self), last_cnx_age))

sonar/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ def audit(self, settings: types.ConfigSettings = None) -> list[Problem]:
423423

424424
today = dt.datetime.now(dt.timezone.utc).astimezone()
425425
problems = [p for t in self.tokens() for p in t.audit(settings=settings, today=today)]
426-
if self.last_login:
426+
if self.last_login and settings.get(c.AUDIT_MODE_PARAM, "") != "housekeeper":
427427
age = util.age(self.last_login, now=today)
428428
if age > settings.get("audit.users.maxLoginAge", 180):
429429
problems.append(Problem(get_rule(RuleId.USER_UNUSED), self, str(self), age))

0 commit comments

Comments
 (0)