Merge "Add driver-specific pipeline requirements" into feature/zuulv3
This commit is contained in:
commit
2cf8d2ac6d
|
@ -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
|
||||
|
|
|
@ -604,6 +604,8 @@ class PipelineParser(object):
|
|||
methods = {
|
||||
'trigger': 'getTriggerSchema',
|
||||
'reporter': 'getReporterSchema',
|
||||
'require': 'getRequireSchema',
|
||||
'reject': 'getRejectSchema',
|
||||
}
|
||||
|
||||
schema = {}
|
||||
|
@ -665,6 +667,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',
|
||||
|
@ -741,24 +747,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()
|
||||
|
|
|
@ -27,8 +27,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
|
||||
|
@ -73,7 +74,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')
|
||||
|
@ -321,7 +322,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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Reference in New Issue