
This change, while substantial, is mostly organizational. Currently, connections, sources, triggers, and reporters are discrete concepts, and yet are related by virtue of the fact that the ConnectionRegistry is used to instantiate each of them. The method used to instantiate them is called "_getDriver", in recognition that behind each "trigger", etc., which appears in the config file, there is a class in the zuul.trigger hierarchy implementing the driver for that trigger. Connections also specify a "driver" in the config file. In this change, we redefine a "driver" as a single class that organizes related connections, sources, triggers and reporters. The connection, source, trigger, and reporter interfaces still exist. A driver class is responsible for indicating which of those interfaces it supports and instantiating them when asked to do so. Zuul instantiates a single instance of each driver class it knows about (currently hardcoded, but in the future, we will be able to easily ask entrypoints for these). That instance will be retained for the life of the Zuul server process. When Zuul is (re-)configured, it asks the driver instances to create new connection, source, trigger, reporter instances as necessary. For instance, a user may specify a connection that uses the "gerrit" driver, and the ConnectionRegistry would call getConnection() on the Gerrit driver instance. This is done for two reasons: first, it allows us to organize all of the code related to interfacing with an external system together. All of the existing connection, source, trigger, and reporter classes are moved as follows: zuul.connection.FOO -> zuul.driver.FOO.FOOconnection zuul.source.FOO -> zuul.driver.FOO.FOOsource zuul.trigger.FOO -> zuul.driver.FOO.FOOtrigger zuul.reporter.FOO -> zuul.driver.FOO.FOOreporter For instance, all of the code related to interfacing with Gerrit is now is zuul.driver.gerrit. Second, the addition of a single, long-lived object associated with each of these systems allows us to better support some types of interfaces. For instance, the Zuul trigger maintains a list of events it is required to emit -- this list relates to a tenant as a whole rather than individual pipelines or triggers. The timer trigger maintains a single scheduler instance for all tenants, but must be able to add or remove cron jobs based on an individual tenant being reconfigured. The global driver instance for each of these can be used to accomplish this. As a result of using the driver interface to create new connection, source, trigger and reporter instances, the connection setup in ConnectionRegistry is much simpler, and can easily be extended with entrypoints in the future. The existing tests of connections, sources, triggers, and reporters which only tested that they could be instantiated and have names have been removed, as there are functional tests which cover them. Change-Id: Ib2f7297d81f7a003de48f799dc1b09e82d4894bc
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
# Copyright 2014 Rackspace Australia
|
|
#
|
|
# 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 abc
|
|
import logging
|
|
|
|
import six
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class BaseReporter(object):
|
|
"""Base class for reporters.
|
|
|
|
Defines the exact public methods that must be supplied.
|
|
"""
|
|
|
|
log = logging.getLogger("zuul.reporter.BaseReporter")
|
|
|
|
def __init__(self, driver, connection, config=None):
|
|
self.driver = driver
|
|
self.connection = connection
|
|
self.config = config or {}
|
|
self._action = None
|
|
|
|
def setAction(self, action):
|
|
self._action = action
|
|
|
|
@abc.abstractmethod
|
|
def report(self, source, pipeline, item):
|
|
"""Send the compiled report message."""
|
|
|
|
def getSubmitAllowNeeds(self):
|
|
"""Get a list of code review labels that are allowed to be
|
|
"needed" in the submit records for a change, with respect
|
|
to this queue. In other words, the list of review labels
|
|
this reporter itself is likely to set before submitting.
|
|
"""
|
|
return []
|
|
|
|
def postConfig(self):
|
|
"""Run tasks after configuration is reloaded"""
|
|
|
|
def _getFormatter(self):
|
|
format_methods = {
|
|
'start': self._formatItemReportStart,
|
|
'success': self._formatItemReportSuccess,
|
|
'failure': self._formatItemReportFailure,
|
|
'merge-failure': self._formatItemReportMergeFailure,
|
|
'disabled': self._formatItemReportDisabled
|
|
}
|
|
return format_methods[self._action]
|
|
|
|
# TODOv3(jeblair): Consider removing pipeline argument in favor of
|
|
# item.pipeline
|
|
def _formatItemReport(self, pipeline, item):
|
|
"""Format a report from the given items. Usually to provide results to
|
|
a reporter taking free-form text."""
|
|
ret = self._getFormatter()(pipeline, item)
|
|
|
|
if pipeline.footer_message:
|
|
ret += '\n' + pipeline.footer_message
|
|
|
|
return ret
|
|
|
|
def _formatItemReportStart(self, pipeline, item):
|
|
return pipeline.start_message.format(pipeline=pipeline)
|
|
|
|
def _formatItemReportSuccess(self, pipeline, item):
|
|
return (pipeline.success_message + '\n\n' +
|
|
self._formatItemReportJobs(pipeline, item))
|
|
|
|
def _formatItemReportFailure(self, pipeline, item):
|
|
if item.dequeued_needing_change:
|
|
msg = 'This change depends on a change that failed to merge.\n'
|
|
elif item.didMergerFail():
|
|
msg = pipeline.merge_failure_message
|
|
else:
|
|
msg = (pipeline.failure_message + '\n\n' +
|
|
self._formatItemReportJobs(pipeline, item))
|
|
return msg
|
|
|
|
def _formatItemReportMergeFailure(self, pipeline, item):
|
|
return pipeline.merge_failure_message
|
|
|
|
def _formatItemReportDisabled(self, pipeline, item):
|
|
if item.current_build_set.result == 'SUCCESS':
|
|
return self._formatItemReportSuccess(pipeline, item)
|
|
elif item.current_build_set.result == 'FAILURE':
|
|
return self._formatItemReportFailure(pipeline, item)
|
|
else:
|
|
return self._formatItemReport(pipeline, item)
|
|
|
|
def _formatItemReportJobs(self, pipeline, item):
|
|
# Return the list of jobs portion of the report
|
|
ret = ''
|
|
|
|
config = self.connection.sched.config
|
|
if config.has_option('zuul', 'url_pattern'):
|
|
url_pattern = config.get('zuul', 'url_pattern')
|
|
else:
|
|
url_pattern = None
|
|
|
|
for job in item.getJobs():
|
|
build = item.current_build_set.getBuild(job.name)
|
|
(result, url) = item.formatJobResult(job, url_pattern)
|
|
if not job.voting:
|
|
voting = ' (non-voting)'
|
|
else:
|
|
voting = ''
|
|
|
|
if config and config.has_option(
|
|
'zuul', 'report_times'):
|
|
report_times = config.getboolean(
|
|
'zuul', 'report_times')
|
|
else:
|
|
report_times = True
|
|
|
|
if report_times and build.end_time and build.start_time:
|
|
dt = int(build.end_time - build.start_time)
|
|
m, s = divmod(dt, 60)
|
|
h, m = divmod(m, 60)
|
|
if h:
|
|
elapsed = ' in %dh %02dm %02ds' % (h, m, s)
|
|
elif m:
|
|
elapsed = ' in %dm %02ds' % (m, s)
|
|
else:
|
|
elapsed = ' in %ds' % (s)
|
|
else:
|
|
elapsed = ''
|
|
name = ''
|
|
if config.has_option('zuul', 'job_name_in_report'):
|
|
if config.getboolean('zuul',
|
|
'job_name_in_report'):
|
|
name = job.name + ' '
|
|
ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
|
|
voting)
|
|
return ret
|