Merge "Add driver-specific pipeline requirements" into feature/zuulv3
This commit is contained in:
commit
2cf8d2ac6d
@ -54,7 +54,7 @@ Changes
|
|||||||
Filters
|
Filters
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
.. autoclass:: zuul.model.ChangeishFilter
|
.. autoclass:: zuul.model.RefFilter
|
||||||
.. autoclass:: zuul.model.EventFilter
|
.. autoclass:: zuul.model.EventFilter
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,8 +11,9 @@
|
|||||||
gerrit:
|
gerrit:
|
||||||
verified: -1
|
verified: -1
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- email: jenkins@example.com
|
approval:
|
||||||
|
- email: jenkins@example.com
|
||||||
|
|
||||||
- pipeline:
|
- pipeline:
|
||||||
name: trigger
|
name: trigger
|
||||||
|
@ -11,9 +11,10 @@
|
|||||||
gerrit:
|
gerrit:
|
||||||
verified: -1
|
verified: -1
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- username: jenkins
|
approval:
|
||||||
newer-than: 48h
|
- username: jenkins
|
||||||
|
newer-than: 48h
|
||||||
|
|
||||||
- pipeline:
|
- pipeline:
|
||||||
name: trigger
|
name: trigger
|
||||||
|
@ -11,9 +11,10 @@
|
|||||||
gerrit:
|
gerrit:
|
||||||
verified: -1
|
verified: -1
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- username: jenkins
|
approval:
|
||||||
older-than: 48h
|
- username: jenkins
|
||||||
|
older-than: 48h
|
||||||
|
|
||||||
- pipeline:
|
- pipeline:
|
||||||
name: trigger
|
name: trigger
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
name: pipeline
|
name: pipeline
|
||||||
manager: independent
|
manager: independent
|
||||||
reject:
|
reject:
|
||||||
approval:
|
gerrit:
|
||||||
- username: jenkins
|
approval:
|
||||||
|
- username: jenkins
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: comment-added
|
- event: comment-added
|
||||||
|
@ -2,16 +2,18 @@
|
|||||||
name: pipeline
|
name: pipeline
|
||||||
manager: independent
|
manager: independent
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- username: jenkins
|
approval:
|
||||||
verified:
|
- username: jenkins
|
||||||
- 1
|
verified:
|
||||||
- 2
|
- 1
|
||||||
|
- 2
|
||||||
reject:
|
reject:
|
||||||
approval:
|
gerrit:
|
||||||
- verified:
|
approval:
|
||||||
- -1
|
- verified:
|
||||||
- -2
|
- -1
|
||||||
|
- -2
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: comment-added
|
- event: comment-added
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
name: current-check
|
name: current-check
|
||||||
manager: independent
|
manager: independent
|
||||||
require:
|
require:
|
||||||
current-patchset: true
|
gerrit:
|
||||||
|
current-patchset: true
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: patchset-created
|
- event: patchset-created
|
||||||
@ -18,7 +19,8 @@
|
|||||||
name: open-check
|
name: open-check
|
||||||
manager: independent
|
manager: independent
|
||||||
require:
|
require:
|
||||||
open: true
|
gerrit:
|
||||||
|
open: true
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: patchset-created
|
- event: patchset-created
|
||||||
@ -34,7 +36,8 @@
|
|||||||
name: status-check
|
name: status-check
|
||||||
manager: independent
|
manager: independent
|
||||||
require:
|
require:
|
||||||
status: NEW
|
gerrit:
|
||||||
|
status: NEW
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: patchset-created
|
- event: patchset-created
|
||||||
|
@ -11,8 +11,9 @@
|
|||||||
gerrit:
|
gerrit:
|
||||||
verified: -1
|
verified: -1
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- username: ^(jenkins|zuul)$
|
approval:
|
||||||
|
- username: ^(jenkins|zuul)$
|
||||||
|
|
||||||
- pipeline:
|
- pipeline:
|
||||||
name: trigger
|
name: trigger
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
name: pipeline
|
name: pipeline
|
||||||
manager: independent
|
manager: independent
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- username: jenkins
|
approval:
|
||||||
verified: 1
|
- username: jenkins
|
||||||
|
verified: 1
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: comment-added
|
- event: comment-added
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
name: pipeline
|
name: pipeline
|
||||||
manager: independent
|
manager: independent
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- username: jenkins
|
approval:
|
||||||
verified:
|
- username: jenkins
|
||||||
- 1
|
verified:
|
||||||
- 2
|
- 1
|
||||||
|
- 2
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: comment-added
|
- event: comment-added
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
name: check
|
name: check
|
||||||
manager: independent
|
manager: independent
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- verified: -1
|
approval:
|
||||||
|
- verified: -1
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: patchset-created
|
- event: patchset-created
|
||||||
@ -21,8 +22,9 @@
|
|||||||
name: gate
|
name: gate
|
||||||
manager: dependent
|
manager: dependent
|
||||||
require:
|
require:
|
||||||
approval:
|
gerrit:
|
||||||
- verified: 1
|
approval:
|
||||||
|
- verified: 1
|
||||||
trigger:
|
trigger:
|
||||||
gerrit:
|
gerrit:
|
||||||
- event: comment-added
|
- event: comment-added
|
||||||
|
@ -604,6 +604,8 @@ class PipelineParser(object):
|
|||||||
methods = {
|
methods = {
|
||||||
'trigger': 'getTriggerSchema',
|
'trigger': 'getTriggerSchema',
|
||||||
'reporter': 'getReporterSchema',
|
'reporter': 'getReporterSchema',
|
||||||
|
'require': 'getRequireSchema',
|
||||||
|
'reject': 'getRejectSchema',
|
||||||
}
|
}
|
||||||
|
|
||||||
schema = {}
|
schema = {}
|
||||||
@ -665,6 +667,10 @@ class PipelineParser(object):
|
|||||||
'_source_context': model.SourceContext,
|
'_source_context': model.SourceContext,
|
||||||
'_start_mark': yaml.Mark,
|
'_start_mark': yaml.Mark,
|
||||||
}
|
}
|
||||||
|
pipeline['require'] = PipelineParser.getDriverSchema('require',
|
||||||
|
connections)
|
||||||
|
pipeline['reject'] = PipelineParser.getDriverSchema('reject',
|
||||||
|
connections)
|
||||||
pipeline['trigger'] = vs.Required(
|
pipeline['trigger'] = vs.Required(
|
||||||
PipelineParser.getDriverSchema('trigger', connections))
|
PipelineParser.getDriverSchema('trigger', connections))
|
||||||
for action in ['start', 'success', 'failure', 'merge-failure',
|
for action in ['start', 'success', 'failure', 'merge-failure',
|
||||||
@ -741,24 +747,21 @@ class PipelineParser(object):
|
|||||||
pipeline.setManager(manager)
|
pipeline.setManager(manager)
|
||||||
layout.pipelines[conf['name']] = pipeline
|
layout.pipelines[conf['name']] = pipeline
|
||||||
|
|
||||||
if 'require' in conf or 'reject' in conf:
|
for source_name, require_config in conf.get('require', {}).items():
|
||||||
require = conf.get('require', {})
|
source = connections.getSource(source_name)
|
||||||
reject = conf.get('reject', {})
|
manager.changeish_filters.extend(
|
||||||
f = model.ChangeishFilter(
|
source.getRequireFilters(require_config))
|
||||||
open=require.get('open'),
|
|
||||||
current_patchset=require.get('current-patchset'),
|
for source_name, reject_config in conf.get('reject', {}).items():
|
||||||
statuses=as_list(require.get('status')),
|
source = connections.getSource(source_name)
|
||||||
required_approvals=as_list(require.get('approval')),
|
manager.changeish_filters.extend(
|
||||||
reject_approvals=as_list(reject.get('approval'))
|
source.getRejectFilters(reject_config))
|
||||||
)
|
|
||||||
manager.changeish_filters.append(f)
|
|
||||||
|
|
||||||
for trigger_name, trigger_config in conf.get('trigger').items():
|
for trigger_name, trigger_config in conf.get('trigger').items():
|
||||||
trigger = connections.getTrigger(trigger_name, trigger_config)
|
trigger = connections.getTrigger(trigger_name, trigger_config)
|
||||||
pipeline.triggers.append(trigger)
|
pipeline.triggers.append(trigger)
|
||||||
|
manager.event_filters.extend(
|
||||||
manager.event_filters += trigger.getEventFilters(
|
trigger.getEventFilters(conf['trigger'][trigger_name]))
|
||||||
conf['trigger'][trigger_name])
|
|
||||||
|
|
||||||
return pipeline
|
return pipeline
|
||||||
|
|
||||||
|
@ -191,6 +191,30 @@ class SourceInterface(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
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)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class ReporterInterface(object):
|
class ReporterInterface(object):
|
||||||
|
@ -41,3 +41,9 @@ class GerritDriver(Driver, ConnectionInterface, TriggerInterface,
|
|||||||
|
|
||||||
def getReporterSchema(self):
|
def getReporterSchema(self):
|
||||||
return gerritreporter.getSchema()
|
return gerritreporter.getSchema()
|
||||||
|
|
||||||
|
def getRequireSchema(self):
|
||||||
|
return gerritsource.getRequireSchema()
|
||||||
|
|
||||||
|
def getRejectSchema(self):
|
||||||
|
return gerritsource.getRejectSchema()
|
||||||
|
@ -27,8 +27,9 @@ import pprint
|
|||||||
import voluptuous as v
|
import voluptuous as v
|
||||||
|
|
||||||
from zuul.connection import BaseConnection
|
from zuul.connection import BaseConnection
|
||||||
from zuul.model import TriggerEvent, Change, Ref
|
from zuul.model import Ref
|
||||||
from zuul import exceptions
|
from zuul import exceptions
|
||||||
|
from zuul.driver.gerrit.gerritmodel import GerritChange, GerritTriggerEvent
|
||||||
|
|
||||||
|
|
||||||
# Walk the change dependency tree to find a cycle
|
# Walk the change dependency tree to find a cycle
|
||||||
@ -73,7 +74,7 @@ class GerritEventConnector(threading.Thread):
|
|||||||
# should always be a constant number of seconds behind Gerrit.
|
# should always be a constant number of seconds behind Gerrit.
|
||||||
now = time.time()
|
now = time.time()
|
||||||
time.sleep(max((ts + self.delay) - now, 0.0))
|
time.sleep(max((ts + self.delay) - now, 0.0))
|
||||||
event = TriggerEvent()
|
event = GerritTriggerEvent()
|
||||||
event.type = data.get('type')
|
event.type = data.get('type')
|
||||||
event.trigger_name = 'gerrit'
|
event.trigger_name = 'gerrit'
|
||||||
change = data.get('change')
|
change = data.get('change')
|
||||||
@ -321,7 +322,7 @@ class GerritConnection(BaseConnection):
|
|||||||
if change and not refresh:
|
if change and not refresh:
|
||||||
return change
|
return change
|
||||||
if not change:
|
if not change:
|
||||||
change = Change(None)
|
change = GerritChange(None)
|
||||||
change.number = number
|
change.number = number
|
||||||
change.patchset = patchset
|
change.patchset = patchset
|
||||||
key = '%s,%s' % (change.number, change.patchset)
|
key = '%s,%s' % (change.number, change.patchset)
|
||||||
|
343
zuul/driver/gerrit/gerritmodel.py
Normal file
343
zuul/driver/gerrit/gerritmodel.py
Normal file
@ -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
|
@ -13,8 +13,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import voluptuous as vs
|
||||||
from zuul.source import BaseSource
|
from zuul.source import BaseSource
|
||||||
from zuul.model import Project
|
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):
|
class GerritSource(BaseSource):
|
||||||
@ -59,3 +62,41 @@ class GerritSource(BaseSource):
|
|||||||
|
|
||||||
def _getGitwebUrl(self, project, sha=None):
|
def _getGitwebUrl(self, project, sha=None):
|
||||||
return self.connection._getGitwebUrl(project, sha)
|
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
|
||||||
|
@ -14,8 +14,9 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import voluptuous as v
|
import voluptuous as v
|
||||||
from zuul.model import EventFilter
|
|
||||||
from zuul.trigger import BaseTrigger
|
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):
|
class GerritTrigger(BaseTrigger):
|
||||||
@ -23,43 +24,36 @@ class GerritTrigger(BaseTrigger):
|
|||||||
log = logging.getLogger("zuul.GerritTrigger")
|
log = logging.getLogger("zuul.GerritTrigger")
|
||||||
|
|
||||||
def getEventFilters(self, trigger_conf):
|
def getEventFilters(self, trigger_conf):
|
||||||
def toList(item):
|
|
||||||
if not item:
|
|
||||||
return []
|
|
||||||
if isinstance(item, list):
|
|
||||||
return item
|
|
||||||
return [item]
|
|
||||||
|
|
||||||
efilters = []
|
efilters = []
|
||||||
for trigger in toList(trigger_conf):
|
for trigger in to_list(trigger_conf):
|
||||||
approvals = {}
|
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():
|
for key, val in approval_dict.items():
|
||||||
approvals[key] = val
|
approvals[key] = val
|
||||||
# Backwards compat for *_filter versions of these args
|
# Backwards compat for *_filter versions of these args
|
||||||
comments = toList(trigger.get('comment'))
|
comments = to_list(trigger.get('comment'))
|
||||||
if not comments:
|
if not comments:
|
||||||
comments = toList(trigger.get('comment_filter'))
|
comments = to_list(trigger.get('comment_filter'))
|
||||||
emails = toList(trigger.get('email'))
|
emails = to_list(trigger.get('email'))
|
||||||
if not emails:
|
if not emails:
|
||||||
emails = toList(trigger.get('email_filter'))
|
emails = to_list(trigger.get('email_filter'))
|
||||||
usernames = toList(trigger.get('username'))
|
usernames = to_list(trigger.get('username'))
|
||||||
if not usernames:
|
if not usernames:
|
||||||
usernames = toList(trigger.get('username_filter'))
|
usernames = to_list(trigger.get('username_filter'))
|
||||||
ignore_deletes = trigger.get('ignore-deletes', True)
|
ignore_deletes = trigger.get('ignore-deletes', True)
|
||||||
f = EventFilter(
|
f = GerritEventFilter(
|
||||||
trigger=self,
|
trigger=self,
|
||||||
types=toList(trigger['event']),
|
types=to_list(trigger['event']),
|
||||||
branches=toList(trigger.get('branch')),
|
branches=to_list(trigger.get('branch')),
|
||||||
refs=toList(trigger.get('ref')),
|
refs=to_list(trigger.get('ref')),
|
||||||
event_approvals=approvals,
|
event_approvals=approvals,
|
||||||
comments=comments,
|
comments=comments,
|
||||||
emails=emails,
|
emails=emails,
|
||||||
usernames=usernames,
|
usernames=usernames,
|
||||||
required_approvals=(
|
required_approvals=(
|
||||||
toList(trigger.get('require-approval'))
|
to_list(trigger.get('require-approval'))
|
||||||
),
|
),
|
||||||
reject_approvals=toList(
|
reject_approvals=to_list(
|
||||||
trigger.get('reject-approval')
|
trigger.get('reject-approval')
|
||||||
),
|
),
|
||||||
ignore_deletes=ignore_deletes
|
ignore_deletes=ignore_deletes
|
||||||
@ -80,8 +74,6 @@ def validate_conf(trigger_conf):
|
|||||||
|
|
||||||
|
|
||||||
def getSchema():
|
def getSchema():
|
||||||
def toList(x):
|
|
||||||
return v.Any([x], x)
|
|
||||||
variable_dict = v.Schema(dict)
|
variable_dict = v.Schema(dict)
|
||||||
|
|
||||||
approval = v.Schema({'username': str,
|
approval = v.Schema({'username': str,
|
||||||
@ -93,25 +85,25 @@ def getSchema():
|
|||||||
|
|
||||||
gerrit_trigger = {
|
gerrit_trigger = {
|
||||||
v.Required('event'):
|
v.Required('event'):
|
||||||
toList(v.Any('patchset-created',
|
scalar_or_list(v.Any('patchset-created',
|
||||||
'draft-published',
|
'draft-published',
|
||||||
'change-abandoned',
|
'change-abandoned',
|
||||||
'change-restored',
|
'change-restored',
|
||||||
'change-merged',
|
'change-merged',
|
||||||
'comment-added',
|
'comment-added',
|
||||||
'ref-updated')),
|
'ref-updated')),
|
||||||
'comment_filter': toList(str),
|
'comment_filter': scalar_or_list(str),
|
||||||
'comment': toList(str),
|
'comment': scalar_or_list(str),
|
||||||
'email_filter': toList(str),
|
'email_filter': scalar_or_list(str),
|
||||||
'email': toList(str),
|
'email': scalar_or_list(str),
|
||||||
'username_filter': toList(str),
|
'username_filter': scalar_or_list(str),
|
||||||
'username': toList(str),
|
'username': scalar_or_list(str),
|
||||||
'branch': toList(str),
|
'branch': scalar_or_list(str),
|
||||||
'ref': toList(str),
|
'ref': scalar_or_list(str),
|
||||||
'ignore-deletes': bool,
|
'ignore-deletes': bool,
|
||||||
'approval': toList(variable_dict),
|
'approval': scalar_or_list(variable_dict),
|
||||||
'require-approval': toList(approval),
|
'require-approval': scalar_or_list(approval),
|
||||||
'reject-approval': toList(approval),
|
'reject-approval': scalar_or_list(approval),
|
||||||
}
|
}
|
||||||
|
|
||||||
return gerrit_trigger
|
return gerrit_trigger
|
||||||
|
@ -25,3 +25,9 @@ class GitDriver(Driver, ConnectionInterface, SourceInterface):
|
|||||||
|
|
||||||
def getSource(self, connection):
|
def getSource(self, connection):
|
||||||
return gitsource.GitSource(self, connection)
|
return gitsource.GitSource(self, connection)
|
||||||
|
|
||||||
|
def getRequireSchema(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def getRejectSchema(self):
|
||||||
|
return {}
|
||||||
|
@ -53,3 +53,9 @@ class GitSource(BaseSource):
|
|||||||
|
|
||||||
def getProjectOpenChanges(self, project):
|
def getProjectOpenChanges(self, project):
|
||||||
raise NotImplemented()
|
raise NotImplemented()
|
||||||
|
|
||||||
|
def getRequireFilters(self, config):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def getRejectFilters(self, config):
|
||||||
|
return []
|
||||||
|
@ -41,3 +41,9 @@ class GithubDriver(Driver, ConnectionInterface, TriggerInterface,
|
|||||||
|
|
||||||
def getReporterSchema(self):
|
def getReporterSchema(self):
|
||||||
return githubreporter.getSchema()
|
return githubreporter.getSchema()
|
||||||
|
|
||||||
|
def getRequireSchema(self):
|
||||||
|
return githubsource.getRequireSchema()
|
||||||
|
|
||||||
|
def getRejectSchema(self):
|
||||||
|
return githubsource.getRejectSchema()
|
||||||
|
@ -25,8 +25,9 @@ import github3
|
|||||||
from github3.exceptions import MethodNotAllowed
|
from github3.exceptions import MethodNotAllowed
|
||||||
|
|
||||||
from zuul.connection import BaseConnection
|
from zuul.connection import BaseConnection
|
||||||
from zuul.model import PullRequest, Ref, GithubTriggerEvent
|
from zuul.model import Ref
|
||||||
from zuul.exceptions import MergeFailure
|
from zuul.exceptions import MergeFailure
|
||||||
|
from zuul.driver.github.githubmodel import PullRequest, GithubTriggerEvent
|
||||||
|
|
||||||
|
|
||||||
class GithubWebhookListener():
|
class GithubWebhookListener():
|
||||||
|
163
zuul/driver/github/githubmodel.py
Normal file
163
zuul/driver/github/githubmodel.py
Normal file
@ -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
|
@ -90,3 +90,17 @@ class GithubSource(BaseSource):
|
|||||||
|
|
||||||
def _ghTimestampToDate(self, timestamp):
|
def _ghTimestampToDate(self, timestamp):
|
||||||
return time.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
|
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 {}
|
||||||
|
@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import voluptuous as v
|
import voluptuous as v
|
||||||
from zuul.model import EventFilter
|
|
||||||
from zuul.trigger import BaseTrigger
|
from zuul.trigger import BaseTrigger
|
||||||
|
from zuul.driver.github.githubmodel import GithubEventFilter
|
||||||
|
|
||||||
|
|
||||||
class GithubTrigger(BaseTrigger):
|
class GithubTrigger(BaseTrigger):
|
||||||
@ -32,7 +32,7 @@ class GithubTrigger(BaseTrigger):
|
|||||||
|
|
||||||
efilters = []
|
efilters = []
|
||||||
for trigger in toList(trigger_config):
|
for trigger in toList(trigger_config):
|
||||||
f = EventFilter(
|
f = GithubEventFilter(
|
||||||
trigger=self,
|
trigger=self,
|
||||||
types=toList(trigger['event']),
|
types=toList(trigger['event']),
|
||||||
actions=toList(trigger.get('action')),
|
actions=toList(trigger.get('action')),
|
||||||
|
@ -20,8 +20,8 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
|||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
from zuul.driver import Driver, TriggerInterface
|
from zuul.driver import Driver, TriggerInterface
|
||||||
from zuul.model import TriggerEvent
|
|
||||||
from zuul.driver.timer import timertrigger
|
from zuul.driver.timer import timertrigger
|
||||||
|
from zuul.driver.timer.timermodel import TimerTriggerEvent
|
||||||
|
|
||||||
|
|
||||||
class TimerDriver(Driver, TriggerInterface):
|
class TimerDriver(Driver, TriggerInterface):
|
||||||
@ -81,7 +81,7 @@ class TimerDriver(Driver, TriggerInterface):
|
|||||||
def _onTrigger(self, tenant, pipeline_name, timespec):
|
def _onTrigger(self, tenant, pipeline_name, timespec):
|
||||||
for project_name in tenant.layout.project_configs.keys():
|
for project_name in tenant.layout.project_configs.keys():
|
||||||
project_hostname, project_name = project_name.split('/', 1)
|
project_hostname, project_name = project_name.split('/', 1)
|
||||||
event = TriggerEvent()
|
event = TimerTriggerEvent()
|
||||||
event.type = 'timer'
|
event.type = 'timer'
|
||||||
event.timespec = timespec
|
event.timespec = timespec
|
||||||
event.forced_pipeline = pipeline_name
|
event.forced_pipeline = pipeline_name
|
||||||
|
62
zuul/driver/timer/timermodel.py
Normal file
62
zuul/driver/timer/timermodel.py
Normal file
@ -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
|
@ -15,26 +15,20 @@
|
|||||||
|
|
||||||
import voluptuous as v
|
import voluptuous as v
|
||||||
|
|
||||||
from zuul.model import EventFilter
|
|
||||||
from zuul.trigger import BaseTrigger
|
from zuul.trigger import BaseTrigger
|
||||||
|
from zuul.driver.timer.timermodel import TimerEventFilter
|
||||||
|
from zuul.driver.util import to_list
|
||||||
|
|
||||||
|
|
||||||
class TimerTrigger(BaseTrigger):
|
class TimerTrigger(BaseTrigger):
|
||||||
name = 'timer'
|
name = 'timer'
|
||||||
|
|
||||||
def getEventFilters(self, trigger_conf):
|
def getEventFilters(self, trigger_conf):
|
||||||
def toList(item):
|
|
||||||
if not item:
|
|
||||||
return []
|
|
||||||
if isinstance(item, list):
|
|
||||||
return item
|
|
||||||
return [item]
|
|
||||||
|
|
||||||
efilters = []
|
efilters = []
|
||||||
for trigger in toList(trigger_conf):
|
for trigger in to_list(trigger_conf):
|
||||||
f = EventFilter(trigger=self,
|
f = TimerEventFilter(trigger=self,
|
||||||
types=['timer'],
|
types=['timer'],
|
||||||
timespecs=toList(trigger['time']))
|
timespecs=to_list(trigger['time']))
|
||||||
|
|
||||||
efilters.append(f)
|
efilters.append(f)
|
||||||
|
|
||||||
|
43
zuul/driver/util.py
Normal file
43
zuul/driver/util.py
Normal file
@ -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]
|
@ -15,7 +15,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from zuul.driver import Driver, TriggerInterface
|
from zuul.driver import Driver, TriggerInterface
|
||||||
from zuul.model import TriggerEvent
|
from zuul.driver.zuul.zuulmodel import ZuulTriggerEvent
|
||||||
|
|
||||||
from zuul.driver.zuul import zuultrigger
|
from zuul.driver.zuul import zuultrigger
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class ZuulDriver(Driver, TriggerInterface):
|
|||||||
self._createProjectChangeMergedEvent(open_change)
|
self._createProjectChangeMergedEvent(open_change)
|
||||||
|
|
||||||
def _createProjectChangeMergedEvent(self, change):
|
def _createProjectChangeMergedEvent(self, change):
|
||||||
event = TriggerEvent()
|
event = ZuulTriggerEvent()
|
||||||
event.type = PROJECT_CHANGE_MERGED
|
event.type = PROJECT_CHANGE_MERGED
|
||||||
event.trigger_name = self.name
|
event.trigger_name = self.name
|
||||||
event.project_hostname = change.project.canonical_hostname
|
event.project_hostname = change.project.canonical_hostname
|
||||||
@ -94,7 +94,7 @@ class ZuulDriver(Driver, TriggerInterface):
|
|||||||
self._createParentChangeEnqueuedEvent(needs, pipeline)
|
self._createParentChangeEnqueuedEvent(needs, pipeline)
|
||||||
|
|
||||||
def _createParentChangeEnqueuedEvent(self, change, pipeline):
|
def _createParentChangeEnqueuedEvent(self, change, pipeline):
|
||||||
event = TriggerEvent()
|
event = ZuulTriggerEvent()
|
||||||
event.type = PARENT_CHANGE_ENQUEUED
|
event.type = PARENT_CHANGE_ENQUEUED
|
||||||
event.trigger_name = self.name
|
event.trigger_name = self.name
|
||||||
event.pipeline_name = pipeline.name
|
event.pipeline_name = pipeline.name
|
||||||
|
63
zuul/driver/zuul/zuulmodel.py
Normal file
63
zuul/driver/zuul/zuulmodel.py
Normal file
@ -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
|
@ -15,8 +15,9 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import voluptuous as v
|
import voluptuous as v
|
||||||
from zuul.model import EventFilter
|
|
||||||
from zuul.trigger import BaseTrigger
|
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):
|
class ZuulTrigger(BaseTrigger):
|
||||||
@ -29,25 +30,12 @@ class ZuulTrigger(BaseTrigger):
|
|||||||
self._handle_project_change_merged_events = False
|
self._handle_project_change_merged_events = False
|
||||||
|
|
||||||
def getEventFilters(self, trigger_conf):
|
def getEventFilters(self, trigger_conf):
|
||||||
def toList(item):
|
|
||||||
if not item:
|
|
||||||
return []
|
|
||||||
if isinstance(item, list):
|
|
||||||
return item
|
|
||||||
return [item]
|
|
||||||
|
|
||||||
efilters = []
|
efilters = []
|
||||||
for trigger in toList(trigger_conf):
|
for trigger in to_list(trigger_conf):
|
||||||
f = EventFilter(
|
f = ZuulEventFilter(
|
||||||
trigger=self,
|
trigger=self,
|
||||||
types=toList(trigger['event']),
|
types=to_list(trigger['event']),
|
||||||
pipelines=toList(trigger.get('pipeline')),
|
pipelines=to_list(trigger.get('pipeline')),
|
||||||
required_approvals=(
|
|
||||||
toList(trigger.get('require-approval'))
|
|
||||||
),
|
|
||||||
reject_approvals=toList(
|
|
||||||
trigger.get('reject-approval')
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
efilters.append(f)
|
efilters.append(f)
|
||||||
|
|
||||||
@ -55,9 +43,6 @@ class ZuulTrigger(BaseTrigger):
|
|||||||
|
|
||||||
|
|
||||||
def getSchema():
|
def getSchema():
|
||||||
def toList(x):
|
|
||||||
return v.Any([x], x)
|
|
||||||
|
|
||||||
approval = v.Schema({'username': str,
|
approval = v.Schema({'username': str,
|
||||||
'email-filter': str,
|
'email-filter': str,
|
||||||
'email': str,
|
'email': str,
|
||||||
@ -67,11 +52,11 @@ def getSchema():
|
|||||||
|
|
||||||
zuul_trigger = {
|
zuul_trigger = {
|
||||||
v.Required('event'):
|
v.Required('event'):
|
||||||
toList(v.Any('parent-change-enqueued',
|
scalar_or_list(v.Any('parent-change-enqueued',
|
||||||
'project-change-merged')),
|
'project-change-merged')),
|
||||||
'pipeline': toList(str),
|
'pipeline': scalar_or_list(str),
|
||||||
'require-approval': toList(approval),
|
'require-approval': scalar_or_list(approval),
|
||||||
'reject-approval': toList(approval),
|
'reject-approval': scalar_or_list(approval),
|
||||||
}
|
}
|
||||||
|
|
||||||
return zuul_trigger
|
return zuul_trigger
|
||||||
|
401
zuul/model.py
401
zuul/model.py
@ -16,7 +16,6 @@ import abc
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -28,8 +27,6 @@ OrderedDict = extras.try_imports(['collections.OrderedDict',
|
|||||||
'ordereddict.OrderedDict'])
|
'ordereddict.OrderedDict'])
|
||||||
|
|
||||||
|
|
||||||
EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
|
|
||||||
|
|
||||||
MERGER_MERGE = 1 # "git merge"
|
MERGER_MERGE = 1 # "git merge"
|
||||||
MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
|
MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
|
||||||
MERGER_CHERRY_PICK = 3 # "git cherry-pick"
|
MERGER_CHERRY_PICK = 3 # "git cherry-pick"
|
||||||
@ -78,25 +75,6 @@ NODE_STATES = set([STATE_BUILDING,
|
|||||||
STATE_DELETING])
|
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):
|
class Attributes(object):
|
||||||
"""A class to hold attributes for string formatting."""
|
"""A class to hold attributes for string formatting."""
|
||||||
|
|
||||||
@ -1810,7 +1788,6 @@ class Change(Ref):
|
|||||||
self.can_merge = False
|
self.can_merge = False
|
||||||
self.is_merged = False
|
self.is_merged = False
|
||||||
self.failed_to_merge = False
|
self.failed_to_merge = False
|
||||||
self.approvals = []
|
|
||||||
self.open = None
|
self.open = None
|
||||||
self.status = None
|
self.status = None
|
||||||
self.owner = None
|
self.owner = None
|
||||||
@ -1863,24 +1840,10 @@ class Change(Ref):
|
|||||||
patchset=self.patchset)
|
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):
|
class TriggerEvent(object):
|
||||||
"""Incoming event from an external system."""
|
"""Incoming event from an external system."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
# TODO(jeblair): further reduce this list
|
||||||
self.data = None
|
self.data = None
|
||||||
# common
|
# common
|
||||||
self.type = None
|
self.type = None
|
||||||
@ -1896,20 +1859,13 @@ class TriggerEvent(object):
|
|||||||
self.change_url = None
|
self.change_url = None
|
||||||
self.patch_number = None
|
self.patch_number = None
|
||||||
self.refspec = None
|
self.refspec = None
|
||||||
self.approvals = []
|
|
||||||
self.branch = None
|
self.branch = None
|
||||||
self.comment = None
|
self.comment = None
|
||||||
self.label = None
|
|
||||||
self.unlabel = None
|
|
||||||
self.state = None
|
self.state = None
|
||||||
# ref-updated
|
# ref-updated
|
||||||
self.ref = None
|
self.ref = None
|
||||||
self.oldrev = None
|
self.oldrev = None
|
||||||
self.newrev = None
|
self.newrev = None
|
||||||
# timer
|
|
||||||
self.timespec = None
|
|
||||||
# zuultrigger
|
|
||||||
self.pipeline_name = None
|
|
||||||
# For events that arrive with a destination pipeline (eg, from
|
# For events that arrive with a destination pipeline (eg, from
|
||||||
# an admin command, etc):
|
# an admin command, etc):
|
||||||
self.forced_pipeline = None
|
self.forced_pipeline = None
|
||||||
@ -1918,374 +1874,35 @@ class TriggerEvent(object):
|
|||||||
def canonical_project_name(self):
|
def canonical_project_name(self):
|
||||||
return self.project_hostname + '/' + self.project_name
|
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):
|
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
|
return False
|
||||||
|
|
||||||
def isChangeAbandoned(self):
|
def isChangeAbandoned(self):
|
||||||
if self.type == 'pull_request':
|
|
||||||
return 'closed' == self.action
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BaseFilter(object):
|
class BaseFilter(object):
|
||||||
"""Base Class for filtering which Changes and Events to process."""
|
"""Base Class for filtering which Changes and Events to process."""
|
||||||
def __init__(self, required_approvals=[], reject_approvals=[]):
|
pass
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class EventFilter(BaseFilter):
|
class EventFilter(BaseFilter):
|
||||||
"""Allows a Pipeline to only respond to certain events."""
|
"""Allows a Pipeline to only respond to certain events."""
|
||||||
def __init__(self, trigger, types=[], branches=[], refs=[],
|
def __init__(self, trigger):
|
||||||
event_approvals={}, comments=[], emails=[], usernames=[],
|
super(EventFilter, self).__init__()
|
||||||
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)
|
|
||||||
self.trigger = trigger
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ChangeishFilter(BaseFilter):
|
class RefFilter(BaseFilter):
|
||||||
"""Allows a Manager to only enqueue Changes that meet certain criteria."""
|
"""Allows a Manager to only enqueue Changes that meet certain criteria."""
|
||||||
def __init__(self, open=None, current_patchset=None,
|
def __init__(self):
|
||||||
statuses=[], required_approvals=[],
|
super(RefFilter, self).__init__()
|
||||||
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 matches(self, change):
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,3 +69,13 @@ class BaseSource(object):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def getProjectBranches(self, project):
|
def getProjectBranches(self, project):
|
||||||
"""Get branches for a 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.
|
||||||
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user