Add driver-specific pipeline requirements
As we expand the Github driver, we're seeing a need to specify driver-specific pipeline requirements. To accomplish this, bump the require/reject pipeline keywords down a level underneath connection names. This lets users specify per-source pipeline requirements. This adds new API methods for sources to create the new pipeline filters (by returning instances or subclasses of RefFilter, which used to be called ChangeishFilter). This change also creates and/or moves driver-specific subclasses of EventFilter and TriggerEvent in(to) their respective drivers. Change-Id: Ia56c254e3aa591a688103db5b04b3dddae7b2da4
This commit is contained in:
parent
14b315d556
commit
aad3ae2fe1
@ -54,7 +54,7 @@ Changes
|
||||
Filters
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: zuul.model.ChangeishFilter
|
||||
.. autoclass:: zuul.model.RefFilter
|
||||
.. autoclass:: zuul.model.EventFilter
|
||||
|
||||
|
||||
|
@ -11,8 +11,9 @@
|
||||
gerrit:
|
||||
verified: -1
|
||||
require:
|
||||
approval:
|
||||
- email: jenkins@example.com
|
||||
gerrit:
|
||||
approval:
|
||||
- email: jenkins@example.com
|
||||
|
||||
- pipeline:
|
||||
name: trigger
|
||||
|
@ -11,9 +11,10 @@
|
||||
gerrit:
|
||||
verified: -1
|
||||
require:
|
||||
approval:
|
||||
- username: jenkins
|
||||
newer-than: 48h
|
||||
gerrit:
|
||||
approval:
|
||||
- username: jenkins
|
||||
newer-than: 48h
|
||||
|
||||
- pipeline:
|
||||
name: trigger
|
||||
|
@ -11,9 +11,10 @@
|
||||
gerrit:
|
||||
verified: -1
|
||||
require:
|
||||
approval:
|
||||
- username: jenkins
|
||||
older-than: 48h
|
||||
gerrit:
|
||||
approval:
|
||||
- username: jenkins
|
||||
older-than: 48h
|
||||
|
||||
- pipeline:
|
||||
name: trigger
|
||||
|
@ -2,8 +2,9 @@
|
||||
name: pipeline
|
||||
manager: independent
|
||||
reject:
|
||||
approval:
|
||||
- username: jenkins
|
||||
gerrit:
|
||||
approval:
|
||||
- username: jenkins
|
||||
trigger:
|
||||
gerrit:
|
||||
- event: comment-added
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -11,8 +11,9 @@
|
||||
gerrit:
|
||||
verified: -1
|
||||
require:
|
||||
approval:
|
||||
- username: ^(jenkins|zuul)$
|
||||
gerrit:
|
||||
approval:
|
||||
- username: ^(jenkins|zuul)$
|
||||
|
||||
- pipeline:
|
||||
name: trigger
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -605,6 +605,8 @@ class PipelineParser(object):
|
||||
methods = {
|
||||
'trigger': 'getTriggerSchema',
|
||||
'reporter': 'getReporterSchema',
|
||||
'require': 'getRequireSchema',
|
||||
'reject': 'getRejectSchema',
|
||||
}
|
||||
|
||||
schema = {}
|
||||
@ -666,6 +668,10 @@ class PipelineParser(object):
|
||||
'_source_context': model.SourceContext,
|
||||
'_start_mark': yaml.Mark,
|
||||
}
|
||||
pipeline['require'] = PipelineParser.getDriverSchema('require',
|
||||
connections)
|
||||
pipeline['reject'] = PipelineParser.getDriverSchema('reject',
|
||||
connections)
|
||||
pipeline['trigger'] = vs.Required(
|
||||
PipelineParser.getDriverSchema('trigger', connections))
|
||||
for action in ['start', 'success', 'failure', 'merge-failure',
|
||||
@ -742,24 +748,21 @@ class PipelineParser(object):
|
||||
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
|
||||
|
||||
|
@ -191,6 +191,30 @@ class SourceInterface(object):
|
||||
"""
|
||||
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):
|
||||
|
@ -41,3 +41,9 @@ class GerritDriver(Driver, ConnectionInterface, TriggerInterface,
|
||||
|
||||
def getReporterSchema(self):
|
||||
return gerritreporter.getSchema()
|
||||
|
||||
def getRequireSchema(self):
|
||||
return gerritsource.getRequireSchema()
|
||||
|
||||
def getRejectSchema(self):
|
||||
return gerritsource.getRejectSchema()
|
||||
|
@ -26,8 +26,9 @@ import pprint
|
||||
import voluptuous as v
|
||||
|
||||
from zuul.connection import BaseConnection
|
||||
from zuul.model import TriggerEvent, Change, Ref
|
||||
from zuul.model import Ref
|
||||
from zuul import exceptions
|
||||
from zuul.driver.gerrit.gerritmodel import GerritChange, GerritTriggerEvent
|
||||
|
||||
|
||||
# Walk the change dependency tree to find a cycle
|
||||
@ -72,7 +73,7 @@ class GerritEventConnector(threading.Thread):
|
||||
# should always be a constant number of seconds behind Gerrit.
|
||||
now = time.time()
|
||||
time.sleep(max((ts + self.delay) - now, 0.0))
|
||||
event = TriggerEvent()
|
||||
event = GerritTriggerEvent()
|
||||
event.type = data.get('type')
|
||||
event.trigger_name = 'gerrit'
|
||||
change = data.get('change')
|
||||
@ -316,7 +317,7 @@ class GerritConnection(BaseConnection):
|
||||
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)
|
||||
|
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.
|
||||
|
||||
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 @@ class GerritSource(BaseSource):
|
||||
|
||||
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
|
||||
|
@ -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 @@ class GerritTrigger(BaseTrigger):
|
||||
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 validate_conf(trigger_conf):
|
||||
|
||||
|
||||
def getSchema():
|
||||
def toList(x):
|
||||
return v.Any([x], x)
|
||||
variable_dict = v.Schema(dict)
|
||||
|
||||
approval = v.Schema({'username': str,
|
||||
@ -93,25 +85,25 @@ def getSchema():
|
||||
|
||||
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
|
||||
|
@ -25,3 +25,9 @@ class GitDriver(Driver, ConnectionInterface, SourceInterface):
|
||||
|
||||
def getSource(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):
|
||||
raise NotImplemented()
|
||||
|
||||
def getRequireFilters(self, config):
|
||||
return []
|
||||
|
||||
def getRejectFilters(self, config):
|
||||
return []
|
||||
|
@ -41,3 +41,9 @@ class GithubDriver(Driver, ConnectionInterface, TriggerInterface,
|
||||
|
||||
def getReporterSchema(self):
|
||||
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 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():
|
||||
|
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):
|
||||
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 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 @@ class GithubTrigger(BaseTrigger):
|
||||
|
||||
efilters = []
|
||||
for trigger in toList(trigger_config):
|
||||
f = EventFilter(
|
||||
f = GithubEventFilter(
|
||||
trigger=self,
|
||||
types=toList(trigger['event']),
|
||||
actions=toList(trigger.get('action')),
|
||||
|
@ -20,8 +20,8 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
||||
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 @@ class TimerDriver(Driver, TriggerInterface):
|
||||
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
|
||||
|
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
|
||||
|
||||
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)
|
||||
|
||||
|
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
|
||||
|
||||
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 @@ class ZuulDriver(Driver, TriggerInterface):
|
||||
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 @@ class ZuulDriver(Driver, TriggerInterface):
|
||||
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
|
||||
|
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 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 @@ class ZuulTrigger(BaseTrigger):
|
||||
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 @@ class ZuulTrigger(BaseTrigger):
|
||||
|
||||
|
||||
def getSchema():
|
||||
def toList(x):
|
||||
return v.Any([x], x)
|
||||
|
||||
approval = v.Schema({'username': str,
|
||||
'email-filter': str,
|
||||
'email': str,
|
||||
@ -67,11 +52,11 @@ def getSchema():
|
||||
|
||||
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
|
||||
|
401
zuul/model.py
401
zuul/model.py
@ -16,7 +16,6 @@ import abc
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import time
|
||||
from uuid import uuid4
|
||||
@ -28,8 +27,6 @@ OrderedDict = extras.try_imports(['collections.OrderedDict',
|
||||
'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 @@ NODE_STATES = set([STATE_BUILDING,
|
||||
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 @@ class Change(Ref):
|
||||
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 @@ class Change(Ref):
|
||||
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 @@ class TriggerEvent(object):
|
||||
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 @@ class TriggerEvent(object):
|
||||
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
|
||||
|
||||
|
||||
|
@ -69,3 +69,13 @@ class BaseSource(object):
|
||||
@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.
|
||||
"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user