summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst8
-rw-r--r--doc/source/admin/components.rst59
-rw-r--r--doc/source/admin/connections.rst9
-rw-r--r--doc/source/index.rst8
-rw-r--r--doc/source/user/config.rst7
-rw-r--r--doc/source/user/gating.rst83
-rw-r--r--doc/source/user/jobs.rst7
-rw-r--r--requirements.txt1
-rw-r--r--tests/fixtures/config/protected/git/common-config/zuul.yaml16
-rw-r--r--tests/fixtures/config/protected/git/org_project/playbooks/job-protected.yaml2
-rw-r--r--tests/fixtures/config/protected/git/org_project/zuul.yaml9
-rw-r--r--tests/fixtures/config/protected/git/org_project1/README1
-rw-r--r--tests/fixtures/config/protected/git/org_project1/playbooks/job-child-notok.yaml2
-rw-r--r--tests/fixtures/config/protected/git/org_project1/playbooks/placeholder0
-rw-r--r--tests/fixtures/config/protected/main.yaml9
-rw-r--r--tests/unit/test_connection.py8
-rw-r--r--tests/unit/test_gerrit_crd.py59
-rw-r--r--tests/unit/test_gerrit_legacy_crd.py1
-rw-r--r--tests/unit/test_git_driver.py45
-rw-r--r--tests/unit/test_github_driver.py6
-rwxr-xr-xtests/unit/test_v3.py104
-rwxr-xr-xtools/encrypt_secret.py4
-rwxr-xr-x[-rw-r--r--]tools/github-debugging.py92
-rwxr-xr-xzuul/cmd/__init__.py24
-rwxr-xr-xzuul/cmd/executor.py17
-rw-r--r--zuul/cmd/fingergw.py18
-rwxr-xr-xzuul/cmd/merger.py17
-rwxr-xr-xzuul/cmd/scheduler.py19
-rwxr-xr-xzuul/cmd/web.py8
-rw-r--r--zuul/configloader.py2
-rw-r--r--zuul/driver/git/gitconnection.py3
-rw-r--r--zuul/driver/github/githubconnection.py19
-rw-r--r--zuul/driver/github/githubreporter.py8
-rw-r--r--zuul/driver/sql/alembic/versions/19d3a3ebfe1d_change_patchset_to_string.py29
-rw-r--r--zuul/driver/sql/sqlconnection.py2
-rw-r--r--zuul/manager/dependent.py7
-rw-r--r--zuul/manager/independent.py4
-rw-r--r--zuul/model.py24
-rw-r--r--zuul/reporter/__init__.py19
39 files changed, 542 insertions, 218 deletions
diff --git a/README.rst b/README.rst
index 52b89dfb6..8d0066530 100644
--- a/README.rst
+++ b/README.rst
@@ -10,6 +10,14 @@ preparation for the third major version of Zuul. We call this effort
The latest documentation for Zuul v3 is published at:
https://docs.openstack.org/infra/zuul/feature/zuulv3/
+If you are looking for the Edge routing service named Zuul that is
+related to Netflix, it can be found here:
+https://github.com/Netflix/zuul
+
+If you are looking for the Javascript testing tool named Zuul, it
+can be found here:
+https://github.com/defunctzombie/zuul
+
Contributing
------------
diff --git a/doc/source/admin/components.rst b/doc/source/admin/components.rst
index 18bbfa3f4..d6b0984d2 100644
--- a/doc/source/admin/components.rst
+++ b/doc/source/admin/components.rst
@@ -8,7 +8,6 @@ Components
Zuul is a distributed system consisting of several components, each of
which is described below.
-
.. graphviz::
:align: center
@@ -31,7 +30,27 @@ which is described below.
Scheduler -- GitHub;
}
+Each of the Zuul processes may run on the same host, or different
+hosts. Within Zuul, the components communicate with the scheduler via
+the Gearman protocol, so each Zuul component needs to be able to
+connect to the host running the Gearman server (the scheduler has a
+built-in Gearman server which is recommended) on the Gearman port --
+TCP port 4730 by default.
+
+The Zuul scheduler communicates with Nodepool via the ZooKeeper
+protocol. Nodepool requires an external ZooKeeper cluster, and the
+Zuul scheduler needs to be able to connect to the hosts in that
+cluster on TCP port 2181.
+
+Both the Nodepool launchers and Zuul executors need to be able to
+communicate with the hosts which nodepool provides. If these are on
+private networks, the Executors will need to be able to route traffic
+to them.
+If statsd is enabled, every service needs to be able to emit data to
+statsd. Statsd can be configured to run on each host and forward
+data, or services may emit to a centralized statsd collector. Statsd
+listens on UDP port 8125 by default.
All Zuul processes read the ``/etc/zuul/zuul.conf`` file (an alternate
location may be supplied on the command line) which uses an INI file
@@ -154,6 +173,23 @@ connections to remote systems which have been configured, enqueues
items into pipelines, distributes jobs to executors, and reports
results.
+The scheduler includes a Gearman server which is used to communicate
+with other components of Zuul. It is possible to use an external
+Gearman server, but the built-in server is well-tested and
+recommended. If the built-in server is used, other Zuul hosts will
+need to be able to connect to the scheduler on the Gearman port, TCP
+port 4730. It is also strongly recommended to use SSL certs with
+Gearman, as secrets are transferred from the scheduler to executors
+over this link.
+
+The scheduler must be able to connect to the ZooKeeper cluster used by
+Nodepool in order to request nodes. It does not need to connect
+directly to the nodes themselves, however -- that function is handled
+by the Executors.
+
+It must also be able to connect to any services for which connections
+are configured (Gerrit, GitHub, etc).
+
Configuration
~~~~~~~~~~~~~
@@ -280,6 +316,10 @@ perform them, large numbers may impact their ability to run jobs.
Therefore, administrators may wish to run standalone mergers in order
to reduce the load on executors.
+Mergers need to be able to connect to the Gearman server (usually the
+scheduler host) as well as any services for which connections are
+configured (Gerrit, GitHub, etc).
+
Configuration
~~~~~~~~~~~~~
@@ -358,6 +398,11 @@ perform any tasks normally performed by standalone mergers. Because
the executor performs both roles, small Zuul installations may not
need to run standalone mergers.
+Executors need to be able to connect to the Gearman server (usually
+the scheduler host), any services for which connections are configured
+(Gerrit, GitHub, etc), as well as directly to the hosts which Nodepool
+provides.
+
Trusted and Untrusted Playbooks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -508,7 +553,7 @@ The following sections of ``zuul.conf`` are used by the executor:
significant protections against malicious users and accidental
breakage in playbooks. As such, `nullwrap` is not recommended
for use in production.
-
+
This option, and thus, `nullwrap`, may be removed in the future.
`bubblewrap` has become integral to securely operating Zuul. If you
have a valid use case for it, we encourage you to let us know.
@@ -577,6 +622,12 @@ The Zuul web server currently acts as a websocket interface to live log
streaming. Eventually, it will serve as the single process handling all
HTTP interactions with Zuul.
+Web servers need to be able to connect to the Gearman server (usually
+the scheduler host). If the SQL reporter is used, they need to be
+able to connect to the database it reports to in order to support the
+dashboard. If a GitHub connection is configured, they need to be
+reachable by GitHub so they may receive notifications.
+
Configuration
~~~~~~~~~~~~~
@@ -636,6 +687,10 @@ For example::
The above would stream the logs for the build identified by `UUID`.
+Finger gateway servers need to be able to connect to the Gearman
+server (usually the scheduler host), as well as the console streaming
+port on the executors (usually 7900).
+
Configuration
~~~~~~~~~~~~~
diff --git a/doc/source/admin/connections.rst b/doc/source/admin/connections.rst
index 55ac629c1..b04dbb06a 100644
--- a/doc/source/admin/connections.rst
+++ b/doc/source/admin/connections.rst
@@ -33,6 +33,15 @@ may appear as:
driver=gerrit
server=review.example.com
+Zuul needs to use a single connection to look up information about
+changes hosted by a given system. When it looks up changes, it will
+do so using the first connection it finds that matches the server name
+it's looking for. It's generally best to use only a single connection
+for a given server, however, if you need more than one (for example,
+to satisfy unique reporting requirements) be sure to list the primary
+connection first as that is what Zuul will use to look up all changes
+for that server.
+
.. _drivers:
Drivers
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 677e9584c..6e1b52e21 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -12,6 +12,14 @@ are installing or operating a Zuul system, you will also find the
:doc:`admin/index` useful. If you want help make Zuul itself better,
take a look at the :doc:`developer/index`.
+If you are looking for the Edge routing service named Zuul that is
+related to Netflix, it can be found here:
+https://github.com/Netflix/zuul
+
+If you are looking for the Javascript testing tool named Zuul, it
+can be found here:
+https://github.com/defunctzombie/zuul
+
Contents:
.. toctree::
diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst
index fff673b55..525cb3892 100644
--- a/doc/source/user/config.rst
+++ b/doc/source/user/config.rst
@@ -539,6 +539,13 @@ Here is an example of two job definitions:
specified in a project's pipeline, set this attribute to
``true``.
+ .. attr:: protected
+ :default: false
+
+ When set to ``true`` only jobs defined in the same project may inherit
+ from this job. Once this is set to ``true`` it cannot be reset to
+ ``false``.
+
.. attr:: success-message
:default: SUCCESS
diff --git a/doc/source/user/gating.rst b/doc/source/user/gating.rst
index 795df723d..543a8cccc 100644
--- a/doc/source/user/gating.rst
+++ b/doc/source/user/gating.rst
@@ -246,11 +246,25 @@ Zuul's cross-project dependencies behave like a directed acyclic graph
between changes in different git repositories. Change A may depend on
B, but B may not depend on A.
-.. TODO: update for v3 crd syntax
+To use them, include ``Depends-On: <change-url>`` in the footer of a
+commit message. For example, a change which depends on a GitHub pull
+request (PR #4) might have the following footer::
-To use them, include ``Depends-On: <gerrit-change-id>`` in the footer of
-a commit message. Use the full Change-ID ('I' + 40 characters).
+ Depends-On: https://github.com/example/test/pull/4
+And a change which depends on a Gerrit change (change number 3)::
+
+ Depends-On: https://review.example.com/3
+
+Changes may depend on changes in any other project, even projects not
+on the same system (i.e., a Gerrit change may depend on a GitHub pull
+request).
+
+.. note::
+
+ An older syntax of specifying dependencies using Gerrit change-ids
+ is still supported, however it is deprecated and will be removed in
+ a future version.
Dependent Pipeline
~~~~~~~~~~~~~~~~~~
@@ -277,7 +291,7 @@ dependent pipeline, B will appear first and A will follow:
B_status [ class = greendot ]
B_status -- A_status
- 'Change B\nChange-Id: Iabc' <- 'Change A\nDepends-On: Iabc'
+ 'Change B\nURL: .../4' <- 'Change A\nDepends-On: .../4'
}
If tests for B fail, both B and A will be removed from the pipeline, and
@@ -328,7 +342,7 @@ up as red or green (depending on the jobs results):
B_status [class = "dot", color = grey]
B_status -- A_status
- "Change B" <- "Change A\nDepends-On: B"
+ "Change B\nURL: .../4" <- "Change A\nDepends-On: .../4"
}
This is to indicate that the grey changes are only there to establish
@@ -337,56 +351,13 @@ will show up as a grey dot when used as a dependency, but separately and
additionally will appear as its own red or green dot for its test.
-.. TODO: relevant for v3?
-
Multiple Changes
~~~~~~~~~~~~~~~~
-A Gerrit change ID may refer to multiple changes (on multiple branches
-of the same project, or even multiple projects). In these cases, Zuul
-will treat all of the changes with that change ID as dependencies. So
-if you say that change in project A Depends-On a change ID that has
-changes in two branches of project B, then when testing the change to
-project A, both project B changes will be applied, and when deciding
-whether the project A change can merge, both changes must merge ahead
-of it.
-
-.. blockdiag::
- :align: center
-
- blockdiag crdmultirepos {
- orientation = portrait
- span_width = 30
- class greendot [
- label = "",
- shape = circle,
- color = green,
- width = 20, height = 20
- ]
-
- B_stable_status [ class = "greendot" ]
- B_master_status [ class = "greendot" ]
- A_status [ class = "greendot" ]
- B_stable_status -- B_master_status -- A_status
-
- A [ label = "Repo A\nDepends-On: I123" ]
- group {
- orientation = portrait
- label = "Dependencies"
- color = "lightgray"
-
- B_stable [ label = "Repo B\nChange-Id: I123\nBranch: stable" ]
- B_master [ label = "Repo B\nChange-Id: I123\nBranch: master" ]
- }
- B_master <- A
- B_stable <- A
-
- }
-
-A change may depend on more than one Gerrit change ID as well. So it
-is possible for a change in project A to depend on a change in project
-B and a change in project C. Simply add more ``Depends-On:`` lines to
-the commit message footer.
+A change may list more than one dependency by simply adding more
+``Depends-On:`` lines to the commit message footer. It is possible
+for a change in project A to depend on a change in project B and a
+change in project C.
.. blockdiag::
:align: center
@@ -406,20 +377,18 @@ the commit message footer.
A_status [ class = "greendot" ]
C_status -- B_status -- A_status
- A [ label = "Repo A\nDepends-On: I123\nDepends-On: Iabc" ]
+ A [ label = "Repo A\nDepends-On: .../3\nDepends-On: .../4" ]
group {
orientation = portrait
label = "Dependencies"
color = "lightgray"
- B [ label = "Repo B\nChange-Id: I123" ]
- C [ label = "Repo C\nChange-Id: Iabc" ]
+ B [ label = "Repo B\nURL: .../3" ]
+ C [ label = "Repo C\nURL: .../4" ]
}
B, C <- A
}
-.. TODO: update for v3
-
Cycles
~~~~~~
diff --git a/doc/source/user/jobs.rst b/doc/source/user/jobs.rst
index 4b6255b20..9ec464671 100644
--- a/doc/source/user/jobs.rst
+++ b/doc/source/user/jobs.rst
@@ -63,12 +63,15 @@ in this format.
Note that these git repositories are located on the executor; in order
to be useful to most kinds of jobs, they will need to be present on
-the test nodes. The ``base`` job in the standard library contains a
+the test nodes. The ``base`` job in the standard library (see
+`zuul-base-jobs documentation`_ for details) contains a
pre-playbook which copies the repositories to all of the job's nodes.
It is recommended to always inherit from this base job to ensure that
behavior.
-.. TODO: link to base job documentation and/or document src (and logs?) directory
+.. _zuul-base-jobs documentation: https://docs.openstack.org/infra/zuul-base-jobs/jobs.html#job-base
+
+.. TODO: document src (and logs?) directory
Variables
---------
diff --git a/requirements.txt b/requirements.txt
index 193c64e71..39a2b0268 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -25,6 +25,5 @@ cryptography>=1.6
cachecontrol
pyjwt
iso8601
-yarl>=0.11,<1.0
aiohttp
uvloop;python_version>='3.5'
diff --git a/tests/fixtures/config/protected/git/common-config/zuul.yaml b/tests/fixtures/config/protected/git/common-config/zuul.yaml
new file mode 100644
index 000000000..c941573e6
--- /dev/null
+++ b/tests/fixtures/config/protected/git/common-config/zuul.yaml
@@ -0,0 +1,16 @@
+- pipeline:
+ name: check
+ manager: independent
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ Verified: 1
+ failure:
+ gerrit:
+ Verified: -1
+
+- job:
+ name: base
+ parent: null
diff --git a/tests/fixtures/config/protected/git/org_project/playbooks/job-protected.yaml b/tests/fixtures/config/protected/git/org_project/playbooks/job-protected.yaml
new file mode 100644
index 000000000..f679dceae
--- /dev/null
+++ b/tests/fixtures/config/protected/git/org_project/playbooks/job-protected.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+ tasks: []
diff --git a/tests/fixtures/config/protected/git/org_project/zuul.yaml b/tests/fixtures/config/protected/git/org_project/zuul.yaml
new file mode 100644
index 000000000..95f33df6f
--- /dev/null
+++ b/tests/fixtures/config/protected/git/org_project/zuul.yaml
@@ -0,0 +1,9 @@
+- job:
+ name: job-protected
+ protected: true
+ run: playbooks/job-protected.yaml
+
+- project:
+ name: org/project
+ check:
+ jobs: []
diff --git a/tests/fixtures/config/protected/git/org_project1/README b/tests/fixtures/config/protected/git/org_project1/README
new file mode 100644
index 000000000..9daeafb98
--- /dev/null
+++ b/tests/fixtures/config/protected/git/org_project1/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/protected/git/org_project1/playbooks/job-child-notok.yaml b/tests/fixtures/config/protected/git/org_project1/playbooks/job-child-notok.yaml
new file mode 100644
index 000000000..f679dceae
--- /dev/null
+++ b/tests/fixtures/config/protected/git/org_project1/playbooks/job-child-notok.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+ tasks: []
diff --git a/tests/fixtures/config/protected/git/org_project1/playbooks/placeholder b/tests/fixtures/config/protected/git/org_project1/playbooks/placeholder
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/fixtures/config/protected/git/org_project1/playbooks/placeholder
diff --git a/tests/fixtures/config/protected/main.yaml b/tests/fixtures/config/protected/main.yaml
new file mode 100644
index 000000000..5f57245cc
--- /dev/null
+++ b/tests/fixtures/config/protected/main.yaml
@@ -0,0 +1,9 @@
+- tenant:
+ name: tenant-one
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
+ - org/project1
diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py
index 197b5256d..c45da94cb 100644
--- a/tests/unit/test_connection.py
+++ b/tests/unit/test_connection.py
@@ -115,7 +115,7 @@ class TestSQLConnection(ZuulDBTestCase):
self.assertEqual('check', buildset0['pipeline'])
self.assertEqual('org/project', buildset0['project'])
self.assertEqual(1, buildset0['change'])
- self.assertEqual(1, buildset0['patchset'])
+ self.assertEqual('1', buildset0['patchset'])
self.assertEqual('SUCCESS', buildset0['result'])
self.assertEqual('Build succeeded.', buildset0['message'])
self.assertEqual('tenant-one', buildset0['tenant'])
@@ -141,7 +141,7 @@ class TestSQLConnection(ZuulDBTestCase):
self.assertEqual('check', buildset1['pipeline'])
self.assertEqual('org/project', buildset1['project'])
self.assertEqual(2, buildset1['change'])
- self.assertEqual(1, buildset1['patchset'])
+ self.assertEqual('1', buildset1['patchset'])
self.assertEqual('FAILURE', buildset1['result'])
self.assertEqual('Build failed.', buildset1['message'])
@@ -194,7 +194,7 @@ class TestSQLConnection(ZuulDBTestCase):
self.assertEqual('check', buildsets_resultsdb[0]['pipeline'])
self.assertEqual('org/project', buildsets_resultsdb[0]['project'])
self.assertEqual(1, buildsets_resultsdb[0]['change'])
- self.assertEqual(1, buildsets_resultsdb[0]['patchset'])
+ self.assertEqual('1', buildsets_resultsdb[0]['patchset'])
self.assertEqual('SUCCESS', buildsets_resultsdb[0]['result'])
self.assertEqual('Build succeeded.', buildsets_resultsdb[0]['message'])
@@ -215,7 +215,7 @@ class TestSQLConnection(ZuulDBTestCase):
self.assertEqual(
'org/project', buildsets_resultsdb_failures[0]['project'])
self.assertEqual(2, buildsets_resultsdb_failures[0]['change'])
- self.assertEqual(1, buildsets_resultsdb_failures[0]['patchset'])
+ self.assertEqual('1', buildsets_resultsdb_failures[0]['patchset'])
self.assertEqual('FAILURE', buildsets_resultsdb_failures[0]['result'])
self.assertEqual(
'Build failed.', buildsets_resultsdb_failures[0]['message'])
diff --git a/tests/unit/test_gerrit_crd.py b/tests/unit/test_gerrit_crd.py
index 732bc3d60..a8924b902 100644
--- a/tests/unit/test_gerrit_crd.py
+++ b/tests/unit/test_gerrit_crd.py
@@ -90,6 +90,40 @@ class TestGerritCRD(ZuulTestCase):
'project-merge', 'org/project1').changes
self.assertEqual(changes, '2,1 1,1')
+ def test_crd_gate_triangle(self):
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+ A.addApproval('Code-Review', 2)
+ B.addApproval('Code-Review', 2)
+ C.addApproval('Code-Review', 2)
+ A.addApproval('Approved', 1)
+ B.addApproval('Approved', 1)
+
+ # C-->B
+ # \ /
+ # v
+ # A
+
+ # C Depends-On: A
+ C.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ C.subject, A.data['url'])
+ # B Depends-On: A
+ B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ B.subject, A.data['url'])
+ # C git-depends on B
+ C.setDependsOn(B, 1)
+ self.fake_gerrit.addEvent(C.addApproval('Approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(self.history[-1].changes, '1,1 2,1 3,1')
+
def test_crd_branch(self):
"Test cross-repo dependencies in multiple branches"
@@ -257,6 +291,7 @@ class TestGerritCRD(ZuulTestCase):
B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
A.addApproval('Code-Review', 2)
B.addApproval('Code-Review', 2)
+ B.addApproval('Approved', 1)
# A -> B -> A (via commit-depends)
@@ -522,6 +557,30 @@ class TestGerritCRD(ZuulTestCase):
for job in self.history:
self.assertEqual(len(job.changes.split()), 1)
+ def test_crd_check_triangle(self):
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+
+ # C-->B
+ # \ /
+ # v
+ # A
+
+ # C Depends-On: A
+ C.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ C.subject, A.data['url'])
+ # B Depends-On: A
+ B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ B.subject, A.data['url'])
+ # C git-depends on B
+ C.setDependsOn(B, 1)
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(C.reported, 1)
+ self.assertEqual(self.history[0].changes, '1,1 2,1 3,1')
+
@simple_layout('layouts/three-projects.yaml')
def test_crd_check_transitive(self):
"Test transitive cross-repo dependencies"
diff --git a/tests/unit/test_gerrit_legacy_crd.py b/tests/unit/test_gerrit_legacy_crd.py
index c711e4d95..90c93ecae 100644
--- a/tests/unit/test_gerrit_legacy_crd.py
+++ b/tests/unit/test_gerrit_legacy_crd.py
@@ -260,6 +260,7 @@ class TestGerritLegacyCRD(ZuulTestCase):
B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
A.addApproval('Code-Review', 2)
B.addApproval('Code-Review', 2)
+ B.addApproval('Approved', 1)
# A -> B -> A (via commit-depends)
diff --git a/tests/unit/test_git_driver.py b/tests/unit/test_git_driver.py
index b9e6c6e92..e8762d0d2 100644
--- a/tests/unit/test_git_driver.py
+++ b/tests/unit/test_git_driver.py
@@ -24,6 +24,11 @@ class TestGitDriver(ZuulTestCase):
config_file = 'zuul-git-driver.conf'
tenant_config_file = 'config/git-driver/main.yaml'
+ def setUp(self):
+ super(TestGitDriver, self).setUp()
+ self.git_connection = self.sched.connections.getSource('git').\
+ connection
+
def setup_config(self):
super(TestGitDriver, self).setup_config()
self.config.set('connection git', 'baseurl', self.upstream_root)
@@ -70,8 +75,8 @@ class TestGitDriver(ZuulTestCase):
self.addCommitToRepo(
'common-config', 'Change zuul.yaml configuration', files)
- # Let some time for the tenant reconfiguration to happen
- time.sleep(2)
+ # Wait for the tenant reconfiguration to happen
+ count = self.waitForEvent()
self.waitUntilSettled()
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
@@ -109,8 +114,8 @@ class TestGitDriver(ZuulTestCase):
# Restart the git watcher
self.sched.connections.getSource('git').connection.w_pause = False
- # Let some time for the tenant reconfiguration to happen
- time.sleep(2)
+ # Wait for the tenant reconfiguration to happen
+ self.waitForEvent(count)
self.waitUntilSettled()
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
@@ -123,23 +128,33 @@ class TestGitDriver(ZuulTestCase):
def ensure_watcher_has_context(self):
# Make sure watcher have read initial refs shas
- cnx = self.sched.connections.getSource('git').connection
delay = 0.1
max_delay = 1
- while not cnx.projects_refs:
+ while not self.git_connection.projects_refs:
time.sleep(delay)
max_delay -= delay
if max_delay <= 0:
raise Exception("Timeout waiting for initial read")
+ return self.git_connection.watcher_thread._event_count
+
+ def waitForEvent(self, initial_count=0):
+ delay = 0.1
+ max_delay = 1
+ while self.git_connection.watcher_thread._event_count <= initial_count:
+ time.sleep(delay)
+ max_delay -= delay
+ if max_delay <= 0:
+ raise Exception("Timeout waiting for event")
+ return self.git_connection.watcher_thread._event_count
@simple_layout('layouts/basic-git.yaml', driver='git')
def test_ref_updated_event(self):
- self.ensure_watcher_has_context()
+ count = self.ensure_watcher_has_context()
# Add a commit to trigger a ref-updated event
self.addCommitToRepo(
'org/project', 'A change for ref-updated', {'f1': 'Content'})
- # Let some time for the git watcher to detect the ref-update event
- time.sleep(0.2)
+ # Wait for the git watcher to detect the ref-update event
+ self.waitForEvent(count)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
self.assertEqual('SUCCESS',
@@ -147,12 +162,12 @@ class TestGitDriver(ZuulTestCase):
@simple_layout('layouts/basic-git.yaml', driver='git')
def test_ref_created(self):
- self.ensure_watcher_has_context()
+ count = self.ensure_watcher_has_context()
# Tag HEAD to trigger a ref-updated event
self.addTagToRepo(
'org/project', 'atag', 'HEAD')
- # Let some time for the git watcher to detect the ref-update event
- time.sleep(0.2)
+ # Wait for the git watcher to detect the ref-update event
+ self.waitForEvent(count)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
self.assertEqual('SUCCESS',
@@ -160,12 +175,12 @@ class TestGitDriver(ZuulTestCase):
@simple_layout('layouts/basic-git.yaml', driver='git')
def test_ref_deleted(self):
- self.ensure_watcher_has_context()
+ count = self.ensure_watcher_has_context()
# Delete default tag init to trigger a ref-updated event
self.delTagFromRepo(
'org/project', 'init')
- # Let some time for the git watcher to detect the ref-update event
- time.sleep(0.2)
+ # Wait for the git watcher to detect the ref-update event
+ self.waitForEvent(count)
self.waitUntilSettled()
# Make sure no job as run as ignore-delete is True by default
self.assertEqual(len(self.history), 0)
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 6ab1a2650..3942b0be8 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -50,6 +50,12 @@ class TestGithubDriver(ZuulTestCase):
self.assertEqual(str(A.head_sha), zuulvars['patchset'])
self.assertEqual('master', zuulvars['branch'])
self.assertEqual(1, len(A.comments))
+ self.assertThat(
+ A.comments[0],
+ MatchesRegex('.*\[project-test1 \]\(.*\).*', re.DOTALL))
+ self.assertThat(
+ A.comments[0],
+ MatchesRegex('.*\[project-test2 \]\(.*\).*', re.DOTALL))
self.assertEqual(2, len(self.history))
# test_pull_unmatched_branch_event(self):
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 2779e6e66..163a58b90 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -73,6 +73,110 @@ class TestMultipleTenants(AnsibleZuulTestCase):
"not affect tenant one")
+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
+
+ - project:
+ name: org/project
+ check:
+ jobs:
+ - job-child-ok
+
+ - job:
+ name: job-child-ok
+ parent: job-protected
+
+ - 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()
+
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '1')
+
+ def test_protected_reset(self):
+ # try to reset protected flag
+ in_repo_conf = textwrap.dedent(
+ """
+ - job:
+ name: job-protected
+ protected: true
+ run: playbooks/job-protected.yaml
+
+ - job:
+ name: job-child-reset-protected
+ parent: job-protected
+ protected: false
+
+ - project:
+ name: org/project
+ check:
+ jobs:
+ - job-child-reset-protected
+
+ """)
+
+ 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()
+
+ # The second patch tried to override some variables.
+ # Thus it should fail.
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '-1')
+ self.assertIn('Unable to reset protected attribute', A.messages[0])
+
+ def test_protected_inherit_not_ok(self):
+ # try to inherit from a protected job in different project
+ in_repo_conf = textwrap.dedent(
+ """
+ - job:
+ name: job-child-notok
+ run: playbooks/job-child-notok.yaml
+ parent: job-protected
+
+ - project:
+ name: org/project1
+ check:
+ jobs:
+ - job-child-notok
+
+ """)
+
+ 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.assertEqual(A.reported, 1)
+ self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '-1')
+ self.assertIn(
+ "which is defined in review.example.com/org/project is protected "
+ "and cannot be inherited from other projects.", A.messages[0])
+
+
class TestFinal(ZuulTestCase):
tenant_config_file = 'config/final/main.yaml'
diff --git a/tools/encrypt_secret.py b/tools/encrypt_secret.py
index c0ee9be64..4cb166631 100755
--- a/tools/encrypt_secret.py
+++ b/tools/encrypt_secret.py
@@ -46,6 +46,8 @@ def main():
# TODO(jeblair): Throw a fit if SSL is not used.
parser.add_argument('project',
help="The name of the project.")
+ parser.add_argument('--strip', action='store_true', default=False,
+ help="Strip whitespace from beginning/end of input.")
parser.add_argument('--infile',
default=None,
help="A filename whose contents will be encrypted. "
@@ -68,6 +70,8 @@ def main():
plaintext = sys.stdin.read()
plaintext = plaintext.encode("utf-8")
+ if args.strip:
+ plaintext = plaintext.strip()
pubkey_file = tempfile.NamedTemporaryFile(delete=False)
try:
diff --git a/tools/github-debugging.py b/tools/github-debugging.py
index 171627ab9..101fd1118 100644..100755
--- a/tools/github-debugging.py
+++ b/tools/github-debugging.py
@@ -1,55 +1,71 @@
-import github3
+#!/usr/bin/env python3
+
import logging
-import time
+
+from zuul.driver.github.githubconnection import GithubConnection
+from zuul.driver.github import GithubDriver
+from zuul.model import Change, Project
# This is a template with boilerplate code for debugging github issues
# TODO: for real use override the following variables
-url = 'https://example.com'
+server = 'github.com'
api_token = 'xxxx'
-org = 'org'
-project = 'project'
-pull_nr = 3
+org = 'example'
+repo = 'sandbox'
+pull_nr = 8
+
+
+def configure_logging(context):
+ stream_handler = logging.StreamHandler()
+ logger = logging.getLogger(context)
+ logger.addHandler(stream_handler)
+ logger.setLevel(logging.DEBUG)
+
+
+# uncomment for more logging
+# configure_logging('urllib3')
+# configure_logging('github3')
+# configure_logging('cachecontrol')
+
+
+# This is all that's needed for getting a usable github connection
+def create_connection(server, api_token):
+ driver = GithubDriver()
+ connection_config = {
+ 'server': server,
+ 'api_token': api_token,
+ }
+ conn = GithubConnection(driver, 'github', connection_config)
+ conn._authenticateGithubAPI()
+ return conn
-# Send the logs to stderr as well
-stream_handler = logging.StreamHandler()
+def get_change(connection: GithubConnection,
+ org: str,
+ repo: str,
+ pull: int) -> Change:
+ p = Project("%s/%s" % (org, repo), connection.source)
+ github = connection.getGithubClient(p)
+ pr = github.pull_request(org, repo, pull)
+ sha = pr.head.sha
+ return conn._getChange(p, pull, sha, True)
-logger_urllib3 = logging.getLogger('requests.packages.logger_urllib3')
-# logger_urllib3.addHandler(stream_handler)
-logger_urllib3.setLevel(logging.DEBUG)
-logger = logging.getLogger('github3')
-# logger.addHandler(stream_handler)
-logger.setLevel(logging.DEBUG)
+# create github connection
+conn = create_connection(server, api_token)
-github = github3.GitHubEnterprise(url)
+# Now we can do anything we want with the connection, e.g. check canMerge for
+# a pull request.
+change = get_change(conn, org, repo, pull_nr)
+print(conn.canMerge(change, {'cc/gate2'}))
-# This is the currently broken cache adapter, enable or replace it to debug
-# caching
-# import cachecontrol
-# from cachecontrol.cache import DictCache
-# cache_adapter = cachecontrol.CacheControlAdapter(
-# DictCache(),
-# cache_etags=True)
+# Or just use the github object.
+# github = conn.getGithubClient()
#
-# github.session.mount('http://', cache_adapter)
-# github.session.mount('https://', cache_adapter)
-
-
-github.login(token=api_token)
-
-i = 0
-while True:
- pr = github.pull_request(org, project, pull_nr)
- prdict = pr.as_dict()
- issue = pr.issue()
- labels = list(issue.labels())
- print(labels)
- i += 1
- print(i)
- time.sleep(1)
+# repository = github.repository(org, repo)
+# print(repository.as_dict())
diff --git a/zuul/cmd/__init__.py b/zuul/cmd/__init__.py
index 07d4a8d08..bf11c6fc9 100755
--- a/zuul/cmd/__init__.py
+++ b/zuul/cmd/__init__.py
@@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import abc
import argparse
import configparser
import daemon
@@ -156,7 +157,7 @@ class ZuulApp(object):
self.connections.configure(self.config, source_only)
-class ZuulDaemonApp(ZuulApp):
+class ZuulDaemonApp(ZuulApp, metaclass=abc.ABCMeta):
def createParser(self):
parser = super(ZuulDaemonApp, self).createParser()
parser.add_argument('-d', dest='nodaemon', action='store_true',
@@ -169,6 +170,21 @@ class ZuulDaemonApp(ZuulApp):
expand_user=True)
return pid_fn
+ @abc.abstractmethod
+ def exit_handler(self, signum, frame):
+ """
+ This is a signal handler which is called on SIGINT and SIGTERM and must
+ take care of stopping the application.
+ """
+ pass
+
+ @abc.abstractmethod
+ def run(self):
+ """
+ This is the main run method of the application.
+ """
+ pass
+
def main(self):
self.parseArguments()
self.readConfig()
@@ -176,7 +192,13 @@ class ZuulDaemonApp(ZuulApp):
pid_fn = self.getPidFile()
pid = pid_file_module.TimeoutPIDLockFile(pid_fn, 10)
+ # Early register the stack dump handler for all zuul apps. This makes
+ # it possible to also gather stack dumps during startup hangs.
+ signal.signal(signal.SIGUSR2, stack_dump_handler)
+
if self.args.nodaemon:
+ signal.signal(signal.SIGTERM, self.exit_handler)
+ signal.signal(signal.SIGINT, self.exit_handler)
self.run()
else:
# Exercise the pidfile before we do anything else (including
diff --git a/zuul/cmd/executor.py b/zuul/cmd/executor.py
index ad7aaa837..5b06f0cca 100755
--- a/zuul/cmd/executor.py
+++ b/zuul/cmd/executor.py
@@ -17,7 +17,6 @@
import logging
import os
import sys
-import signal
import tempfile
import zuul.cmd
@@ -49,9 +48,8 @@ class Executor(zuul.cmd.ZuulDaemonApp):
if self.args.command:
self.args.nodaemon = True
- def exit_handler(self):
+ def exit_handler(self, signum, frame):
self.executor.stop()
- self.executor.join()
def start_log_streamer(self):
pipe_read, pipe_write = os.pipe()
@@ -108,18 +106,7 @@ class Executor(zuul.cmd.ZuulDaemonApp):
log_streaming_port=self.finger_port)
self.executor.start()
- signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
-
- if self.args.nodaemon:
- while True:
- try:
- signal.pause()
- except KeyboardInterrupt:
- print("Ctrl + C: asking executor to exit nicely...\n")
- self.exit_handler()
- sys.exit(0)
- else:
- self.executor.join()
+ self.executor.join()
def main():
diff --git a/zuul/cmd/fingergw.py b/zuul/cmd/fingergw.py
index 920eed8f2..0d47f0848 100644
--- a/zuul/cmd/fingergw.py
+++ b/zuul/cmd/fingergw.py
@@ -14,7 +14,6 @@
# under the License.
import logging
-import signal
import sys
import zuul.cmd
@@ -47,6 +46,9 @@ class FingerGatewayApp(zuul.cmd.ZuulDaemonApp):
if self.args.command:
self.args.nodaemon = True
+ def exit_handler(self, signum, frame):
+ self.stop()
+
def run(self):
'''
Main entry point for the FingerGatewayApp.
@@ -84,19 +86,7 @@ class FingerGatewayApp(zuul.cmd.ZuulDaemonApp):
self.log.info('Starting Zuul finger gateway app')
self.gateway.start()
- if self.args.nodaemon:
- # NOTE(Shrews): When running in non-daemon mode, although sending
- # the 'stop' command via the command socket will shutdown the
- # gateway, it's still necessary to Ctrl+C to stop the app.
- while True:
- try:
- signal.pause()
- except KeyboardInterrupt:
- print("Ctrl + C: asking gateway to exit nicely...\n")
- self.stop()
- break
- else:
- self.gateway.wait()
+ self.gateway.wait()
self.log.info('Stopped Zuul finger gateway app')
diff --git a/zuul/cmd/merger.py b/zuul/cmd/merger.py
index 7db1beeaf..390191f4a 100755
--- a/zuul/cmd/merger.py
+++ b/zuul/cmd/merger.py
@@ -14,7 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import signal
import sys
import zuul.cmd
@@ -42,9 +41,8 @@ class Merger(zuul.cmd.ZuulDaemonApp):
if self.args.command:
self.args.nodaemon = True
- def exit_handler(self):
+ def exit_handler(self, signum, frame):
self.merger.stop()
- self.merger.join()
def run(self):
# See comment at top of file about zuul imports
@@ -61,18 +59,7 @@ class Merger(zuul.cmd.ZuulDaemonApp):
self.connections)
self.merger.start()
- signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
-
- if self.args.nodaemon:
- while True:
- try:
- signal.pause()
- except KeyboardInterrupt:
- print("Ctrl + C: asking merger to exit nicely...\n")
- self.exit_handler()
- sys.exit(0)
- else:
- self.merger.join()
+ self.merger.join()
def main():
diff --git a/zuul/cmd/scheduler.py b/zuul/cmd/scheduler.py
index 7722d6e9c..3cffa1010 100755
--- a/zuul/cmd/scheduler.py
+++ b/zuul/cmd/scheduler.py
@@ -61,14 +61,9 @@ class Scheduler(zuul.cmd.ZuulDaemonApp):
self.log.exception("Reconfiguration failed:")
signal.signal(signal.SIGHUP, self.reconfigure_handler)
- def exit_handler(self):
+ def exit_handler(self, signum, frame):
self.sched.exit()
- self.sched.join()
- self.stop_gear_server()
-
- def term_handler(self, signum, frame):
self.stop_gear_server()
- os._exit(0)
def start_gear_server(self):
pipe_read, pipe_write = os.pipe()
@@ -126,7 +121,6 @@ class Scheduler(zuul.cmd.ZuulDaemonApp):
import zuul.webapp
import zuul.zk
- signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
if (self.config.has_option('gearman_server', 'start') and
self.config.getboolean('gearman_server', 'start')):
self.start_gear_server()
@@ -179,16 +173,7 @@ class Scheduler(zuul.cmd.ZuulDaemonApp):
signal.signal(signal.SIGHUP, self.reconfigure_handler)
- if self.args.nodaemon:
- while True:
- try:
- signal.pause()
- except KeyboardInterrupt:
- print("Ctrl + C: asking scheduler to exit nicely...\n")
- self.exit_handler()
- sys.exit(0)
- else:
- self.sched.join()
+ self.sched.join()
def main():
diff --git a/zuul/cmd/web.py b/zuul/cmd/web.py
index 4687de653..78392db51 100755
--- a/zuul/cmd/web.py
+++ b/zuul/cmd/web.py
@@ -89,12 +89,6 @@ class WebServer(zuul.cmd.ZuulDaemonApp):
name='web')
self.thread.start()
- try:
- signal.pause()
- except KeyboardInterrupt:
- print("Ctrl + C: asking web server to exit nicely...\n")
- self.exit_handler(signal.SIGINT, None)
-
self.thread.join()
loop.stop()
loop.close()
@@ -106,8 +100,6 @@ class WebServer(zuul.cmd.ZuulDaemonApp):
self.configure_connections()
- signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
-
try:
self._run()
except Exception:
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 3a7e9b970..d62237043 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -474,6 +474,7 @@ class JobParser(object):
# Attributes of a job that can also be used in Project and ProjectTemplate
job_attributes = {'parent': vs.Any(str, None),
'final': bool,
+ 'protected': bool,
'failure-message': str,
'success-message': str,
'failure-url': str,
@@ -513,6 +514,7 @@ class JobParser(object):
simple_attributes = [
'final',
+ 'protected',
'timeout',
'workspace',
'voting',
diff --git a/zuul/driver/git/gitconnection.py b/zuul/driver/git/gitconnection.py
index 03b24cadc..1886cfcca 100644
--- a/zuul/driver/git/gitconnection.py
+++ b/zuul/driver/git/gitconnection.py
@@ -38,6 +38,8 @@ class GitWatcher(threading.Thread):
self.poll_delay = poll_delay
self._stopped = False
self.projects_refs = self.git_connection.projects_refs
+ # This is used by the test framework
+ self._event_count = 0
def compareRefs(self, project, refs):
partial_events = []
@@ -112,6 +114,7 @@ class GitWatcher(threading.Thread):
self.git_connection.logEvent(event)
# Pass the event to the scheduler
self.git_connection.sched.addEvent(event)
+ self._event_count += 1
except Exception as e:
self.log.debug("Unexpected issue in _run loop: %s" % str(e))
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index a7aefe0cd..b766c6f55 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -684,8 +684,7 @@ class GithubConnection(BaseConnection):
change.files = self.getPushedFileNames(event)
return change
- def _getChange(self, project, number, patchset=None, refresh=False,
- history=None):
+ def _getChange(self, project, number, patchset=None, refresh=False):
key = (project.name, number, patchset)
change = self._change_cache.get(key)
if change and not refresh:
@@ -697,7 +696,7 @@ class GithubConnection(BaseConnection):
change.patchset = patchset
self._change_cache[key] = change
try:
- self._updateChange(change, history)
+ self._updateChange(change)
except Exception:
if key in self._change_cache:
del self._change_cache[key]
@@ -769,13 +768,7 @@ class GithubConnection(BaseConnection):
return changes
- def _updateChange(self, change, history=None):
- # If this change is already in the history, we have a cyclic
- # dependency loop and we do not need to update again, since it
- # was done in a previous frame.
- if history and (change.project.name, change.number) in history:
- return change
-
+ def _updateChange(self, change):
self.log.info("Updating %s" % (change,))
change.pr = self.getPull(change.project.name, change.number)
change.ref = "refs/pull/%s/head" % change.number
@@ -794,12 +787,6 @@ class GithubConnection(BaseConnection):
change.updated_at = self._ghTimestampToDate(
change.pr.get('updated_at'))
- if history is None:
- history = []
- else:
- history = history[:]
- history.append((change.project.name, change.number))
-
self.sched.onChangeUpdated(change)
return change
diff --git a/zuul/driver/github/githubreporter.py b/zuul/driver/github/githubreporter.py
index 505757fa2..848ae1b3a 100644
--- a/zuul/driver/github/githubreporter.py
+++ b/zuul/driver/github/githubreporter.py
@@ -75,6 +75,14 @@ class GithubReporter(BaseReporter):
msg = self._formatItemReportMergeFailure(item)
self.addPullComment(item, msg)
+ def _formatItemReportJobs(self, item):
+ # Return the list of jobs portion of the report
+ ret = ''
+ jobs_fields = self._getItemReportJobsFields(item)
+ for job_fields in jobs_fields:
+ ret += '- [%s](%s) : %s%s%s%s\n' % job_fields
+ return ret
+
def addPullComment(self, item, comment=None):
message = comment or self._formatItemReport(item)
project = item.change.project.name
diff --git a/zuul/driver/sql/alembic/versions/19d3a3ebfe1d_change_patchset_to_string.py b/zuul/driver/sql/alembic/versions/19d3a3ebfe1d_change_patchset_to_string.py
new file mode 100644
index 000000000..505a1ed73
--- /dev/null
+++ b/zuul/driver/sql/alembic/versions/19d3a3ebfe1d_change_patchset_to_string.py
@@ -0,0 +1,29 @@
+"""Change patchset to string
+
+Revision ID: 19d3a3ebfe1d
+Revises: cfc0dc45f341
+Create Date: 2018-01-10 07:42:16.546751
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '19d3a3ebfe1d'
+down_revision = 'cfc0dc45f341'
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+
+BUILDSET_TABLE = 'zuul_buildset'
+
+
+def upgrade(table_prefix=''):
+ op.alter_column(table_prefix + BUILDSET_TABLE,
+ 'patchset',
+ type_=sa.String(255),
+ existing_nullable=True)
+
+
+def downgrade():
+ raise Exception("Downgrades not supported")
diff --git a/zuul/driver/sql/sqlconnection.py b/zuul/driver/sql/sqlconnection.py
index 285d0c23f..715d72bba 100644
--- a/zuul/driver/sql/sqlconnection.py
+++ b/zuul/driver/sql/sqlconnection.py
@@ -92,7 +92,7 @@ class SQLConnection(BaseConnection):
sa.Column('pipeline', sa.String(255)),
sa.Column('project', sa.String(255)),
sa.Column('change', sa.Integer, nullable=True),
- sa.Column('patchset', sa.Integer, nullable=True),
+ sa.Column('patchset', sa.String(255), nullable=True),
sa.Column('ref', sa.String(255)),
sa.Column('oldrev', sa.String(255)),
sa.Column('newrev', sa.String(255)),
diff --git a/zuul/manager/dependent.py b/zuul/manager/dependent.py
index 20b376d6a..5ad761196 100644
--- a/zuul/manager/dependent.py
+++ b/zuul/manager/dependent.py
@@ -141,13 +141,16 @@ class DependentPipelineManager(PipelineManager):
def enqueueChangesAhead(self, change, quiet, ignore_requirements,
change_queue, history=None):
- if history and change.number in history:
+ if history and change in history:
# detected dependency cycle
self.log.warn("Dependency cycle detected")
return False
if hasattr(change, 'number'):
history = history or []
- history.append(change.number)
+ history = history + [change]
+ else:
+ # Don't enqueue dependencies ahead of a non-change ref.
+ return True
ret = self.checkForChangesNeededBy(change, change_queue)
if ret in [True, False]:
diff --git a/zuul/manager/independent.py b/zuul/manager/independent.py
index 0c2baf010..9da40d582 100644
--- a/zuul/manager/independent.py
+++ b/zuul/manager/independent.py
@@ -34,13 +34,13 @@ class IndependentPipelineManager(PipelineManager):
def enqueueChangesAhead(self, change, quiet, ignore_requirements,
change_queue, history=None):
- if history and change.number in history:
+ if history and change in history:
# detected dependency cycle
self.log.warn("Dependency cycle detected")
return False
if hasattr(change, 'number'):
history = history or []
- history.append(change.number)
+ history = history + [change]
else:
# Don't enqueue dependencies ahead of a non-change ref.
return True
diff --git a/zuul/model.py b/zuul/model.py
index bac9e4cc8..29c5a9d7e 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -845,6 +845,7 @@ class Job(object):
semaphore=None,
attempts=3,
final=False,
+ protected=None,
roles=(),
required_projects={},
allowed_projects=None,
@@ -862,6 +863,7 @@ class Job(object):
inheritance_path=(),
parent_data=None,
description=None,
+ protected_origin=None,
)
self.inheritable_attributes = {}
@@ -1039,12 +1041,21 @@ class Job(object):
for k in self.execution_attributes:
if (other._get(k) is not None and
- k not in set(['final'])):
+ k not in set(['final', 'protected'])):
if self.final:
raise Exception("Unable to modify final job %s attribute "
"%s=%s with variant %s" % (
repr(self), k, other._get(k),
repr(other)))
+ if self.protected_origin:
+ # this is a protected job, check origin of job definition
+ this_origin = self.protected_origin
+ other_origin = other.source_context.project.canonical_name
+ if this_origin != other_origin:
+ raise Exception("Job %s which is defined in %s is "
+ "protected and cannot be inherited "
+ "from other projects."
+ % (repr(self), this_origin))
if k not in set(['pre_run', 'run', 'post_run', 'roles',
'variables', 'required_projects']):
# TODO(jeblair): determine if deepcopy is required
@@ -1055,6 +1066,17 @@ class Job(object):
if other.final != self.attributes['final']:
self.final = other.final
+ # Protected may only be set to true
+ if other.protected is not None:
+ # don't allow to reset protected flag
+ if not other.protected and self.protected_origin:
+ raise Exception("Unable to reset protected attribute of job"
+ " %s by job %s" % (
+ repr(self), repr(other)))
+ if not self.protected_origin:
+ self.protected_origin = \
+ other.source_context.project.canonical_name
+
# We must update roles before any playbook contexts
if other._get('roles') is not None:
self.addRoles(other.roles)
diff --git a/zuul/reporter/__init__.py b/zuul/reporter/__init__.py
index ecf88553a..1bff5cb94 100644
--- a/zuul/reporter/__init__.py
+++ b/zuul/reporter/__init__.py
@@ -109,12 +109,10 @@ class BaseReporter(object, metaclass=abc.ABCMeta):
else:
return self._formatItemReport(item)
- def _formatItemReportJobs(self, item):
- # Return the list of jobs portion of the report
- ret = ''
-
+ def _getItemReportJobsFields(self, item):
+ # Extract the report elements from an item
config = self.connection.sched.config
-
+ jobs_fields = []
for job in item.getJobs():
build = item.current_build_set.getBuild(job.name)
(result, url) = item.formatJobResult(job)
@@ -147,6 +145,13 @@ class BaseReporter(object, metaclass=abc.ABCMeta):
else:
error = ''
name = job.name + ' '
- ret += '- %s%s : %s%s%s%s\n' % (name, url, result, error,
- elapsed, voting)
+ jobs_fields.append((name, url, result, error, elapsed, voting))
+ return jobs_fields
+
+ def _formatItemReportJobs(self, item):
+ # Return the list of jobs portion of the report
+ ret = ''
+ jobs_fields = self._getItemReportJobsFields(item)
+ for job_fields in jobs_fields:
+ ret += '- %s%s : %s%s%s%s\n' % job_fields
return ret