Skip to content

Commit

Permalink
Merge pull request #3744 from tardyp/badges
Browse files Browse the repository at this point in the history
add badges plugin
  • Loading branch information
tardyp committed Nov 24, 2017
2 parents fcb74ce + 15bbcb2 commit 294ed3a
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -8,7 +8,7 @@ VENV_NAME:=.venv$(VENV_PY_VERSION)
PIP?=$(VENV_NAME)/bin/pip
VENV_PY_VERSION?=python

WWW_PKGS := pkg www/base www/console_view www/grid_view www/waterfall_view www/wsgi_dashboards
WWW_PKGS := pkg www/base www/console_view www/grid_view www/waterfall_view www/wsgi_dashboards www/badges
WWW_EX_PKGS := nestedexample codeparameter
ALL_PKGS := master worker $(WWW_PKGS)

Expand Down
1 change: 1 addition & 0 deletions master/buildbot/newsfragments/badges.feature
@@ -0,0 +1 @@
new :ref`badges` plugin which reimplement the buildbot eight png badge system.
3 changes: 2 additions & 1 deletion master/buildbot/www/plugin.py
Expand Up @@ -25,14 +25,15 @@

class Application(object):

def __init__(self, modulename, description):
def __init__(self, modulename, description, ui=True):
self.description = description
self.version = pkg_resources.resource_string(
modulename, "/VERSION").strip()
self.version = bytes2NativeString(self.version)
self.static_dir = pkg_resources.resource_filename(
modulename, "/static")
self.resource = static.File(self.static_dir)
self.ui = ui

def setMaster(self, master):
self.master = master
Expand Down
6 changes: 4 additions & 2 deletions master/buildbot/www/service.py
Expand Up @@ -267,7 +267,8 @@ def setupSite(self, new_config):
raise RuntimeError("could not find buildbot-www; is it installed?")

root = self.apps.get('base').resource
for key, plugin in iteritems(new_config.www.get('plugins', {})):
known_plugins = set(new_config.www.get('plugins', {})) | set(['base'])
for key, plugin in list(iteritems(new_config.www.get('plugins', {}))):
log.msg("initializing www plugin %r" % (key,))
if key not in self.apps:
raise RuntimeError(
Expand All @@ -276,7 +277,8 @@ def setupSite(self, new_config):
app.setMaster(self.master)
app.setConfiguration(plugin)
root.putChild(unicode2bytes(key), app.resource)
known_plugins = set(new_config.www.get('plugins', {})) | set(['base'])
if not app.ui:
del new_config.www['plugins'][key]
for plugin_name in set(self.apps.names) - known_plugins:
log.msg("NOTE: www plugin %r is installed but not "
"configured" % (plugin_name,))
Expand Down
4 changes: 4 additions & 0 deletions master/docs/developer/www-base-app.rst
Expand Up @@ -83,6 +83,10 @@ The entrypoint containing a Resource, nothing forbids plugin writers to add more
For that, a reference to the master singleton is provided in ``master`` attribute of the Application entrypoint.
You are even not restricted to twisted, and could even `load a wsgi application using flask, django, etc <http://twistedmatrix.com/documents/13.1.0/web/howto/web-in-60/wsgi.html>`_.

It is also possible to make a web plugin which only adds http endpoint, and has no javascript UI.
For that the ``Application`` endpoint object should have ``ui=False`` argument.
You can look at the :src:`www/badges` plugin for an example of a ui-less plugin.

.. _Routing:

Routing
Expand Down
55 changes: 55 additions & 0 deletions master/docs/manual/cfg-www.rst
Expand Up @@ -227,6 +227,61 @@ This feature is similar to the one in the builder list.
'plugins': {'grid_view': True}
}
.. _Badges:

Badges
++++++

Buildbot badges plugin produces an image in SVG or PNG format with information about the last build for the given builder name.
PNG generation is based on the CAIRO_ SVG engine, it requires a bit more CPU to generate.


.. code-block:: bash
pip install buildbot-badges
.. code-block:: python
c['www'] = {
'plugins': {'badges': {}}
}
You can the access your builder's badges using urls like ``http://<buildbotURL>/badges/<buildername>.svg``.
The default templates are very much configurable via the following options.

.. code-block:: python
{
"left_text": "Build Status", # text on the left part of the image
"left_color": "#555", # color of the left part of the image
"style": "flat", # style of the template availables are "flat", "flat-square", "plastic"
"template_name": "{style}.svg.j2", # name of the template
"font_face": "DejaVu Sans",
"font_size": 11,
"color_scheme": { # color to be used for right part of the image
"exception": "#007ec6", # blue
"failure": "#e05d44", # red
"retry": "#007ec6", # blue
"skipped": "a4a61d", # yellowgreen
"success": "#4c1", # brightgreen
"unknown": "#9f9f9f", # lightgrey
"warnings": "#dfb317" # yellow
}
}
Those options can be configured either using the plugin configuration:

