Story 2011049: Delete also old views when --delete-old option is specified

Improve 'update' command to handle old views also, not just jobs.

Task: 49601

Change-Id: I60431f662d5a69c084b2698d08104a1524d02767
This commit is contained in:
Vsevolod Fedorov 2024-03-22 12:56:48 +03:00
parent 1f320ad9f9
commit eb08e60765
5 changed files with 205 additions and 15 deletions

View File

@ -4,6 +4,11 @@ Changelog
Release 6.2.0
-------------
Features added
~~~~~~~~~~~~~~
* `--delete-old` flag for `update` cli command is now deletes obsolete views also, not only jobs.
Bugs fixed
~~~~~~~~~~

View File

@ -183,7 +183,7 @@ class JenkinsManager(object):
self._job_list = None
return self.jobs
def is_managed(self, job_name):
def is_managed_job(self, job_name):
xml = self.jenkins.get_job_config(job_name)
try:
out = XML.fromstring(xml.encode("utf-8"))
@ -199,7 +199,7 @@ class JenkinsManager(object):
self._plugins_list = self.get_plugins_info()
return self._plugins_list
def delete_old_managed(self, keep=None):
def delete_old_managed_jobs(self, keep=None):
jobs = self.get_jobs()
deleted_jobs = 0
if keep is None:
@ -210,7 +210,7 @@ class JenkinsManager(object):
if job["fullname"] not in keep and self.is_job(
job["fullname"], use_cache=False
):
if self.is_managed(job["fullname"]):
if self.is_managed_job(job["fullname"]):
logger.info(
"Removing obsolete jenkins job {0}".format(job["fullname"])
)
@ -376,14 +376,42 @@ class JenkinsManager(object):
self._view_list = None
return self.views
def is_view(self, view_name):
# first use cache
if view_name in self.view_list:
def is_view(self, view_name, use_cache=True):
if use_cache and view_name in self.view_list:
return True
# if not exists, use jenkins
return self.jenkins.view_exists(view_name)
def is_managed_view(self, view_name):
xml = self.jenkins.get_view_config(view_name)
try:
out = XML.fromstring(xml.encode("utf-8"))
description = out.find(".//description").text
return description.endswith(MAGIC_MANAGE_STRING)
except (TypeError, AttributeError):
pass
return False
def delete_old_managed_views(self, keep=None):
view_list = self.get_views()
deleted_views = 0
if keep is None:
keep = []
for view in view_list:
if view["name"] not in keep and self.is_view(view["name"], use_cache=False):
if self.is_managed_view(view["name"]):
logger.info(
"Removing obsolete jenkins view {0}".format(view["name"])
)
self.delete_view(view["name"])
deleted_views += 1
else:
logger.debug("Not deleting unmanaged jenkins view %s", view["name"])
else:
logger.debug("Keeping view %s", view["name"])
return deleted_views
def delete_view(self, view_name):
if self.is_view(view_name):
logger.info("Deleting jenkins view {}".format(view_name))

View File

@ -147,5 +147,9 @@ class UpdateSubCommand(base.JobsSubCommand):
if options.delete_old:
if options.update in {"jobs", "all"}:
keep_jobs = [job.name for job in xml_jobs]
n = builder.delete_old_managed(keep=keep_jobs)
n = builder.delete_old_managed_jobs(keep=keep_jobs)
logger.info("Number of jobs deleted: %d", n)
if options.update in {"views", "all"}:
keep_views = [view.name for view in xml_views]
n = builder.delete_old_managed_views(keep=keep_views)
logger.info("Number of views deleted: %d", n)

View File

