Merge "Add driver-specific pipeline requirements" into feature/zuulv3
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 90440f7..1374e9b 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -604,6 +604,8 @@
methods = {
'trigger': 'getTriggerSchema',
'reporter': 'getReporterSchema',
+ 'require': 'getRequireSchema',
+ 'reject': 'getRejectSchema',
}
schema = {}
@@ -665,6 +667,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',
@@ -741,24 +747,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 25cce42..dcbc172 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -27,8 +27,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
@@ -73,7 +74,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')
@@ -321,7 +322,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.
+ """