Add driver-specific pipeline requirements
As we expand the Github driver, we're seeing a need to specify driver-specific
pipeline requirements. To accomplish this, bump the require/reject pipeline
keywords down a level underneath connection names. This lets users specify
per-source pipeline requirements.
This adds new API methods for sources to create the new pipeline filters
(by returning instances or subclasses of RefFilter, which used to be called
ChangeishFilter).
This change also creates and/or moves driver-specific subclasses of EventFilter
and TriggerEvent in(to) their respective drivers.
Change-Id: Ia56c254e3aa591a688103db5b04b3dddae7b2da4
diff --git a/doc/source/developer/datamodel.rst b/doc/source/developer/datamodel.rst
index 2996ff4..acb8612 100644
--- a/doc/source/developer/datamodel.rst
+++ b/doc/source/developer/datamodel.rst
@@ -54,7 +54,7 @@
Filters
~~~~~~~
-.. autoclass:: zuul.model.ChangeishFilter
+.. autoclass:: zuul.model.RefFilter
.. autoclass:: zuul.model.EventFilter
diff --git a/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
index 1a5baed..efc3b32 100644
--- a/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
@@ -11,8 +11,9 @@
gerrit:
verified: -1
require:
- approval:
- - email: jenkins@example.com
+ gerrit:
+ approval:
+ - email: jenkins@example.com
- pipeline:
name: trigger
diff --git a/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
index fa230de..6f0601d 100644
--- a/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
@@ -11,9 +11,10 @@
gerrit:
verified: -1
require:
- approval:
- - username: jenkins
- newer-than: 48h
+ gerrit:
+ approval:
+ - username: jenkins
+ newer-than: 48h
- pipeline:
name: trigger
diff --git a/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
index 14541b6..77ee388 100644
--- a/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
@@ -11,9 +11,10 @@
gerrit:
verified: -1
require:
- approval:
- - username: jenkins
- older-than: 48h
+ gerrit:
+ approval:
+ - username: jenkins
+ older-than: 48h
- pipeline:
name: trigger
diff --git a/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
index 61f3819..9e9d000 100644
--- a/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
@@ -2,8 +2,9 @@
name: pipeline
manager: independent
reject:
- approval:
- - username: jenkins
+ gerrit:
+ approval:
+ - username: jenkins
trigger:
gerrit:
- event: comment-added
diff --git a/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
index 32a7582..b08a105 100644
--- a/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
@@ -2,16 +2,18 @@
name: pipeline
manager: independent
require:
- approval:
- - username: jenkins
- verified:
- - 1
- - 2
+ gerrit:
+ approval:
+ - username: jenkins
+ verified:
+ - 1
+ - 2
reject:
- approval:
- - verified:
- - -1
- - -2
+ gerrit:
+ approval:
+ - verified:
+ - -1
+ - -2
trigger:
gerrit:
- event: comment-added
diff --git a/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
index ffc3453..bd9dc8f 100644
--- a/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
@@ -2,7 +2,8 @@
name: current-check
manager: independent
require:
- current-patchset: true
+ gerrit:
+ current-patchset: true
trigger:
gerrit:
- event: patchset-created
@@ -18,7 +19,8 @@
name: open-check
manager: independent
require:
- open: true
+ gerrit:
+ open: true
trigger:
gerrit:
- event: patchset-created
@@ -34,7 +36,8 @@
name: status-check
manager: independent
require:
- status: NEW
+ gerrit:
+ status: NEW
trigger:
gerrit:
- event: patchset-created
diff --git a/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
index bc1083a..455d9de 100644
--- a/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
@@ -11,8 +11,9 @@
gerrit:
verified: -1
require:
- approval:
- - username: ^(jenkins|zuul)$
+ gerrit:
+ approval:
+ - username: ^(jenkins|zuul)$
- pipeline:
name: trigger
diff --git a/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
index 7d9164d..799282d 100644
--- a/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
@@ -2,9 +2,10 @@
name: pipeline
manager: independent
require:
- approval:
- - username: jenkins
- verified: 1
+ gerrit:
+ approval:
+ - username: jenkins
+ verified: 1
trigger:
gerrit:
- event: comment-added
diff --git a/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
index 7308c8a..f337371 100644
--- a/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
@@ -2,11 +2,12 @@
name: pipeline
manager: independent
require:
- approval:
- - username: jenkins
- verified:
- - 1
- - 2
+ gerrit:
+ approval:
+ - username: jenkins
+ verified:
+ - 1
+ - 2
trigger:
gerrit:
- event: comment-added
diff --git a/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml b/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
index 2b21c9b..351092c 100644
--- a/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
@@ -2,8 +2,9 @@
name: check
manager: independent
require:
- approval:
- - verified: -1
+ gerrit:
+ approval:
+ - verified: -1
trigger:
gerrit:
- event: patchset-created
@@ -21,8 +22,9 @@
name: gate
manager: dependent
require:
- approval:
- - verified: 1
+ gerrit:
+ approval:
+ - verified: 1
trigger:
gerrit:
- event: comment-added
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 50f5d16..d95e861 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -605,6 +605,8 @@
methods = {
'trigger': 'getTriggerSchema',
'reporter': 'getReporterSchema',
+ 'require': 'getRequireSchema',
+ 'reject': 'getRejectSchema',
}
schema = {}
@@ -666,6 +668,10 @@
'_source_context': model.SourceContext,
'_start_mark': yaml.Mark,
}
+ pipeline['require'] = PipelineParser.getDriverSchema('require',
+ connections)
+ pipeline['reject'] = PipelineParser.getDriverSchema('reject',
+ connections)
pipeline['trigger'] = vs.Required(
PipelineParser.getDriverSchema('trigger', connections))
for action in ['start', 'success', 'failure', 'merge-failure',
@@ -742,24 +748,21 @@
pipeline.setManager(manager)
layout.pipelines[conf['name']] = pipeline
- if 'require' in conf or 'reject' in conf:
- require = conf.get('require', {})
- reject = conf.get('reject', {})
- f = model.ChangeishFilter(
- open=require.get('open'),
- current_patchset=require.get('current-patchset'),
- statuses=as_list(require.get('status')),
- required_approvals=as_list(require.get('approval')),
- reject_approvals=as_list(reject.get('approval'))
- )
- manager.changeish_filters.append(f)
+ for source_name, require_config in conf.get('require', {}).items():
+ source = connections.getSource(source_name)
+ manager.changeish_filters.extend(
+ source.getRequireFilters(require_config))
+
+ for source_name, reject_config in conf.get('reject', {}).items():
+ source = connections.getSource(source_name)
+ manager.changeish_filters.extend(
+ source.getRejectFilters(reject_config))
for trigger_name, trigger_config in conf.get('trigger').items():
trigger = connections.getTrigger(trigger_name, trigger_config)
pipeline.triggers.append(trigger)
-
- manager.event_filters += trigger.getEventFilters(
- conf['trigger'][trigger_name])
+ manager.event_filters.extend(
+ trigger.getEventFilters(conf['trigger'][trigger_name]))
return pipeline
diff --git a/zuul/driver/__init__.py b/zuul/driver/__init__.py
index 57b5cf9..671996a 100644
--- a/zuul/driver/__init__.py
+++ b/zuul/driver/__init__.py
@@ -191,6 +191,30 @@
"""
pass
+ @abc.abstractmethod
+ def getRequireSchema(self):
+ """Get the schema for this driver's pipeline requirement filter.
+
+ This method is required by the interface.
+
+ :returns: A voluptuous schema.
+ :rtype: dict or Schema
+
+ """
+ pass
+
+ @abc.abstractmethod
+ def getRejectSchema(self):
+ """Get the schema for this driver's pipeline reject filter.
+
+ This method is required by the interface.
+
+ :returns: A voluptuous schema.
+ :rtype: dict or Schema
+
+ """
+ pass
+
@six.add_metaclass(abc.ABCMeta)
class ReporterInterface(object):
diff --git a/zuul/driver/gerrit/__init__.py b/zuul/driver/gerrit/__init__.py
index a36e912..76ab5b7 100644
--- a/zuul/driver/gerrit/__init__.py
+++ b/zuul/driver/gerrit/__init__.py
@@ -41,3 +41,9 @@
def getReporterSchema(self):
return gerritreporter.getSchema()
+
+ def getRequireSchema(self):
+ return gerritsource.getRequireSchema()
+
+ def getRejectSchema(self):
+ return gerritsource.getRejectSchema()
diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py
index 73979be..275c185 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -26,8 +26,9 @@
import voluptuous as v
from zuul.connection import BaseConnection
-from zuul.model import TriggerEvent, Change, Ref
+from zuul.model import Ref
from zuul import exceptions
+from zuul.driver.gerrit.gerritmodel import GerritChange, GerritTriggerEvent
# Walk the change dependency tree to find a cycle
@@ -72,7 +73,7 @@
# should always be a constant number of seconds behind Gerrit.
now = time.time()
time.sleep(max((ts + self.delay) - now, 0.0))
- event = TriggerEvent()
+ event = GerritTriggerEvent()
event.type = data.get('type')
event.trigger_name = 'gerrit'
change = data.get('change')
@@ -316,7 +317,7 @@
if change and not refresh:
return change
if not change:
- change = Change(None)
+ change = GerritChange(None)
change.number = number
change.patchset = patchset
key = '%s,%s' % (change.number, change.patchset)
diff --git a/zuul/driver/gerrit/gerritmodel.py b/zuul/driver/gerrit/gerritmodel.py
new file mode 100644
index 0000000..009a723
--- /dev/null
+++ b/zuul/driver/gerrit/gerritmodel.py
@@ -0,0 +1,343 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import re
+import time
+
+from zuul.model import EventFilter, RefFilter
+from zuul.model import Change, TriggerEvent
+from zuul.driver.util import time_to_seconds
+
+
+EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
+
+
+def normalize_category(name):
+ name = name.lower()
+ return re.sub(' ', '-', name)
+
+
+class GerritChange(Change):
+ def __init__(self, project):
+ super(GerritChange, self).__init__(project)
+ self.approvals = []
+
+
+class GerritTriggerEvent(TriggerEvent):
+ """Incoming event from an external system."""
+ def __init__(self):
+ super(GerritTriggerEvent, self).__init__()
+ self.approvals = []
+
+ def __repr__(self):
+ ret = '<GerritTriggerEvent %s %s' % (self.type,
+ self.canonical_project_name)
+
+ if self.branch:
+ ret += " %s" % self.branch
+ if self.change_number:
+ ret += " %s,%s" % (self.change_number, self.patch_number)
+ if self.approvals:
+ ret += ' ' + ', '.join(
+ ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
+ ret += '>'
+
+ return ret
+
+ def isPatchsetCreated(self):
+ return 'patchset-created' == self.type
+
+ def isChangeAbandoned(self):
+ return 'change-abandoned' == self.type
+
+
+class GerritApprovalFilter(object):
+ def __init__(self, required_approvals=[], reject_approvals=[]):
+ self._required_approvals = copy.deepcopy(required_approvals)
+ self.required_approvals = self._tidy_approvals(required_approvals)
+ self._reject_approvals = copy.deepcopy(reject_approvals)
+ self.reject_approvals = self._tidy_approvals(reject_approvals)
+
+ def _tidy_approvals(self, approvals):
+ for a in approvals:
+ for k, v in a.items():
+ if k == 'username':
+ a['username'] = re.compile(v)
+ elif k in ['email', 'email-filter']:
+ a['email'] = re.compile(v)
+ elif k == 'newer-than':
+ a[k] = time_to_seconds(v)
+ elif k == 'older-than':
+ a[k] = time_to_seconds(v)
+ if 'email-filter' in a:
+ del a['email-filter']
+ return approvals
+
+ def _match_approval_required_approval(self, rapproval, approval):
+ # Check if the required approval and approval match
+ if 'description' not in approval:
+ return False
+ now = time.time()
+ by = approval.get('by', {})
+ for k, v in rapproval.items():
+ if k == 'username':
+ if (not v.search(by.get('username', ''))):
+ return False
+ elif k == 'email':
+ if (not v.search(by.get('email', ''))):
+ return False
+ elif k == 'newer-than':
+ t = now - v
+ if (approval['grantedOn'] < t):
+ return False
+ elif k == 'older-than':
+ t = now - v
+ if (approval['grantedOn'] >= t):
+ return False
+ else:
+ if not isinstance(v, list):
+ v = [v]
+ if (normalize_category(approval['description']) != k or
+ int(approval['value']) not in v):
+ return False
+ return True
+
+ def matchesApprovals(self, change):
+ if (self.required_approvals and not change.approvals
+ or self.reject_approvals and not change.approvals):
+ # A change with no approvals can not match
+ return False
+
+ # TODO(jhesketh): If we wanted to optimise this slightly we could
+ # analyse both the REQUIRE and REJECT filters by looping over the
+ # approvals on the change and keeping track of what we have checked
+ # rather than needing to loop on the change approvals twice
+ return (self.matchesRequiredApprovals(change) and
+ self.matchesNoRejectApprovals(change))
+
+ def matchesRequiredApprovals(self, change):
+ # Check if any approvals match the requirements
+ for rapproval in self.required_approvals:
+ matches_rapproval = False
+ for approval in change.approvals:
+ if self._match_approval_required_approval(rapproval, approval):
+ # We have a matching approval so this requirement is
+ # fulfilled
+ matches_rapproval = True
+ break
+ if not matches_rapproval:
+ return False
+ return True
+
+ def matchesNoRejectApprovals(self, change):
+ # Check to make sure no approvals match a reject criteria
+ for rapproval in self.reject_approvals:
+ for approval in change.approvals:
+ if self._match_approval_required_approval(rapproval, approval):
+ # A reject approval has been matched, so we reject
+ # immediately
+ return False
+ # To get here no rejects can have been matched so we should be good to
+ # queue
+ return True
+
+
+class GerritEventFilter(EventFilter, GerritApprovalFilter):
+ def __init__(self, trigger, types=[], branches=[], refs=[],
+ event_approvals={}, comments=[], emails=[], usernames=[],
+ required_approvals=[], reject_approvals=[],
+ ignore_deletes=True):
+
+ EventFilter.__init__(self, trigger)
+
+ GerritApprovalFilter.__init__(self,
+ required_approvals=required_approvals,
+ reject_approvals=reject_approvals)
+
+ self._types = types
+ self._branches = branches
+ self._refs = refs
+ self._comments = comments
+ self._emails = emails
+ self._usernames = usernames
+ self.types = [re.compile(x) for x in types]
+ self.branches = [re.compile(x) for x in branches]
+ self.refs = [re.compile(x) for x in refs]
+ self.comments = [re.compile(x) for x in comments]
+ self.emails = [re.compile(x) for x in emails]
+ self.usernames = [re.compile(x) for x in usernames]
+ self.event_approvals = event_approvals
+ self.ignore_deletes = ignore_deletes
+
+ def __repr__(self):
+ ret = '<GerritEventFilter'
+
+ if self._types:
+ ret += ' types: %s' % ', '.join(self._types)
+ if self._branches:
+ ret += ' branches: %s' % ', '.join(self._branches)
+ if self._refs:
+ ret += ' refs: %s' % ', '.join(self._refs)
+ if self.ignore_deletes:
+ ret += ' ignore_deletes: %s' % self.ignore_deletes
+ if self.event_approvals:
+ ret += ' event_approvals: %s' % ', '.join(
+ ['%s:%s' % a for a in self.event_approvals.items()])
+ if self.required_approvals:
+ ret += ' required_approvals: %s' % ', '.join(
+ ['%s' % a for a in self._required_approvals])
+ if self.reject_approvals:
+ ret += ' reject_approvals: %s' % ', '.join(
+ ['%s' % a for a in self._reject_approvals])
+ if self._comments:
+ ret += ' comments: %s' % ', '.join(self._comments)
+ if self._emails:
+ ret += ' emails: %s' % ', '.join(self._emails)
+ if self._usernames:
+ ret += ' usernames: %s' % ', '.join(self._usernames)
+ ret += '>'
+
+ return ret
+
+ def matches(self, event, change):
+ # event types are ORed
+ matches_type = False
+ for etype in self.types:
+ if etype.match(event.type):
+ matches_type = True
+ if self.types and not matches_type:
+ return False
+
+ # branches are ORed
+ matches_branch = False
+ for branch in self.branches:
+ if branch.match(event.branch):
+ matches_branch = True
+ if self.branches and not matches_branch:
+ return False
+
+ # refs are ORed
+ matches_ref = False
+ if event.ref is not None:
+ for ref in self.refs:
+ if ref.match(event.ref):
+ matches_ref = True
+ if self.refs and not matches_ref:
+ return False
+ if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
+ # If the updated ref has an empty git sha (all 0s),
+ # then the ref is being deleted
+ return False
+
+ # comments are ORed
+ matches_comment_re = False
+ for comment_re in self.comments:
+ if (event.comment is not None and
+ comment_re.search(event.comment)):
+ matches_comment_re = True
+ if self.comments and not matches_comment_re:
+ return False
+
+ # We better have an account provided by Gerrit to do
+ # email filtering.
+ if event.account is not None:
+ account_email = event.account.get('email')
+ # emails are ORed
+ matches_email_re = False
+ for email_re in self.emails:
+ if (account_email is not None and
+ email_re.search(account_email)):
+ matches_email_re = True
+ if self.emails and not matches_email_re:
+ return False
+
+ # usernames are ORed
+ account_username = event.account.get('username')
+ matches_username_re = False
+ for username_re in self.usernames:
+ if (account_username is not None and
+ username_re.search(account_username)):
+ matches_username_re = True
+ if self.usernames and not matches_username_re:
+ return False
+
+ # approvals are ANDed
+ for category, value in self.event_approvals.items():
+ matches_approval = False
+ for eapp in event.approvals:
+ if (normalize_category(eapp['description']) == category and
+ int(eapp['value']) == int(value)):
+ matches_approval = True
+ if not matches_approval:
+ return False
+
+ # required approvals are ANDed (reject approvals are ORed)
+ if not self.matchesApprovals(change):
+ return False
+
+ return True
+
+
+class GerritRefFilter(RefFilter, GerritApprovalFilter):
+ def __init__(self, open=None, current_patchset=None,
+ statuses=[], required_approvals=[],
+ reject_approvals=[]):
+ RefFilter.__init__(self)
+
+ GerritApprovalFilter.__init__(self,
+ required_approvals=required_approvals,
+ reject_approvals=reject_approvals)
+
+ self.open = open
+ self.current_patchset = current_patchset
+ self.statuses = statuses
+
+ def __repr__(self):
+ ret = '<GerritRefFilter'
+
+ if self.open is not None:
+ ret += ' open: %s' % self.open
+ if self.current_patchset is not None:
+ ret += ' current-patchset: %s' % self.current_patchset
+ if self.statuses:
+ ret += ' statuses: %s' % ', '.join(self.statuses)
+ if self.required_approvals:
+ ret += (' required-approvals: %s' %
+ str(self.required_approvals))
+ if self.reject_approvals:
+ ret += (' reject-approvals: %s' %
+ str(self.reject_approvals))
+ ret += '>'
+
+ return ret
+
+ def matches(self, change):
+ if self.open is not None:
+ if self.open != change.open:
+ return False
+
+ if self.current_patchset is not None:
+ if self.current_patchset != change.is_current_patchset:
+ return False
+
+ if self.statuses:
+ if change.status not in self.statuses:
+ return False
+
+ # required approvals are ANDed (reject approvals are ORed)
+ if not self.matchesApprovals(change):
+ return False
+
+ return True
diff --git a/zuul/driver/gerrit/gerritsource.py b/zuul/driver/gerrit/gerritsource.py
index e6230df..6cb0c39 100644
--- a/zuul/driver/gerrit/gerritsource.py
+++ b/zuul/driver/gerrit/gerritsource.py
@@ -13,8 +13,11 @@
# under the License.
import logging
+import voluptuous as vs
from zuul.source import BaseSource
from zuul.model import Project
+from zuul.driver.gerrit.gerritmodel import GerritRefFilter
+from zuul.driver.util import scalar_or_list, to_list
class GerritSource(BaseSource):
@@ -59,3 +62,41 @@
def _getGitwebUrl(self, project, sha=None):
return self.connection._getGitwebUrl(project, sha)
+
+ def getRequireFilters(self, config):
+ f = GerritRefFilter(
+ open=config.get('open'),
+ current_patchset=config.get('current-patchset'),
+ statuses=to_list(config.get('status')),
+ required_approvals=to_list(config.get('approval')),
+ )
+ return [f]
+
+ def getRejectFilters(self, config):
+ f = GerritRefFilter(
+ reject_approvals=to_list(config.get('approval')),
+ )
+ return [f]
+
+
+approval = vs.Schema({'username': str,
+ 'email-filter': str,
+ 'email': str,
+ 'older-than': str,
+ 'newer-than': str,
+ }, extra=vs.ALLOW_EXTRA)
+
+
+def getRequireSchema():
+ require = {'approval': scalar_or_list(approval),
+ 'open': bool,
+ 'current-patchset': bool,
+ 'status': scalar_or_list(str)}
+
+ return require
+
+
+def getRejectSchema():
+ reject = {'approval': scalar_or_list(approval)}
+
+ return reject
diff --git a/zuul/driver/gerrit/gerrittrigger.py b/zuul/driver/gerrit/gerrittrigger.py
index 70c65fd..706b7df 100644
--- a/zuul/driver/gerrit/gerrittrigger.py
+++ b/zuul/driver/gerrit/gerrittrigger.py
@@ -14,8 +14,9 @@
import logging
import voluptuous as v
-from zuul.model import EventFilter
from zuul.trigger import BaseTrigger
+from zuul.driver.gerrit.gerritmodel import GerritEventFilter
+from zuul.driver.util import scalar_or_list, to_list
class GerritTrigger(BaseTrigger):
@@ -23,43 +24,36 @@
log = logging.getLogger("zuul.GerritTrigger")
def getEventFilters(self, trigger_conf):
- def toList(item):
- if not item:
- return []
- if isinstance(item, list):
- return item
- return [item]
-
efilters = []
- for trigger in toList(trigger_conf):
+ for trigger in to_list(trigger_conf):
approvals = {}
- for approval_dict in toList(trigger.get('approval')):
+ for approval_dict in to_list(trigger.get('approval')):
for key, val in approval_dict.items():
approvals[key] = val
# Backwards compat for *_filter versions of these args
- comments = toList(trigger.get('comment'))
+ comments = to_list(trigger.get('comment'))
if not comments:
- comments = toList(trigger.get('comment_filter'))
- emails = toList(trigger.get('email'))
+ comments = to_list(trigger.get('comment_filter'))
+ emails = to_list(trigger.get('email'))
if not emails:
- emails = toList(trigger.get('email_filter'))
- usernames = toList(trigger.get('username'))
+ emails = to_list(trigger.get('email_filter'))
+ usernames = to_list(trigger.get('username'))
if not usernames:
- usernames = toList(trigger.get('username_filter'))
+ usernames = to_list(trigger.get('username_filter'))
ignore_deletes = trigger.get('ignore-deletes', True)
- f = EventFilter(
+ f = GerritEventFilter(
trigger=self,
- types=toList(trigger['event']),
- branches=toList(trigger.get('branch')),
- refs=toList(trigger.get('ref')),
+ types=to_list(trigger['event']),
+ branches=to_list(trigger.get('branch')),
+ refs=to_list(trigger.get('ref')),
event_approvals=approvals,
comments=comments,
emails=emails,
usernames=usernames,
required_approvals=(
- toList(trigger.get('require-approval'))
+ to_list(trigger.get('require-approval'))
),
- reject_approvals=toList(
+ reject_approvals=to_list(
trigger.get('reject-approval')
),
ignore_deletes=ignore_deletes
@@ -80,8 +74,6 @@
def getSchema():
- def toList(x):
- return v.Any([x], x)
variable_dict = v.Schema(dict)
approval = v.Schema({'username': str,
@@ -93,25 +85,25 @@
gerrit_trigger = {
v.Required('event'):
- toList(v.Any('patchset-created',
- 'draft-published',
- 'change-abandoned',
- 'change-restored',
- 'change-merged',
- 'comment-added',
- 'ref-updated')),
- 'comment_filter': toList(str),
- 'comment': toList(str),
- 'email_filter': toList(str),
- 'email': toList(str),
- 'username_filter': toList(str),
- 'username': toList(str),
- 'branch': toList(str),
- 'ref': toList(str),
+ scalar_or_list(v.Any('patchset-created',
+ 'draft-published',
+ 'change-abandoned',
+ 'change-restored',
+ 'change-merged',
+ 'comment-added',
+ 'ref-updated')),
+ 'comment_filter': scalar_or_list(str),
+ 'comment': scalar_or_list(str),
+ 'email_filter': scalar_or_list(str),
+ 'email': scalar_or_list(str),
+ 'username_filter': scalar_or_list(str),
+ 'username': scalar_or_list(str),
+ 'branch': scalar_or_list(str),
+ 'ref': scalar_or_list(str),
'ignore-deletes': bool,
- 'approval': toList(variable_dict),
- 'require-approval': toList(approval),
- 'reject-approval': toList(approval),
+ 'approval': scalar_or_list(variable_dict),
+ 'require-approval': scalar_or_list(approval),
+ 'reject-approval': scalar_or_list(approval),
}
return gerrit_trigger
diff --git a/zuul/driver/git/__init__.py b/zuul/driver/git/__init__.py
index 5ebedac..0faa036 100644
--- a/zuul/driver/git/__init__.py
+++ b/zuul/driver/git/__init__.py
@@ -25,3 +25,9 @@
def getSource(self, connection):
return gitsource.GitSource(self, connection)
+
+ def getRequireSchema(self):
+ return {}
+
+ def getRejectSchema(self):
+ return {}
diff --git a/zuul/driver/git/gitsource.py b/zuul/driver/git/gitsource.py
index 485a6e4..61a328e 100644
--- a/zuul/driver/git/gitsource.py
+++ b/zuul/driver/git/gitsource.py
@@ -53,3 +53,9 @@
def getProjectOpenChanges(self, project):
raise NotImplemented()
+
+ def getRequireFilters(self, config):
+ return []
+
+ def getRejectFilters(self, config):
+ return []
diff --git a/zuul/driver/github/__init__.py b/zuul/driver/github/__init__.py
index e59dc58..f75e907 100644
--- a/zuul/driver/github/__init__.py
+++ b/zuul/driver/github/__init__.py
@@ -41,3 +41,9 @@
def getReporterSchema(self):
return githubreporter.getSchema()
+
+ def getRequireSchema(self):
+ return githubsource.getRequireSchema()
+
+ def getRejectSchema(self):
+ return githubsource.getRejectSchema()
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index b7fb05d..4b945a5 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -25,8 +25,9 @@
from github3.exceptions import MethodNotAllowed
from zuul.connection import BaseConnection
-from zuul.model import PullRequest, Ref, GithubTriggerEvent
+from zuul.model import Ref
from zuul.exceptions import MergeFailure
+from zuul.driver.github.githubmodel import PullRequest, GithubTriggerEvent
class GithubWebhookListener():
diff --git a/zuul/driver/github/githubmodel.py b/zuul/driver/github/githubmodel.py
new file mode 100644
index 0000000..0d77cae
--- /dev/null
+++ b/zuul/driver/github/githubmodel.py
@@ -0,0 +1,163 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+# Copyright 2017 IBM Corp.
+# Copyright 2017 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import re
+
+from zuul.model import Change, TriggerEvent, EventFilter
+
+
+EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
+
+
+class PullRequest(Change):
+ def __init__(self, project):
+ super(PullRequest, self).__init__(project)
+ self.updated_at = None
+ self.title = None
+
+ def isUpdateOf(self, other):
+ if (hasattr(other, 'number') and self.number == other.number and
+ hasattr(other, 'patchset') and self.patchset != other.patchset and
+ hasattr(other, 'updated_at') and
+ self.updated_at > other.updated_at):
+ return True
+ return False
+
+
+class GithubTriggerEvent(TriggerEvent):
+ def __init__(self):
+ super(GithubTriggerEvent, self).__init__()
+ self.title = None
+ self.label = None
+ self.unlabel = None
+
+ def isPatchsetCreated(self):
+ if self.type == 'pull_request':
+ return self.action in ['opened', 'changed']
+ return False
+
+ def isChangeAbandoned(self):
+ if self.type == 'pull_request':
+ return 'closed' == self.action
+ return False
+
+
+class GithubEventFilter(EventFilter):
+ def __init__(self, trigger, types=[], branches=[], refs=[],
+ comments=[], actions=[], labels=[], unlabels=[],
+ states=[], ignore_deletes=True):
+
+ EventFilter.__init__(self, trigger)
+
+ self._types = types
+ self._branches = branches
+ self._refs = refs
+ self._comments = comments
+ self.types = [re.compile(x) for x in types]
+ self.branches = [re.compile(x) for x in branches]
+ self.refs = [re.compile(x) for x in refs]
+ self.comments = [re.compile(x) for x in comments]
+ self.actions = actions
+ self.labels = labels
+ self.unlabels = unlabels
+ self.states = states
+ self.ignore_deletes = ignore_deletes
+
+ def __repr__(self):
+ ret = '<GithubEventFilter'
+
+ if self._types:
+ ret += ' types: %s' % ', '.join(self._types)
+ if self._branches:
+ ret += ' branches: %s' % ', '.join(self._branches)
+ if self._refs:
+ ret += ' refs: %s' % ', '.join(self._refs)
+ if self.ignore_deletes:
+ ret += ' ignore_deletes: %s' % self.ignore_deletes
+ if self._comments:
+ ret += ' comments: %s' % ', '.join(self._comments)
+ if self.actions:
+ ret += ' actions: %s' % ', '.join(self.actions)
+ if self.labels:
+ ret += ' labels: %s' % ', '.join(self.labels)
+ if self.unlabels:
+ ret += ' unlabels: %s' % ', '.join(self.unlabels)
+ if self.states:
+ ret += ' states: %s' % ', '.join(self.states)
+ ret += '>'
+
+ return ret
+
+ def matches(self, event, change):
+ # event types are ORed
+ matches_type = False
+ for etype in self.types:
+ if etype.match(event.type):
+ matches_type = True
+ if self.types and not matches_type:
+ return False
+
+ # branches are ORed
+ matches_branch = False
+ for branch in self.branches:
+ if branch.match(event.branch):
+ matches_branch = True
+ if self.branches and not matches_branch:
+ return False
+
+ # refs are ORed
+ matches_ref = False
+ if event.ref is not None:
+ for ref in self.refs:
+ if ref.match(event.ref):
+ matches_ref = True
+ if self.refs and not matches_ref:
+ return False
+ if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
+ # If the updated ref has an empty git sha (all 0s),
+ # then the ref is being deleted
+ return False
+
+ # comments are ORed
+ matches_comment_re = False
+ for comment_re in self.comments:
+ if (event.comment is not None and
+ comment_re.search(event.comment)):
+ matches_comment_re = True
+ if self.comments and not matches_comment_re:
+ return False
+
+ # actions are ORed
+ matches_action = False
+ for action in self.actions:
+ if (event.action == action):
+ matches_action = True
+ if self.actions and not matches_action:
+ return False
+
+ # labels are ORed
+ if self.labels and event.label not in self.labels:
+ return False
+
+ # unlabels are ORed
+ if self.unlabels and event.unlabel not in self.unlabels:
+ return False
+
+ # states are ORed
+ if self.states and event.state not in self.states:
+ return False
+
+ return True
diff --git a/zuul/driver/github/githubsource.py b/zuul/driver/github/githubsource.py
index 312ee87..e7d19ac 100644
--- a/zuul/driver/github/githubsource.py
+++ b/zuul/driver/github/githubsource.py
@@ -90,3 +90,17 @@
def _ghTimestampToDate(self, timestamp):
return time.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
+
+ def getRequireFilters(self, config):
+ return []
+
+ def getRejectFilters(self, config):
+ return []
+
+
+def getRequireSchema():
+ return {}
+
+
+def getRejectSchema():
+ return {}
diff --git a/zuul/driver/github/githubtrigger.py b/zuul/driver/github/githubtrigger.py
index b9c1026..f0bd2f4 100644
--- a/zuul/driver/github/githubtrigger.py
+++ b/zuul/driver/github/githubtrigger.py
@@ -14,8 +14,8 @@
import logging
import voluptuous as v
-from zuul.model import EventFilter
from zuul.trigger import BaseTrigger
+from zuul.driver.github.githubmodel import GithubEventFilter
class GithubTrigger(BaseTrigger):
@@ -32,7 +32,7 @@
efilters = []
for trigger in toList(trigger_config):
- f = EventFilter(
+ f = GithubEventFilter(
trigger=self,
types=toList(trigger['event']),
actions=toList(trigger.get('action')),
diff --git a/zuul/driver/timer/__init__.py b/zuul/driver/timer/__init__.py
index bca91a1..cdaea74 100644
--- a/zuul/driver/timer/__init__.py
+++ b/zuul/driver/timer/__init__.py
@@ -20,8 +20,8 @@
from apscheduler.triggers.cron import CronTrigger
from zuul.driver import Driver, TriggerInterface
-from zuul.model import TriggerEvent
from zuul.driver.timer import timertrigger
+from zuul.driver.timer.timermodel import TimerTriggerEvent
class TimerDriver(Driver, TriggerInterface):
@@ -81,7 +81,7 @@
def _onTrigger(self, tenant, pipeline_name, timespec):
for project_name in tenant.layout.project_configs.keys():
project_hostname, project_name = project_name.split('/', 1)
- event = TriggerEvent()
+ event = TimerTriggerEvent()
event.type = 'timer'
event.timespec = timespec
event.forced_pipeline = pipeline_name
diff --git a/zuul/driver/timer/timermodel.py b/zuul/driver/timer/timermodel.py
new file mode 100644
index 0000000..d6f1415
--- /dev/null
+++ b/zuul/driver/timer/timermodel.py
@@ -0,0 +1,62 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import re
+
+from zuul.model import EventFilter, TriggerEvent
+
+
+class TimerEventFilter(EventFilter):
+ def __init__(self, trigger, types=[], timespecs=[]):
+ EventFilter.__init__(self, trigger)
+
+ self._types = types
+ self.types = [re.compile(x) for x in types]
+ self.timespecs = timespecs
+
+ def __repr__(self):
+ ret = '<TimerEventFilter'
+
+ if self._types:
+ ret += ' types: %s' % ', '.join(self._types)
+ if self.timespecs:
+ ret += ' timespecs: %s' % ', '.join(self.timespecs)
+ ret += '>'
+
+ return ret
+
+ def matches(self, event, change):
+ # event types are ORed
+ matches_type = False
+ for etype in self.types:
+ if etype.match(event.type):
+ matches_type = True
+ if self.types and not matches_type:
+ return False
+
+ # timespecs are ORed
+ matches_timespec = False
+ for timespec in self.timespecs:
+ if (event.timespec == timespec):
+ matches_timespec = True
+ if self.timespecs and not matches_timespec:
+ return False
+
+ return True
+
+
+class TimerTriggerEvent(TriggerEvent):
+ def __init__(self):
+ super(TimerTriggerEvent, self).__init__()
+ self.timespec = None
diff --git a/zuul/driver/timer/timertrigger.py b/zuul/driver/timer/timertrigger.py
index b0f282c..81b41a1 100644
--- a/zuul/driver/timer/timertrigger.py
+++ b/zuul/driver/timer/timertrigger.py
@@ -15,26 +15,20 @@
import voluptuous as v
-from zuul.model import EventFilter
from zuul.trigger import BaseTrigger
+from zuul.driver.timer.timermodel import TimerEventFilter
+from zuul.driver.util import to_list
class TimerTrigger(BaseTrigger):
name = 'timer'
def getEventFilters(self, trigger_conf):
- def toList(item):
- if not item:
- return []
- if isinstance(item, list):
- return item
- return [item]
-
efilters = []
- for trigger in toList(trigger_conf):
- f = EventFilter(trigger=self,
- types=['timer'],
- timespecs=toList(trigger['time']))
+ for trigger in to_list(trigger_conf):
+ f = TimerEventFilter(trigger=self,
+ types=['timer'],
+ timespecs=to_list(trigger['time']))
efilters.append(f)
diff --git a/zuul/driver/util.py b/zuul/driver/util.py
new file mode 100644
index 0000000..902ce76
--- /dev/null
+++ b/zuul/driver/util.py
@@ -0,0 +1,43 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# Utility methods to promote consistent configuration among drivers.
+
+import voluptuous as vs
+
+
+def time_to_seconds(s):
+ if s.endswith('s'):
+ return int(s[:-1])
+ if s.endswith('m'):
+ return int(s[:-1]) * 60
+ if s.endswith('h'):
+ return int(s[:-1]) * 60 * 60
+ if s.endswith('d'):
+ return int(s[:-1]) * 24 * 60 * 60
+ if s.endswith('w'):
+ return int(s[:-1]) * 7 * 24 * 60 * 60
+ raise Exception("Unable to parse time value: %s" % s)
+
+
+def scalar_or_list(x):
+ return vs.Any([x], x)
+
+
+def to_list(item):
+ if not item:
+ return []
+ if isinstance(item, list):
+ return item
+ return [item]
diff --git a/zuul/driver/zuul/__init__.py b/zuul/driver/zuul/__init__.py
index 4c3be3d..08612dc 100644
--- a/zuul/driver/zuul/__init__.py
+++ b/zuul/driver/zuul/__init__.py
@@ -15,7 +15,7 @@
import logging
from zuul.driver import Driver, TriggerInterface
-from zuul.model import TriggerEvent
+from zuul.driver.zuul.zuulmodel import ZuulTriggerEvent
from zuul.driver.zuul import zuultrigger
@@ -73,7 +73,7 @@
self._createProjectChangeMergedEvent(open_change)
def _createProjectChangeMergedEvent(self, change):
- event = TriggerEvent()
+ event = ZuulTriggerEvent()
event.type = PROJECT_CHANGE_MERGED
event.trigger_name = self.name
event.project_hostname = change.project.canonical_hostname
@@ -94,7 +94,7 @@
self._createParentChangeEnqueuedEvent(needs, pipeline)
def _createParentChangeEnqueuedEvent(self, change, pipeline):
- event = TriggerEvent()
+ event = ZuulTriggerEvent()
event.type = PARENT_CHANGE_ENQUEUED
event.trigger_name = self.name
event.pipeline_name = pipeline.name
diff --git a/zuul/driver/zuul/zuulmodel.py b/zuul/driver/zuul/zuulmodel.py
new file mode 100644
index 0000000..036f6d2
--- /dev/null
+++ b/zuul/driver/zuul/zuulmodel.py
@@ -0,0 +1,63 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import re
+
+from zuul.model import EventFilter, TriggerEvent
+
+
+class ZuulEventFilter(EventFilter):
+ def __init__(self, trigger, types=[], pipelines=[]):
+ EventFilter.__init__(self, trigger)
+
+ self._types = types
+ self._pipelines = pipelines
+ self.types = [re.compile(x) for x in types]
+ self.pipelines = [re.compile(x) for x in pipelines]
+
+ def __repr__(self):
+ ret = '<ZuulEventFilter'
+
+ if self._types:
+ ret += ' types: %s' % ', '.join(self._types)
+ if self._pipelines:
+ ret += ' pipelines: %s' % ', '.join(self._pipelines)
+ ret += '>'
+
+ return ret
+
+ def matches(self, event, change):
+ # event types are ORed
+ matches_type = False
+ for etype in self.types:
+ if etype.match(event.type):
+ matches_type = True
+ if self.types and not matches_type:
+ return False
+
+ # pipelines are ORed
+ matches_pipeline = False
+ for epipe in self.pipelines:
+ if epipe.match(event.pipeline_name):
+ matches_pipeline = True
+ if self.pipelines and not matches_pipeline:
+ return False
+
+ return True
+
+
+class ZuulTriggerEvent(TriggerEvent):
+ def __init__(self):
+ super(ZuulTriggerEvent, self).__init__()
+ self.pipeline_name = None
diff --git a/zuul/driver/zuul/zuultrigger.py b/zuul/driver/zuul/zuultrigger.py
index c0c2fb3..628687e 100644
--- a/zuul/driver/zuul/zuultrigger.py
+++ b/zuul/driver/zuul/zuultrigger.py
@@ -15,8 +15,9 @@
import logging
import voluptuous as v
-from zuul.model import EventFilter
from zuul.trigger import BaseTrigger
+from zuul.driver.zuul.zuulmodel import ZuulEventFilter
+from zuul.driver.util import scalar_or_list, to_list
class ZuulTrigger(BaseTrigger):
@@ -29,25 +30,12 @@
self._handle_project_change_merged_events = False
def getEventFilters(self, trigger_conf):
- def toList(item):
- if not item:
- return []
- if isinstance(item, list):
- return item
- return [item]
-
efilters = []
- for trigger in toList(trigger_conf):
- f = EventFilter(
+ for trigger in to_list(trigger_conf):
+ f = ZuulEventFilter(
trigger=self,
- types=toList(trigger['event']),
- pipelines=toList(trigger.get('pipeline')),
- required_approvals=(
- toList(trigger.get('require-approval'))
- ),
- reject_approvals=toList(
- trigger.get('reject-approval')
- ),
+ types=to_list(trigger['event']),
+ pipelines=to_list(trigger.get('pipeline')),
)
efilters.append(f)
@@ -55,9 +43,6 @@
def getSchema():
- def toList(x):
- return v.Any([x], x)
-
approval = v.Schema({'username': str,
'email-filter': str,
'email': str,
@@ -67,11 +52,11 @@
zuul_trigger = {
v.Required('event'):
- toList(v.Any('parent-change-enqueued',
- 'project-change-merged')),
- 'pipeline': toList(str),
- 'require-approval': toList(approval),
- 'reject-approval': toList(approval),
+ scalar_or_list(v.Any('parent-change-enqueued',
+ 'project-change-merged')),
+ 'pipeline': scalar_or_list(str),
+ 'require-approval': scalar_or_list(approval),
+ 'reject-approval': scalar_or_list(approval),
}
return zuul_trigger
diff --git a/zuul/model.py b/zuul/model.py
index 7f6223b..4ae6f9a 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -16,7 +16,6 @@
import copy
import logging
import os
-import re
import struct
import time
from uuid import uuid4
@@ -28,8 +27,6 @@
'ordereddict.OrderedDict'])
-EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
-
MERGER_MERGE = 1 # "git merge"
MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
MERGER_CHERRY_PICK = 3 # "git cherry-pick"
@@ -78,25 +75,6 @@
STATE_DELETING])
-def time_to_seconds(s):
- if s.endswith('s'):
- return int(s[:-1])
- if s.endswith('m'):
- return int(s[:-1]) * 60
- if s.endswith('h'):
- return int(s[:-1]) * 60 * 60
- if s.endswith('d'):
- return int(s[:-1]) * 24 * 60 * 60
- if s.endswith('w'):
- return int(s[:-1]) * 7 * 24 * 60 * 60
- raise Exception("Unable to parse time value: %s" % s)
-
-
-def normalizeCategory(name):
- name = name.lower()
- return re.sub(' ', '-', name)
-
-
class Attributes(object):
"""A class to hold attributes for string formatting."""
@@ -1810,7 +1788,6 @@
self.can_merge = False
self.is_merged = False
self.failed_to_merge = False
- self.approvals = []
self.open = None
self.status = None
self.owner = None
@@ -1863,24 +1840,10 @@
patchset=self.patchset)
-class PullRequest(Change):
- def __init__(self, project):
- super(PullRequest, self).__init__(project)
- self.updated_at = None
- self.title = None
-
- def isUpdateOf(self, other):
- if (hasattr(other, 'number') and self.number == other.number and
- hasattr(other, 'patchset') and self.patchset != other.patchset and
- hasattr(other, 'updated_at') and
- self.updated_at > other.updated_at):
- return True
- return False
-
-
class TriggerEvent(object):
"""Incoming event from an external system."""
def __init__(self):
+ # TODO(jeblair): further reduce this list
self.data = None
# common
self.type = None
@@ -1896,20 +1859,13 @@
self.change_url = None
self.patch_number = None
self.refspec = None
- self.approvals = []
self.branch = None
self.comment = None
- self.label = None
- self.unlabel = None
self.state = None
# ref-updated
self.ref = None
self.oldrev = None
self.newrev = None
- # timer
- self.timespec = None
- # zuultrigger
- self.pipeline_name = None
# For events that arrive with a destination pipeline (eg, from
# an admin command, etc):
self.forced_pipeline = None
@@ -1918,374 +1874,35 @@
def canonical_project_name(self):
return self.project_hostname + '/' + self.project_name
- def __repr__(self):
- ret = '<TriggerEvent %s %s' % (self.type, self.canonical_project_name)
-
- if self.branch:
- ret += " %s" % self.branch
- if self.change_number:
- ret += " %s,%s" % (self.change_number, self.patch_number)
- if self.approvals:
- ret += ' ' + ', '.join(
- ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
- ret += '>'
-
- return ret
-
def isPatchsetCreated(self):
- return 'patchset-created' == self.type
-
- def isChangeAbandoned(self):
- return 'change-abandoned' == self.type
-
-
-class GithubTriggerEvent(TriggerEvent):
-
- def __init__(self):
- super(GithubTriggerEvent, self).__init__()
- self.title = None
-
- def isPatchsetCreated(self):
- if self.type == 'pull_request':
- return self.action in ['opened', 'changed']
return False
def isChangeAbandoned(self):
- if self.type == 'pull_request':
- return 'closed' == self.action
return False
class BaseFilter(object):
"""Base Class for filtering which Changes and Events to process."""
- def __init__(self, required_approvals=[], reject_approvals=[]):
- self._required_approvals = copy.deepcopy(required_approvals)
- self.required_approvals = self._tidy_approvals(required_approvals)
- self._reject_approvals = copy.deepcopy(reject_approvals)
- self.reject_approvals = self._tidy_approvals(reject_approvals)
-
- def _tidy_approvals(self, approvals):
- for a in approvals:
- for k, v in a.items():
- if k == 'username':
- a['username'] = re.compile(v)
- elif k in ['email', 'email-filter']:
- a['email'] = re.compile(v)
- elif k == 'newer-than':
- a[k] = time_to_seconds(v)
- elif k == 'older-than':
- a[k] = time_to_seconds(v)
- if 'email-filter' in a:
- del a['email-filter']
- return approvals
-
- def _match_approval_required_approval(self, rapproval, approval):
- # Check if the required approval and approval match
- if 'description' not in approval:
- return False
- now = time.time()
- by = approval.get('by', {})
- for k, v in rapproval.items():
- if k == 'username':
- if (not v.search(by.get('username', ''))):
- return False
- elif k == 'email':
- if (not v.search(by.get('email', ''))):
- return False
- elif k == 'newer-than':
- t = now - v
- if (approval['grantedOn'] < t):
- return False
- elif k == 'older-than':
- t = now - v
- if (approval['grantedOn'] >= t):
- return False
- else:
- if not isinstance(v, list):
- v = [v]
- if (normalizeCategory(approval['description']) != k or
- int(approval['value']) not in v):
- return False
- return True
-
- def matchesApprovals(self, change):
- if (self.required_approvals and not change.approvals
- or self.reject_approvals and not change.approvals):
- # A change with no approvals can not match
- return False
-
- # TODO(jhesketh): If we wanted to optimise this slightly we could
- # analyse both the REQUIRE and REJECT filters by looping over the
- # approvals on the change and keeping track of what we have checked
- # rather than needing to loop on the change approvals twice
- return (self.matchesRequiredApprovals(change) and
- self.matchesNoRejectApprovals(change))
-
- def matchesRequiredApprovals(self, change):
- # Check if any approvals match the requirements
- for rapproval in self.required_approvals:
- matches_rapproval = False
- for approval in change.approvals:
- if self._match_approval_required_approval(rapproval, approval):
- # We have a matching approval so this requirement is
- # fulfilled
- matches_rapproval = True
- break
- if not matches_rapproval:
- return False
- return True
-
- def matchesNoRejectApprovals(self, change):
- # Check to make sure no approvals match a reject criteria
- for rapproval in self.reject_approvals:
- for approval in change.approvals:
- if self._match_approval_required_approval(rapproval, approval):
- # A reject approval has been matched, so we reject
- # immediately
- return False
- # To get here no rejects can have been matched so we should be good to
- # queue
- return True
+ pass
class EventFilter(BaseFilter):
"""Allows a Pipeline to only respond to certain events."""
- def __init__(self, trigger, types=[], branches=[], refs=[],
- event_approvals={}, comments=[], emails=[], usernames=[],
- timespecs=[], required_approvals=[], reject_approvals=[],
- pipelines=[], actions=[], labels=[], unlabels=[], states=[],
- ignore_deletes=True):
- super(EventFilter, self).__init__(
- required_approvals=required_approvals,
- reject_approvals=reject_approvals)
+ def __init__(self, trigger):
+ super(EventFilter, self).__init__()
self.trigger = trigger
- self._types = types
- self._branches = branches
- self._refs = refs
- self._comments = comments
- self._emails = emails
- self._usernames = usernames
- self._pipelines = pipelines
- self.types = [re.compile(x) for x in types]
- self.branches = [re.compile(x) for x in branches]
- self.refs = [re.compile(x) for x in refs]
- self.comments = [re.compile(x) for x in comments]
- self.emails = [re.compile(x) for x in emails]
- self.usernames = [re.compile(x) for x in usernames]
- self.pipelines = [re.compile(x) for x in pipelines]
- self.actions = actions
- self.event_approvals = event_approvals
- self.timespecs = timespecs
- self.labels = labels
- self.unlabels = unlabels
- self.states = states
- self.ignore_deletes = ignore_deletes
- def __repr__(self):
- ret = '<EventFilter'
-
- if self._types:
- ret += ' types: %s' % ', '.join(self._types)
- if self._pipelines:
- ret += ' pipelines: %s' % ', '.join(self._pipelines)
- if self._branches:
- ret += ' branches: %s' % ', '.join(self._branches)
- if self._refs:
- ret += ' refs: %s' % ', '.join(self._refs)
- if self.ignore_deletes:
- ret += ' ignore_deletes: %s' % self.ignore_deletes
- if self.event_approvals:
- ret += ' event_approvals: %s' % ', '.join(
- ['%s:%s' % a for a in self.event_approvals.items()])
- if self.required_approvals:
- ret += ' required_approvals: %s' % ', '.join(
- ['%s' % a for a in self._required_approvals])
- if self.reject_approvals:
- ret += ' reject_approvals: %s' % ', '.join(
- ['%s' % a for a in self._reject_approvals])
- if self._comments:
- ret += ' comments: %s' % ', '.join(self._comments)
- if self._emails:
- ret += ' emails: %s' % ', '.join(self._emails)
- if self._usernames:
- ret += ' username_filters: %s' % ', '.join(self._usernames)
- if self.timespecs:
- ret += ' timespecs: %s' % ', '.join(self.timespecs)
- if self.actions:
- ret += ' actions: %s' % ', '.join(self.actions)
- if self.labels:
- ret += ' labels: %s' % ', '.join(self.labels)
- if self.unlabels:
- ret += ' unlabels: %s' % ', '.join(self.unlabels)
- if self.states:
- ret += ' states: %s' % ', '.join(self.states)
- ret += '>'
-
- return ret
-
- def matches(self, event, change):
- # event types are ORed
- matches_type = False
- for etype in self.types:
- if etype.match(event.type):
- matches_type = True
- if self.types and not matches_type:
- return False
-
- # pipelines are ORed
- matches_pipeline = False
- for epipe in self.pipelines:
- if epipe.match(event.pipeline_name):
- matches_pipeline = True
- if self.pipelines and not matches_pipeline:
- return False
-
- # branches are ORed
- matches_branch = False
- for branch in self.branches:
- if branch.match(event.branch):
- matches_branch = True
- if self.branches and not matches_branch:
- return False
-
- # refs are ORed
- matches_ref = False
- if event.ref is not None:
- for ref in self.refs:
- if ref.match(event.ref):
- matches_ref = True
- if self.refs and not matches_ref:
- return False
- if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
- # If the updated ref has an empty git sha (all 0s),
- # then the ref is being deleted
- return False
-
- # comments are ORed
- matches_comment_re = False
- for comment_re in self.comments:
- if (event.comment is not None and
- comment_re.search(event.comment)):
- matches_comment_re = True
- if self.comments and not matches_comment_re:
- return False
-
- # We better have an account provided by Gerrit to do
- # email filtering.
- if event.account is not None:
- account_email = event.account.get('email')
- # emails are ORed
- matches_email_re = False
- for email_re in self.emails:
- if (account_email is not None and
- email_re.search(account_email)):
- matches_email_re = True
- if self.emails and not matches_email_re:
- return False
-
- # usernames are ORed
- account_username = event.account.get('username')
- matches_username_re = False
- for username_re in self.usernames:
- if (account_username is not None and
- username_re.search(account_username)):
- matches_username_re = True
- if self.usernames and not matches_username_re:
- return False
-
- # approvals are ANDed
- for category, value in self.event_approvals.items():
- matches_approval = False
- for eapproval in event.approvals:
- if (normalizeCategory(eapproval['description']) == category and
- int(eapproval['value']) == int(value)):
- matches_approval = True
- if not matches_approval:
- return False
-
- # required approvals are ANDed (reject approvals are ORed)
- if not self.matchesApprovals(change):
- return False
-
- # timespecs are ORed
- matches_timespec = False
- for timespec in self.timespecs:
- if (event.timespec == timespec):
- matches_timespec = True
- if self.timespecs and not matches_timespec:
- return False
-
- # actions are ORed
- matches_action = False
- for action in self.actions:
- if (event.action == action):
- matches_action = True
- if self.actions and not matches_action:
- return False
-
- # labels are ORed
- if self.labels and event.label not in self.labels:
- return False
-
- # unlabels are ORed
- if self.unlabels and event.unlabel not in self.unlabels:
- return False
-
- # states are ORed
- if self.states and event.state not in self.states:
- return False
-
+ def matches(self, event, ref):
+ # TODO(jeblair): consider removing ref argument
return True
-class ChangeishFilter(BaseFilter):
+class RefFilter(BaseFilter):
"""Allows a Manager to only enqueue Changes that meet certain criteria."""
- def __init__(self, open=None, current_patchset=None,
- statuses=[], required_approvals=[],
- reject_approvals=[]):
- super(ChangeishFilter, self).__init__(
- required_approvals=required_approvals,
- reject_approvals=reject_approvals)
- self.open = open
- self.current_patchset = current_patchset
- self.statuses = statuses
-
- def __repr__(self):
- ret = '<ChangeishFilter'
-
- if self.open is not None:
- ret += ' open: %s' % self.open
- if self.current_patchset is not None:
- ret += ' current-patchset: %s' % self.current_patchset
- if self.statuses:
- ret += ' statuses: %s' % ', '.join(self.statuses)
- if self.required_approvals:
- ret += (' required_approvals: %s' %
- str(self.required_approvals))
- if self.reject_approvals:
- ret += (' reject_approvals: %s' %
- str(self.reject_approvals))
- ret += '>'
-
- return ret
+ def __init__(self):
+ super(RefFilter, self).__init__()
def matches(self, change):
- if self.open is not None:
- if self.open != change.open:
- return False
-
- if self.current_patchset is not None:
- if self.current_patchset != change.is_current_patchset:
- return False
-
- if self.statuses:
- if change.status not in self.statuses:
- return False
-
- # required approvals are ANDed (reject approvals are ORed)
- if not self.matchesApprovals(change):
- return False
-
return True
diff --git a/zuul/source/__init__.py b/zuul/source/__init__.py
index f0eeba6..68baf0e 100644
--- a/zuul/source/__init__.py
+++ b/zuul/source/__init__.py
@@ -69,3 +69,13 @@
@abc.abstractmethod
def getProjectBranches(self, project):
"""Get branches for a project"""
+
+ @abc.abstractmethod
+ def getRequireFilters(self, config):
+ """Return a list of ChangeFilters for the scheduler to match against.
+ """
+
+ @abc.abstractmethod
+ def getRejectFilters(self, config):
+ """Return a list of ChangeFilters for the scheduler to match against.
+ """