@ -112,6 +112,8 @@ def test_update_jobs_and_delete_old(
)
jenkins_delete_job = mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.delete_job")
mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.get_views")
yaml_jobs = ["bar001", "bar002", "baz001", "bam001"]
extra_jobs = ["old_job001", "old_job002", "unmanaged"]
@ -123,7 +125,7 @@ def test_update_jobs_and_delete_old(
]
mocker.patch(
"jenkins_jobs.builder.JenkinsManager.is_managed",
"jenkins_jobs.builder.JenkinsManager.is_managed_job",
side_effect=(lambda name: name != "unmanaged"),
)
execute_jenkins_jobs(args)
@ -155,22 +157,39 @@ def test_update_jobs_and_delete_old_views_only(
jenkins_delete_job = mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.delete_job")
mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.view_exists")
mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.get_views")
jenkins_get_all_views = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.get_views"
)
jenkins_reconfig_view = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.reconfig_view"
)
jenkins_delete_view = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.delete_view"
)
yaml_jobs = ["job-1", "job-2", "job-3"]
extra_managed_jobs = ["old-job-1", "old-job-2"]
unmanaged_jobs = ["unmanaged"]
unmanaged_jobs = ["unmanaged-job"]
yaml_views = ["view-1", "view-2", "view-3"]
extra_managed_views = ["old-view-1", "old-view-2"]
unmanaged_views = ["unmanaged-view"]
jenkins_get_all_jobs.return_value = [
{"fullname": name} for name in yaml_jobs + extra_managed_jobs + unmanaged_jobs
]
jenkins_get_all_views.return_value = [
{"name": name} for name in yaml_views + extra_managed_views + unmanaged_views
]
mocker.patch(
"jenkins_jobs.builder.JenkinsManager.is_managed",
"jenkins_jobs.builder.JenkinsManager.is_managed_job",
side_effect=(lambda name: name not in unmanaged_jobs),
)
mocker.patch(
"jenkins_jobs.builder.JenkinsManager.is_managed_view",
side_effect=(lambda name: name not in unmanaged_views),
)
path = fixtures_dir / "update-both.yaml"
args = [
@ -188,6 +207,7 @@ def test_update_jobs_and_delete_old_views_only(
assert jenkins_delete_job.call_count == 0
assert jenkins_reconfig_view.call_count == 3
assert jenkins_delete_view.call_count == 2
def test_update_views(mocker, fixtures_dir, default_config_file, execute_jenkins_jobs):
@ -296,6 +316,117 @@ def test_update_views_only(
assert reconfig_view.call_count == 3
def test_update_views_and_delete_old(
mocker, fixtures_dir, default_config_file, execute_jenkins_jobs
):
"""Test update behaviour with --delete-old option for views."""
mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.get_all_jobs")
mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.view_exists")
jenkins_get_all_views = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.get_views"
)
jenkins_reconfig_view = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.reconfig_view"
)
jenkins_delete_view = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.delete_view"
)
yaml_views = ["view-1", "view-2", "view-3"]
extra_managed_views = ["old-view-1", "old-view-2"]
unmanaged_views = ["unmanaged"]
path = fixtures_dir / "update-views.yaml"
args = ["--conf", default_config_file, "update", "--delete-old", str(path)]
jenkins_get_all_views.return_value = [
{"name": name} for name in yaml_views + extra_managed_views + unmanaged_views
]
mocker.patch(
"jenkins_jobs.builder.JenkinsManager.is_managed_view",
side_effect=(lambda name: name not in unmanaged_views),
)
execute_jenkins_jobs(args)
jenkins_reconfig_view.assert_has_calls(
[mock.call(view_name, mock.ANY) for view_name in yaml_views], any_order=True
)
calls = [mock.call(name) for name in extra_managed_views]
jenkins_delete_view.assert_has_calls(calls)
assert jenkins_delete_view.call_count == len(calls)
def test_update_views_and_delete_old_jobs_only(
mocker, fixtures_dir, default_config_file, execute_jenkins_jobs
):
"""Test update behaviour with --delete-old option
with --jobs-only option specified.
No views should be deleted or updated.
"""
mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.job_exists")
jenkins_get_all_jobs = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.get_all_jobs"
)
jenkins_reconfig_job = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.reconfig_job"
)
jenkins_delete_job = mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.delete_job")
mocker.patch("jenkins_jobs.builder.jenkins.Jenkins.view_exists")
jenkins_get_all_views = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.get_views"
)
jenkins_reconfig_view = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.reconfig_view"
)
jenkins_delete_view = mocker.patch(
"jenkins_jobs.builder.jenkins.Jenkins.delete_view"
)
yaml_jobs = ["job-1", "job-2", "job-3"]
extra_managed_jobs = ["old-job-1", "old-job-2"]
unmanaged_jobs = ["unmanaged-job"]
yaml_views = ["view-1", "view-2", "view-3"]
extra_managed_views = ["old-view-1", "old-view-2"]
unmanaged_views = ["unmanaged-view"]
path = fixtures_dir / "update-both.yaml"
args = [
"--conf",
default_config_file,
"update",
"--delete-old",
"--jobs-only",
str(path),
]
jenkins_get_all_jobs.return_value = [
{"fullname": name} for name in yaml_jobs + extra_managed_jobs + unmanaged_jobs
]
jenkins_get_all_views.return_value = [
{"name": name} for name in yaml_views + extra_managed_views + unmanaged_views
]
mocker.patch(
"jenkins_jobs.builder.JenkinsManager.is_managed_job",
side_effect=(lambda name: name not in unmanaged_jobs),
)
mocker.patch(
"jenkins_jobs.builder.JenkinsManager.is_managed_view",
side_effect=(lambda name: name not in unmanaged_views),
)
execute_jenkins_jobs(args)
assert jenkins_reconfig_job.call_count == 3
assert jenkins_delete_job.call_count == 2
assert jenkins_reconfig_view.call_count == 0
assert jenkins_delete_view.call_count == 0
@pytest.mark.skip(reason="TODO: Develop actual update timeout test approach.")
def test_update_timeout_not_set():
"""Validate update timeout behavior when timeout not explicitly configured."""

