Add abstract job attribute
Change-Id: I4ee968a8e86cea8676f1c1571ad24595efb04ea7
diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst
index 597062e..8492423 100644
--- a/doc/source/user/config.rst
+++ b/doc/source/user/config.rst
@@ -546,6 +546,12 @@
from this job. Once this is set to ``true`` it cannot be reset to
``false``.
+ .. attr:: abstract
+ :default: false
+
+ To indicate a job is not intended to be run directly, but
+ instead must be inherited from, set this attribute to ``true``.
+
.. attr:: success-message
:default: SUCCESS
diff --git a/tests/fixtures/config/abstract/git/common-config/playbooks/base.yaml b/tests/fixtures/config/abstract/git/common-config/playbooks/base.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/abstract/git/common-config/playbooks/base.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+ tasks: []
diff --git a/tests/fixtures/config/abstract/git/common-config/zuul.yaml b/tests/fixtures/config/abstract/git/common-config/zuul.yaml
new file mode 100644
index 0000000..4aeb947
--- /dev/null
+++ b/tests/fixtures/config/abstract/git/common-config/zuul.yaml
@@ -0,0 +1,25 @@
+- pipeline:
+ name: check
+ manager: independent
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ Verified: 1
+ failure:
+ gerrit:
+ Verified: -1
+
+- job:
+ name: base
+ parent: null
+ run: playbooks/base.yaml
+
+- job:
+ name: job-abstract
+ abstract: true
+
+- job:
+ name: job-child
+ parent: job-abstract
diff --git a/tests/fixtures/config/abstract/git/org_project/zuul.yaml b/tests/fixtures/config/abstract/git/org_project/zuul.yaml
new file mode 100644
index 0000000..cf635e8
--- /dev/null
+++ b/tests/fixtures/config/abstract/git/org_project/zuul.yaml
@@ -0,0 +1,4 @@
+- project:
+ name: org/project
+ check:
+ jobs: []
diff --git a/tests/fixtures/config/abstract/main.yaml b/tests/fixtures/config/abstract/main.yaml
new file mode 100644
index 0000000..208e274
--- /dev/null
+++ b/tests/fixtures/config/abstract/main.yaml
@@ -0,0 +1,8 @@
+- tenant:
+ name: tenant-one
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 44eda82..573c8a6 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -74,44 +74,43 @@
class TestProtected(ZuulTestCase):
-
tenant_config_file = 'config/protected/main.yaml'
def test_protected_ok(self):
- # test clean usage of final parent job
- in_repo_conf = textwrap.dedent(
- """
- - job:
- name: job-protected
- protected: true
- run: playbooks/job-protected.yaml
+ # test clean usage of final parent job
+ in_repo_conf = textwrap.dedent(
+ """
+ - job:
+ name: job-protected
+ protected: true
+ run: playbooks/job-protected.yaml
- - project:
- name: org/project
- check:
- jobs:
- - job-child-ok
+ - project:
+ name: org/project
+ check:
+ jobs:
+ - job-child-ok
- - job:
- name: job-child-ok
- parent: job-protected
+ - job:
+ name: job-child-ok
+ parent: job-protected
- - project:
- name: org/project
- check:
- jobs:
- - job-child-ok
+ - project:
+ name: org/project
+ check:
+ jobs:
+ - job-child-ok
- """)
+ """)
- file_dict = {'zuul.yaml': in_repo_conf}
- A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
- files=file_dict)
- self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
- self.waitUntilSettled()
+ file_dict = {'zuul.yaml': in_repo_conf}
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+ files=file_dict)
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
- self.assertEqual(A.reported, 1)
- self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '1')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '1')
def test_protected_reset(self):
# try to reset protected flag
@@ -177,6 +176,47 @@
"and cannot be inherited from other projects.", A.messages[0])
+class TestAbstract(ZuulTestCase):
+ tenant_config_file = 'config/abstract/main.yaml'
+
+ def test_abstract_fail(self):
+ in_repo_conf = textwrap.dedent(
+ """
+ - project:
+ check:
+ jobs:
+ - job-abstract
+ """)
+
+ file_dict = {'zuul.yaml': in_repo_conf}
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+ files=file_dict)
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '-1')
+ self.assertIn('may not be directly run', A.messages[0])
+
+ def test_child_of_abstract(self):
+ in_repo_conf = textwrap.dedent(
+ """
+ - project:
+ check:
+ jobs:
+ - job-child
+ """)
+
+ file_dict = {'zuul.yaml': in_repo_conf}
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+ files=file_dict)
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '1')
+
+
class TestFinal(ZuulTestCase):
tenant_config_file = 'config/final/main.yaml'
diff --git a/zuul/configloader.py b/zuul/configloader.py
index be1bd63..c7f8fd8 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -474,6 +474,7 @@
# Attributes of a job that can also be used in Project and ProjectTemplate
job_attributes = {'parent': vs.Any(str, None),
'final': bool,
+ 'abstract': bool,
'protected': bool,
'failure-message': str,
'success-message': str,
@@ -514,6 +515,7 @@
simple_attributes = [
'final',
+ 'abstract',
'protected',
'timeout',
'workspace',
diff --git a/zuul/model.py b/zuul/model.py
index 38f2d6b..45fc1a8 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -848,6 +848,7 @@
semaphore=None,
attempts=3,
final=False,
+ abstract=False,
protected=None,
roles=(),
required_projects={},
@@ -1044,7 +1045,7 @@
for k in self.execution_attributes:
if (other._get(k) is not None and
- k not in set(['final', 'protected'])):
+ k not in set(['final', 'abstract', 'protected'])):
if self.final:
raise Exception("Unable to modify final job %s attribute "
"%s=%s with variant %s" % (
@@ -1070,6 +1071,13 @@
if other.final != self.attributes['final']:
self.final = other.final
+ # Abstract may not be reset by a variant, it may only be
+ # cleared by inheriting.
+ if other.name != self.name:
+ self.abstract = other.abstract
+ elif other.abstract:
+ self.abstract = True
+
# Protected may only be set to true
if other.protected is not None:
# don't allow to reset protected flag
@@ -2836,6 +2844,10 @@
item.debug("No matching pipeline variants for {jobname}".
format(jobname=jobname), indent=2)
continue
+ if frozen_job.abstract:
+ raise Exception("Job %s is abstract and may not be "
+ "directly run" %
+ (frozen_job.name,))
if (frozen_job.allowed_projects is not None and
change.project.name not in frozen_job.allowed_projects):
raise Exception("Project %s is not allowed to run job %s" %