.. code-block:: python
c['www'] = {
'plugins': {'badges': {"left_color": "#222"}}
}
Or via the URL arguments like ``http://<buildbotURL>/badges/<buildername>.svg?left_color=222``.
Custom templates can also be specified in a ``template`` directory nearby the ``master.cfg``.

.. _CAIRO: https://www.cairographics.org/

.. _Web-Authentication:

Authentication plugins
Expand Down
2 changes: 1 addition & 1 deletion smokes/master.cfg
Expand Up @@ -113,7 +113,7 @@ c['buildbotURL'] = "http://localhost:8010/"
# minimalistic config to activate new web UI
c['www'] = dict(port=8010,
change_hook_dialects={'base': True},
plugins=dict(waterfall_view={}, console_view={}, grid_view={}))
plugins=dict(waterfall_view={}, console_view={}, grid_view={}, badges={}))

c['buildbotNetUsageData'] = None
####### DB URL
Expand Down
141 changes: 141 additions & 0 deletions www/badges/buildbot_badges/__init__.py
@@ -0,0 +1,141 @@
# This file is part of Buildbot. Buildbot is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members

from __future__ import absolute_import
from __future__ import print_function

import cairocffi as cairo
import cairosvg

import jinja2
from klein import Klein
from twisted.internet import defer

from buildbot.data import resultspec
from buildbot.process.results import Results
from buildbot.www.plugin import Application
from xml.sax.saxutils import escape


class Api(object):
app = Klein()

default = { # note that these defaults are documented in cfg-www.rst
"left_text": "Build Status",
"left_color": "#555",
"style": "plastic",
"template_name": "{style}.svg.j2",
"font_face": "DejaVu Sans",
"font_size": 11,
"color_scheme": {
"exception": "#007ec6", # blue
"failure": "#e05d44", # red
"retry": "#007ec6", # blue
"skipped": "a4a61d", # yellowgreen
"success": "#4c1", # brightgreen
"unknown": "#9f9f9f", # lightgrey
"warnings": "#dfb317" # yellow
}
}

def __init__(self, ep):
self.ep = ep
self.env = jinja2.Environment(loader=jinja2.ChoiceLoader([
jinja2.PackageLoader('buildbot_badges'),
jinja2.FileSystemLoader('templates')
]))

def makeConfiguration(self, request):

config = {}
config.update(self.default)
for k, v in self.ep.config.items():
if k == 'color_scheme':
config[k].update(v)
else:
config[k] = v

for k, v in request.args.items():
config[k] = escape(v[0])
return config

@app.route("/<string:builder>.png", methods=['GET'])
@defer.inlineCallbacks
def getPng(self, request, builder):
svg = yield self.getSvg(request, builder)
request.setHeader('content-type', 'image/png')
defer.returnValue(cairosvg.svg2png(svg))

@app.route("/<string:builder>.svg", methods=['GET'])
@defer.inlineCallbacks
def getSvg(self, request, builder):
config = self.makeConfiguration(request)
request.setHeader('content-type', 'image/svg+xml')
request.setHeader('cache-control', 'no-cache')

# get the last completed build for that builder using the data api
last_build = yield self.ep.master.data.get(
("builders", builder, "builds"),
limit=1, order=['-number'],
filters=[resultspec.Filter('complete', 'eq', [True])])

# get the status text corresponding to results code
results_txt = "unknown"
if last_build:
results = last_build[0]['results']
if results >= 0 and results < len(Results):
results_txt = Results[results]

svgdata = self.makesvg(results_txt, results_txt, left_text=config['left_text'], config=config)
defer.returnValue(svgdata)

