Add max-job-timeout tenant setting
This change adds a tenant setting to limit the timeout value a job can set.
Change-Id: I3875e81b1f6a6e059e7eb57362772e3446e8d022
diff --git a/doc/source/admin/tenants.rst b/doc/source/admin/tenants.rst
index 54bc10a..4722750 100644
--- a/doc/source/admin/tenants.rst
+++ b/doc/source/admin/tenants.rst
@@ -163,6 +163,11 @@
The maximum number of nodes a job can request. A value of
'-1' value removes the limit.
+ .. attr:: max-job-timeout
+ :default: 10800
+
+ The maximum timeout for jobs. A value of '-1' value removes the limit.
+
.. attr:: exclude-unprotected-branches
:default: false
diff --git a/tests/fixtures/config/multi-tenant/main.yaml b/tests/fixtures/config/multi-tenant/main.yaml
index 4916905..e667588 100644
--- a/tests/fixtures/config/multi-tenant/main.yaml
+++ b/tests/fixtures/config/multi-tenant/main.yaml
@@ -1,5 +1,6 @@
- tenant:
name: tenant-one
+ max-job-timeout: 1800
source:
gerrit:
config-projects:
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 2b27b0e..d55ff92 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -1402,7 +1402,7 @@
class TestMaxNodesPerJob(AnsibleZuulTestCase):
tenant_config_file = 'config/multi-tenant/main.yaml'
- def test_max_nodes_reached(self):
+ def test_max_timeout_exceeded(self):
in_repo_conf = textwrap.dedent(
"""
- job:
@@ -1437,6 +1437,32 @@
"B should not fail because of nodes limit")
+class TestMaxTimeout(AnsibleZuulTestCase):
+ tenant_config_file = 'config/multi-tenant/main.yaml'
+
+ def test_max_nodes_reached(self):
+ in_repo_conf = textwrap.dedent(
+ """
+ - job:
+ name: test-job
+ timeout: 3600
+ """)
+ file_dict = {'.zuul.yaml': in_repo_conf}
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A',
+ files=file_dict)
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertIn('The job "test-job" exceeds tenant max-job-timeout',
+ A.messages[0], "A should fail because of timeout limit")
+
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
+ files=file_dict)
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertNotIn("exceeds tenant max-job-timeout", B.messages[0],
+ "B should not fail because of timeout limit")
+
+
class TestBaseJobs(ZuulTestCase):
tenant_config_file = 'config/base-jobs/main.yaml'
diff --git a/zuul/configloader.py b/zuul/configloader.py
index a923fca..3221154 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -76,6 +76,15 @@
super(MaxNodeError, self).__init__(message)
+class MaxTimeoutError(Exception):
+ def __init__(self, job, tenant):
+ message = textwrap.dedent("""\
+ The job "{job}" exceeds tenant max-job-timeout {maxtimeout}.""")
+ message = textwrap.fill(message.format(
+ job=job.name, maxtimeout=tenant.max_job_timeout))
+ super(MaxTimeoutError, self).__init__(message)
+
+
class DuplicateGroupError(Exception):
def __init__(self, nodeset, group):
message = textwrap.dedent("""\
@@ -505,6 +514,10 @@
if secrets and not conf['_source_context'].trusted:
job.post_review = True
+ if conf.get('timeout') and tenant.max_job_timeout != -1 and \
+ int(conf['timeout']) > tenant.max_job_timeout:
+ raise MaxTimeoutError(job, tenant)
+
if 'post-review' in conf:
if conf['post-review']:
job.post_review = True
@@ -1059,6 +1072,7 @@
def getSchema(connections=None):
tenant = {vs.Required('name'): str,
'max-nodes-per-job': int,
+ 'max-job-timeout': int,
'source': TenantParser.validateTenantSources(connections),
'exclude-unprotected-branches': bool,
'default-parent': str,
@@ -1072,6 +1086,8 @@
tenant = model.Tenant(conf['name'])
if conf.get('max-nodes-per-job') is not None:
tenant.max_nodes_per_job = conf['max-nodes-per-job']
+ if conf.get('max-job-timeout') is not None:
+ tenant.max_job_timeout = int(conf['max-job-timeout'])
if conf.get('exclude-unprotected-branches') is not None:
tenant.exclude_unprotected_branches = \
conf['exclude-unprotected-branches']
diff --git a/zuul/model.py b/zuul/model.py
index 1ef8d3a..429a0c3 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -2506,6 +2506,7 @@
def __init__(self, name):
self.name = name
self.max_nodes_per_job = 5
+ self.max_job_timeout = 10800
self.exclude_unprotected_branches = False
self.default_base_job = None
self.layout = None