View File

@ -50,7 +50,7 @@ def test_plugins_list_from_jenkins(mocker, jjb_config):
assert list(builder.plugins_list) == list(_plugins_info.values())
def test_delete_managed(mocker, jjb_config):
def test_delete_old_managed_jobs(mocker, jjb_config):
jjb_config.builder["plugins_info"] = None
builder = jenkins_jobs.builder.JenkinsManager(jjb_config)
@ -58,20 +58,42 @@ def test_delete_managed(mocker, jjb_config):
"jenkins_jobs.builder.JenkinsManager",
get_jobs=mock.DEFAULT,
is_job=mock.DEFAULT,
is_managed=mock.DEFAULT,
is_managed_job=mock.DEFAULT,
delete_job=mock.DEFAULT,
)
patches["get_jobs"].return_value = [
{"fullname": "job1"},
{"fullname": "job2"},
]
patches["is_managed"].side_effect = [True, True]
patches["is_managed_job"].side_effect = [True, True]
patches["is_job"].side_effect = [True, True]
builder.delete_old_managed()
builder.delete_old_managed_jobs()
assert patches["delete_job"].call_count == 2
def test_delete_old_managed_views(mocker, jjb_config):
jjb_config.builder["plugins_info"] = None
builder = jenkins_jobs.builder.JenkinsManager(jjb_config)
patches = mocker.patch.multiple(
"jenkins_jobs.builder.JenkinsManager",
get_views=mock.DEFAULT,
is_view=mock.DEFAULT,
is_managed_view=mock.DEFAULT,
delete_view=mock.DEFAULT,
)
patches["get_views"].return_value = [
{"name": "view-1"},
{"name": "view-2"},
]
patches["is_managed_view"].side_effect = [True, True]
patches["is_view"].side_effect = [True, True]
builder.delete_old_managed_views()
assert patches["delete_view"].call_count == 2
@pytest.mark.parametrize("error_string", ["Connection refused", "Forbidden"])
def test_get_plugins_info_error(mocker, jjb_config, error_string):
builder = jenkins_jobs.builder.JenkinsManager(jjb_config)