def textwidth(self, text, config):
"""Calculates the width of the specified text.
"""
surface = cairo.SVGSurface(None, 1280, 200)
ctx = cairo.Context(surface)
ctx.select_font_face(config['font_face'],
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
ctx.set_font_size(int(config['font_size']))
return ctx.text_extents(text)[2] + 2

def makesvg(self, right_text, status=None, left_text=None,
left_color=None, config=None):
"""Renders an SVG from the template, using the specified data
"""
right_color = config['color_scheme'].get(status, "#9f9f9f") # Grey

left_text = left_text or config['left_text']
left_color = left_color or config['left_color']

left = {
"color": left_color,
"text": left_text,
"width": self.textwidth(left_text, config)
}
right = {
"color": right_color,
"text": right_text,
"width": self.textwidth(right_text, config)
}

template = self.env.get_template(config['template_name'].format(**config))
return template.render(left=left, right=right, config=config)


# create the interface for the setuptools entry point
ep = Application(__name__, "Buildbot badges", ui=False)
ep.resource = Api(ep).app.resource()
Empty file.
10 changes: 10 additions & 0 deletions www/badges/buildbot_badges/templates/flat-square.svg.j2
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="{{ left.width + right.width }}" height="20">
<rect width="{{ left.width + right.width }}" height="20" fill="{{ left.color }}"/>
<rect x="{{ left.width }}" width="{{ right.width }}" height="20" fill="{{ right.color }}"/>
<rect x="{{ left.width }}" width="4" height="20" fill="{{ right.color }}"/>
<rect width="{{ left.width + right.width }}" height="20" fill-opacity=".1"/>
<g fill="#fff" text-anchor="middle" font-family="{{config.font_face}},Verdana,Geneva,sans-serif" font-size="{{config.font_size}}">
<text x="{{ left.width/2+1 }}" y="14">{{ left.text }}</text>
<text x="{{ left.width+right.width/2-1 }}" y="14">{{ right.text }}</text>
</g>
</svg>
16 changes: 16 additions & 0 deletions www/badges/buildbot_badges/templates/flat.svg.j2
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="{{ left.width + right.width }}" height="20">
<linearGradient id="smooth" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<rect rx="3" width="{{ left.width + right.width }}" height="20" fill="{{ left.color }}"/>
<rect rx="3" x="{{ left.width }}" width="{{ right.width }}" height="20" fill="{{ right.color }}"/>
<rect x="{{ left.width }}" width="4" height="20" fill="{{ right.color }}"/>
<rect rx="3" width="{{ left.width + right.width }}" height="20" fill="url(#smooth)"/>
<g fill="#fff" text-anchor="middle" font-family="{{config.font_face}},Verdana,Geneva,sans-serif" font-size="{{config.font_size}}">
<text x="{{ left.width/2+1 }}" y="15" fill="#010101" fill-opacity=".3">{{ left.text }}</text>
<text x="{{ left.width/2+1 }}" y="14">{{ left.text }}</text>
<text x="{{ left.width+right.width/2-1 }}" y="15" fill="#010101" fill-opacity=".3">{{ right.text }}</text>
<text x="{{ left.width+right.width/2-1 }}" y="14">{{ right.text }}</text>
</g>
</svg>
18 changes: 18 additions & 0 deletions www/badges/buildbot_badges/templates/plastic.svg.j2
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="{{ left.width + right.width }}" height="18">
<linearGradient id="smooth" x2="0" y2="100%">
<stop offset="0" stop-color="#fff" stop-opacity=".7"/>
<stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
<stop offset=".9" stop-color="#000" stop-opacity=".3"/>
<stop offset="1" stop-color="#000" stop-opacity=".5"/>
</linearGradient>
<rect rx="4" width="{{ left.width + right.width }}" height="18" fill="{{ left.color }}"/>
<rect rx="4" x="{{ left.width }}" width="{{ right.width }}" height="18" fill="{{ right.color }}"/>
<rect x="{{ left.width }}" width="4" height="18" fill="{{ right.color }}"/>
<rect rx="4" width="{{ left.width+right.width }}" height="18" fill="url(#smooth)"/>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="{{ left.width/2+1 }}" y="14" fill="#010101" fill-opacity=".3">{{ left.text }}</text>
<text x="{{ left.width/2+1 }}" y="13">{{ left.text }}</text>
<text x="{{ left.width+right.width/2-1 }}" y="14" fill="#010101" fill-opacity=".3">{{ right.text }}</text>
<text x="{{ left.width+right.width/2-1 }}" y="13">{{ right.text }}</text>
</g>
</svg>
51 changes: 51 additions & 0 deletions www/badges/setup.py
@@ -0,0 +1,51 @@
#!/usr/bin/env python
#
# This file is part of Buildbot. Buildbot is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

try:
from buildbot_pkg import setup_www_plugin
except ImportError:
import sys
print("Please install buildbot_pkg module in order to install that package, or use the pre-build .whl modules available on pypi", file=sys.stderr)
sys.exit(1)

setup_www_plugin(
name='buildbot-badges',
description='Buildbot badges',
author=u'Buildbot Team Members',
author_email=u'users@buildbot.net',
url='http://buildbot.net/',
license='GNU GPL',
packages=['buildbot_badges'],
install_requires=[
'klein',
'CairoSVG==1.0.22', # cairoSVG 2+ is not py2 compatible
'cairocffi', 'Jinja2'
],
package_data={
'': [
'VERSION', 'templates/*.svg.j2'
],
},
entry_points="""
[buildbot.www]
badges = buildbot_badges:ep
""",
)

0 comments on commit 294ed3a

Please sign in to comment.