Skip to content

Instantly share code, notes, and snippets.

@lpereira
Last active December 18, 2016 20:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lpereira/9cb9b83222044afaafea86e99488a0a0 to your computer and use it in GitHub Desktop.
Save lpereira/9cb9b83222044afaafea86e99488a0a0 to your computer and use it in GitHub Desktop.
Buildbot shield generator
/*
* Buildbot 0.9.x shield generator
* Copyright (c) 2016 Leandro Pereira <leandro@tia.mat.br>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in the
* Software without restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package main
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/golang/freetype"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/image/math/fixed"
"io/ioutil"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"text/template"
"time"
)
var database *sql.DB
var badgeTemplate *template.Template
func fetch(url string) ([]byte, error) {
log.Printf("Fetching %s", url)
resp, err := http.Get(url)
if err != nil {
log.Printf("Error while fetching %s: %v", url, err)
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Printf("Error while reading %s body: %v", url, err)
return nil, err
}
log.Printf("Got %d bytes of response", len(body))
return body, nil
}
type BuildbotBuilds struct {
Builds []BuildbotBuildsBuild `json:"builds"`
}
type BuildbotBuildsBuild struct {
BuilderId int `json:"builderid"`
BuildId int `json:"buildid"`
BuildRequestId int `json:"buildrequestid"`
Complete bool `json:"complete"`
CompleteAt int64 `json:"complete_at"`
StartedAt int64 `json:"started_at"`
MasterId int `json:"masterid"`
Number int `json:"number"`
Results int `json:"results"`
WorkerId int `json:"workerid"`
StateString string `json:"state_string"`
}
func updateBuilderStatus(botId, builderName string, build BuildbotBuildsBuild) error {
stmt, err := database.Prepare("update builders set status=?, last_update=strftime('%s', 'now') where bot_id=? and name=?")
if err != nil {
return fmt.Errorf("Could not prepare status update stmt: %v", err)
}
defer stmt.Close()
_, err = stmt.Exec(resultsToString(build.Results), botId, builderName)
if err != nil {
return fmt.Errorf("Could not execute statement: %v", err)
}
return nil
}
func resultsToString(results int) string {
switch results {
case 0:
return "success"
case 1:
return "warnings"
case 2:
return "failure"
case 3:
return "skipped"
case 4:
return "exception"
case 5:
return "cancelled"
default:
return "unknown"
}
}
func getBuilderId(botId, builderName string) int {
stmt, err := database.Prepare("select id from builders where bot_id=? and name=?")
if err != nil {
log.Printf("Could not prepare statement: %v", err)
return -1
}
defer stmt.Close()
var id int
err = stmt.QueryRow(botId, builderName).Scan(&id)
if err != nil {
log.Printf("Could not query: %v", err)
return -1
}
return id
}
func updateStatus(botId, builderName string) (string, error) {
base := findBaseById(botId)
if base == "" {
return "", fmt.Errorf("Could not find bot ID: %s", botId)
}
b, err := fetch(endpoint(base, "/api/v2/builds?complete=true"))
if err != nil {
return "", fmt.Errorf("Could not fetch build status: %v", err)
}
builds := BuildbotBuilds{}
err = json.Unmarshal(b, &builds)
if err != nil {
return "", fmt.Errorf("Could not unmarshal JSON: %v", err)
}
builderId := getBuilderId(botId, builderName)
if builderId < 0 {
return "", fmt.Errorf("Could not find builder")
}
for _, build := range builds.Builds {
if build.BuilderId != builderId {
continue
}
err := updateBuilderStatus(botId, builderName, build)
if err != nil {
return "exception", err
}
return resultsToString(build.Results), nil
}
return "unknown", nil
}
var ftCtx *freetype.Context
func measureTextWidth(s string) int {
if ftCtx == nil {
fontData, err := ioutil.ReadFile("./DejaVuSans.ttf")
if err != nil {
panic(fmt.Sprintf("Could not load file: %v", err))
}
font, err := freetype.ParseFont(fontData)
if err != nil {
panic(fmt.Sprintf("Could not parse font: %v", err))
}
ftCtx = freetype.NewContext()
ftCtx.SetFont(font)
ftCtx.SetDPI(72)
ftCtx.SetFontSize(11)
}
size, err := ftCtx.DrawString(s, fixed.Point26_6{0, 0})
if err != nil {
log.Printf("Could not draw string using font: %v", err)
return 6 * len(s)
}
return size.X.Round()
}
func writeBadge(w http.ResponseWriter, label, status string) {
type Badge struct {
Label, Status, Color string
Width, HalfWidth int
}
w.Header().Set("Content-Type", "image/svg+xml")
var color string
switch status {
case "success":
color = "#4c1"
case "warnings":
color = "#f40"
case "failure":
color = "#c41"
case "skipped":
color = "#14c"
case "exception":
color = "#c1c"
case "cancelled":
color = "#4cc"
default:
color = "#cc1"
}
labelWidth := measureTextWidth(label)
width := labelWidth + measureTextWidth(status) + 4*2*2
halfWidth := labelWidth + 4*2
badgeTemplate.Execute(w, Badge{label, status, color, width, halfWidth})
}
func parseTimeUnix(s string) time.Time {
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
val = 0
}
return time.Unix(val, 0)
}
func imgHandler(w http.ResponseWriter, r *http.Request) {
elements := strings.Split(r.URL.Path, "/")
if len(elements) != 4 {
writeBadge(w, "error", "invalid-request")
return
}
botId := elements[2]
if len(botId) != 6 {
writeBadge(w, "error", "invalid-bot")
return
}
builderName := elements[3]
stmt, err := database.Prepare("select status,last_update from builders where bot_id=? and name=?")
if err != nil {
writeBadge(w, "error", "internal-stmt")
log.Printf("Could not prepare stmt: %v", err)
return
}
defer stmt.Close()
var status string
var lastUpdateStr string
err = stmt.QueryRow(botId, builderName).Scan(&status, &lastUpdateStr)
if err != nil {
writeBadge(w, "error", "query")
log.Printf("Could not query: %v", err)
return
}
lastUpdate := parseTimeUnix(lastUpdateStr)
if lastUpdate.IsZero() || time.Now().Sub(lastUpdate) > 30*time.Minute {
status, err = updateStatus(botId, builderName)
if err != nil {
writeBadge(w, "error", "update")
log.Printf("Couldn't update status for builder <%s,%s>: %v", botId, builderName, err)
}
if lastUpdate.IsZero() {
status = "unknown"
}
} else {
log.Printf("Status <%s,%s> still fresh <%v>", botId, builderName, time.Now().Sub(lastUpdate))
}
writeBadge(w, builderName, status)
}
type RegResponse struct {
Id string `json:"id"`
Error string `json:"error"`
}
type BuildbotBuilders struct {
Builders []BuildbotBuildersBuilder `json:"builders"`
}
type BuildbotBuildersBuilder struct {
BuilderId int `json:"builderid"`
Description string `json:"description"`
Name string `json:"name"`
}
func findIdByBase(base string) string {
stmt, err := database.Prepare("select id from bots where base=?")
if err != nil {
log.Printf("Could not prepare SQL: %v", err)
return ""
}
defer stmt.Close()
var id string
err = stmt.QueryRow(base).Scan(&id)
if err != nil {
log.Printf("Could not query SQL db: %v", err)
return ""
}
return id
}
func findBaseById(id string) string {
stmt, err := database.Prepare("select base from bots where id=?")
if err != nil {
log.Printf("Could not prepare SQL: %v", err)
return ""
}
defer stmt.Close()
var base string
err = stmt.QueryRow(id).Scan(&base)
if err != nil {
log.Printf("Could not query SQL db: %v", err)
return ""
}
return base
}
func endpoint(url, endpoint string) string {
return strings.TrimSuffix(url, "/") + endpoint
}
func regHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
base := r.FormValue("base")
if base == "" {
encoder.Encode(RegResponse{"", "invalid-api-use"})
return
}
baseId := findIdByBase(base)
if baseId != "" {
encoder.Encode(RegResponse{baseId, "success"})
return
}
b, err := fetch(endpoint(base, "/api/v2/builders"))
if err != nil {
encoder.Encode(RegResponse{"", "not-buildbot-v2"})
return
}
builders := BuildbotBuilders{}
err = json.Unmarshal(b, &builders)
if err != nil {
encoder.Encode(RegResponse{"", "not-buildbot-v2"})
return
}
id, err := registerBase(base, builders)
if err != nil {
encoder.Encode(RegResponse{"", "could-not-register"})
return
}
encoder.Encode(RegResponse{id, "success"})
}
func registerBase(base string, builders BuildbotBuilders) (string, error) {
for {
id := generateNewId()
tx, err := database.Begin()
if err != nil {
log.Printf("Could not create transaction: %v", err)
return "", err
}
botStmt, err := tx.Prepare("insert into bots(id, base) values (?, ?)")
if err != nil {
log.Printf("Could not prepare bot insertion stmt: %v", err)
return "", err
}
defer botStmt.Close()
_, err = botStmt.Exec(id, base)
if err != nil {
log.Printf("Could not register %s: %v", id, err)
continue
}
builderStmt, err := tx.Prepare("insert into builders(bot_id, name, id) values (?, ?, ?)")
if err != nil {
log.Printf("Could not prepare builder insertion stmt: %v", err)
return "", err
}
defer builderStmt.Close()
for _, builder := range builders.Builders {
_, err = builderStmt.Exec(id, builder.Name, builder.BuilderId)
if err != nil {
log.Printf("Could not insert builder: %v", err)
return "", err
}
}
tx.Commit()
return id, nil
}
}
var runes = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func generateNewId() string {
b := make([]rune, 6)
for i := range b {
b[i] = runes[rand.Intn(len(runes))]
}
return string(b)
}
func main() {
badgeTemplate = template.Must(template.New("badge").Parse(`<svg xmlns="http://www.w3.org/2000/svg" width="{{.Width}}" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="a">
<rect width="{{.Width}}" height="20" rx="3" fill="#fff"/>
</mask>
<g mask="url(#a)">
<path fill="#555" d="M0 0h{{.HalfWidth}}v20H0z"/>
<path fill="{{.Color}}" d="M{{.HalfWidth}} 0h{{.Width}}v20H{{.HalfWidth}}z"/>
<path fill="url(#b)" d="M0 0h{{.Width}}v20H0z"/>
</g>
<g fill="#fff" text-anchor="left" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="4" y="15" fill="#010101" fill-opacity=".3">{{.Label}}</text>
<text x="4" y="14">{{.Label}}</text>
<text x="{{.HalfWidth}}" dx="4" y="15" fill="#010101" fill-opacity=".3">{{.Status}}</text>
<text x="{{.HalfWidth}}" dx="4" y="14">{{.Status}}</text>
</g>
</svg>`))
log.Printf("Initializing random seed")
rand.Seed(time.Now().UnixNano())
log.Printf("Opening database")
var err error
database, err = sql.Open("sqlite3", "./bbshield.db")
if err != nil {
log.Fatal(err)
}
defer database.Close()
err = database.Ping()
if err != nil {
log.Fatal(err)
}
log.Printf("Registering handlers")
http.HandleFunc("/reg", regHandler)
http.HandleFunc("/img/", imgHandler)
http.Handle("/", http.FileServer(http.Dir("./public")))
log.Printf("Listening")
http.ListenAndServe("127.0.0.1:9999", nil)
}
CREATE TABLE bots (id string unique primary key, base string);
CREATE TABLE builders (bot_id string, name string, last_update timedate default 0, status string default 'unknown', id int, foreign key(bot_id) references bots(id));
<html> <head> <title>Buildbot 0.9.x status shield service</title>
</head>
<body> <h1>Buildbot 0.9.x status shield service</h1> <h2>API</h2>
<h3>Register</h3> <blockquote> <pre>$ curl https://shield.lwan.ws/reg?base=https://buildbot-www-instance/</pre>
</blockquote>
<p>Response is a JSON object with two string fields: <tt>id</tt> and <tt>error</tt>. If <tt>error</tt> is <tt>"success"</tt>, the
value of the <tt>id</tt> can be used with the <tt>/img</tt> endpoint.</p> <p>Please register just once. Registering multiple times for the
same buildbot instance will return the same ID.</p>
<h3>Generating shields</h3>
<p>Just obtain <tt>https://shield.lwan.ws/img/<b>$id</b>/<b>$builder-name</b></tt>,
where <tt>builder-name</tt> is the builder name that appears in the
Buildbot dashboard.</p> <p>The output is an SVG file. Example: <img
src="https://shield.lwan.ws/img/gycKbr/release">.</p>
<p>Status will be cached for half an hour. If expired, the service
will fetch the status. There's no way to purge the cache or force
the status to be re-fetched other than waiting.</p>
<h2>Credits &amp; usage license</h2>
<p><a href="https://github.com/lpereira">@lpereira</a> wrote and
maintains the service. This service can be used by any project that
is maintained by an individual or non-profit organization to serve
Free and Open Source projects.</p>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment