|
/* |
|
* 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) |
|
} |