Commit 41601d31 authored by zauberstuhl's avatar zauberstuhl

Merge branch 'badge_support' into 'master'

Add badge support for single projects

See merge request feneas/federation-testsuite-server!3
parents 455b6994 b718c2ef
Pipeline #766 passed with stage
in 1 minute and 29 seconds
......@@ -9,8 +9,8 @@ RUN mkdir -p $GI_DIR
COPY . $GI_DIR
WORKDIR $GI_DIR
RUN go get ./... && go build
RUN mv github-integration /usr/local/bin/github-integration
RUN go get ./... && go build -o server
RUN mv server /usr/local/bin/server
RUN mv templates /home/user && \
chown -R user:user /home/user/templates
RUN rm -rv $GOPATH/src/*
......@@ -20,4 +20,4 @@ WORKDIR /home/user
EXPOSE 8181
ENTRYPOINT ["/usr/local/bin/github-integration"]
ENTRYPOINT ["/usr/local/bin/server"]
//
// Federation Testsuite Server
// Copyright (C) 2018 Lukas Matt <lukas@zauberstuhl.de>
//
// This program 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, either version 3 of the License, or
// any later version.
//
// 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, see <http://www.gnu.org/licenses/>.
//
package main
import (
"bytes"
"text/template"
"net/http"
"regexp"
"strings"
"fmt"
)
type BadgeColor string
var (
BadgeBrightGreen BadgeColor = "#44cc11"
BadgeGreen BadgeColor = "#97ca00"
BadgeYellow BadgeColor = "#dfb317"
BadgeOrange BadgeColor = "#fe7d37"
BadgeRed BadgeColor = "#e05d44"
)
// badgeTemplate was copied from https://github.com/imsky/covbadger
// see https://github.com/imsky/covbadger/blob/master/LICENSE
var badgeTemplate string = `<svg xmlns="http://www.w3.org/2000/svg" width="156" height="20">
<title>Success/Failed/Skipped</title>
<desc>Generated with the Federation Testsuite</desc>
<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="156" height="20" fill="#555" />
<rect rx="3" x="100" width="56" height="20" fill="{{.Color}}" />
<rect x="100" width="4" height="20" fill="{{.Color}}" />
<rect rx="3" width="156" height="20" fill="url(#smooth)" />
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,sans-serif" font-size="11">
<text x="50" y="15" fill="#010101" fill-opacity=".3">federation tests</text>
<text x="50" y="14">federation tests</text>
<text x="128" y="15" fill="#010101" fill-opacity=".3">{{.Desc}}</text>
<text x="128" y="14">{{.Desc}}</text>
</g>
</svg>`
func reportsBadge(fqdn, slug string, w http.ResponseWriter, r *http.Request) {
cacheBadgeKey := fmt.Sprintf("images-stats-%s-%s", fqdn, slug)
svg, found := cache.Get(cacheBadgeKey); if !found {
var project Project
err := project.FindByFQDNAndSlug("https://" + fqdn, slug)
if err != nil {
logger.Println(err)
http.NotFound(w, r)
return
}
var build Build
err = build.FindLastByProjectID(project.ID)
if err != nil {
logger.Println(err)
http.NotFound(w, r)
return
}
reports, err := build.GetReport()
if err != nil {
logger.Println(err)
http.NotFound(w, r)
return
}
var success, fail, skip int
reSkip := regexp.MustCompile(`^ok.*#\sskip`)
reOk := regexp.MustCompile(`^ok`)
reNot := regexp.MustCompile(`^not`)
for _, report := range reports {
for _, line := range strings.Split(report, "\n") {
if reSkip.MatchString(line) {
skip += 1
continue
}
if reOk.MatchString(line) {
success += 1
continue
}
if reNot.MatchString(line) {
fail += 1
continue
}
}
}
var color = BadgeRed
if fail == 0 {
if skip == 0 && success == 0 {
color = BadgeGreen
} else if skip == 0 {
color = BadgeBrightGreen
} else if skip < success {
color = BadgeYellow
} else {
color = BadgeOrange
}
}
var buffer bytes.Buffer
badgeTemplate, err := template.New("badge").Parse(badgeTemplate)
if err != nil {
logger.Println(err)
http.NotFound(w, r)
return
}
badge := struct {
Desc string
Color BadgeColor
}{
Desc: fmt.Sprintf("%d/%d/%d", success, fail, skip),
Color: color,
}
err = badgeTemplate.Execute(&buffer, &badge)
if err != nil {
logger.Println(err)
http.NotFound(w, r)
return
}
svg = buffer.String()
SetCache(cacheBadgeKey, svg)
}
svgString, ok := svg.(string); if !ok {
logger.Printf("Cannot type cast to string svg=%+v\n", svg)
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "image/svg+xml")
fmt.Fprintf(w, svgString)
}
......@@ -118,25 +118,63 @@ func (build *Build) Pipeline() error {
return nil
}
func (build *Build) GetStatus() (BuildStatus, error) {
resp, err := test_server_request("GET", fmt.Sprintf(
"/projects/%d/pipelines/%d", confd.IntDefault("gitlab.project.id", 102),
build.PipelineID), url.Values{})
func (build *Build) get(method, path string, values url.Values) (raw []byte, err error) {
resp, err := test_server_request(method, path, values)
if err != nil {
logger.Printf("#%d: Cannot fetch build status: %+v\n", build.ID, err)
return STATUS_PENDING, err
return raw, err
}
defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body)
raw, err = ioutil.ReadAll(resp.Body)
if err != nil {
logger.Printf("#%d: Cannot read status body: %+v\n", build.ID, err)
return STATUS_PENDING, err
return raw, err
}
if resp.StatusCode >= 300 {
// something went wrong
return STATUS_PENDING, errors.New(string(raw))
return raw, errors.New(string(raw))
}
return raw, nil
}
func (build *Build) GetReport() (reports []string, err error) {
jobs := []struct {ID int `json:"id"`}{}
raw, err := build.get("GET", fmt.Sprintf("/projects/%d/pipelines/%d/jobs",
confd.IntDefault("gitlab.project.id", 102), build.PipelineID), url.Values{})
if err != nil {
logger.Printf("#%d: Cannot fetch jobs: %+v\n", build.ID, err)
return reports, err
}
err = json.Unmarshal(raw, &jobs)
if err != nil {
logger.Printf("#%d: Cannot unmarshal body: %+v <> %s\n",
build.ID, err, string(raw))
return reports, err
}
for _, job := range jobs {
raw, err := build.get("GET", fmt.Sprintf(
"/projects/%d/jobs/%d/artifacts/report.tap",
confd.IntDefault("gitlab.project.id", 102), job.ID), url.Values{})
if err != nil {
logger.Printf("#%d: Cannot fetch artifacts: %+v\n", build.ID, err)
continue
}
reports = append(reports, string(raw))
}
return
}
func (build *Build) GetStatus() (BuildStatus, error) {
raw, err := build.get("GET", fmt.Sprintf("/projects/%d/pipelines/%d",
confd.IntDefault("gitlab.project.id", 102), build.PipelineID), url.Values{})
if err != nil {
logger.Printf("#%d: Cannot fetch build status: %+v\n", build.ID, err)
return STATUS_PENDING, err
}
status := struct {Status string `json:"status"`}{}
......
......@@ -78,7 +78,7 @@ func buildsPNG(w http.ResponseWriter, r *http.Request) {
// sort
var keys sortTime
for key, _ := range statesPerDay {
for key := range statesPerDay {
keys = append(keys, key)
}
sort.Sort(keys)
......
......@@ -38,6 +38,9 @@ func render(w http.ResponseWriter, name string, s interface{}) {
rootTmpl := template.New("").Funcs(template.FuncMap{
"add": add,
"len": length,
"removeHTTP": func(s string) string {
return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://")
},
})
tmpl, err := rootTmpl.ParseFiles(
......@@ -162,3 +165,21 @@ func authGithubPage(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, url, http.StatusMovedPermanently)
}
}
func generatePictures(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/images/stats/")
pathParts := strings.Split(path, "/")
if len(pathParts) == 1 {
if pathParts[0] == "builds.png" {
buildsPNG(w, r)
return
}
} else if len(pathParts) > 2 {
slug := strings.Join(pathParts[1:], "/")
slug = strings.TrimSuffix(slug, ".svg")
reportsBadge(pathParts[0], slug, w, r)
return
}
http.NotFound(w, r)
}
......@@ -22,11 +22,12 @@ import (
"golang.org/x/oauth2"
oauth2Github "golang.org/x/oauth2/github"
"github.com/revel/config"
gocache "github.com/patrickmn/go-cache"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"math/rand"
"time"
"os"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
var (
......@@ -34,6 +35,7 @@ var (
confd *config.Context
devMode = false
cache = gocache.New(5*time.Minute, 10*time.Minute)
logger = log.New(os.Stdout, "", log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
......@@ -84,6 +86,10 @@ func OpenDatabase() (*gorm.DB, error) {
confd.StringDefault("database.dsn", "./server.db"))
}
func SetCache(key string, val interface{}) {
cache.Set(key, val, gocache.DefaultExpiration)
}
func Secret(n int) string {
b := make([]rune, n)
for i := range b {
......
......@@ -110,6 +110,16 @@ func (project *Project) CreateOrUpdate() error {
return err
}
func (project *Project) FindByFQDNAndSlug(fqdn, slug string) error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Where("fqdn = ? and slug = ?", fqdn, slug).Find(project).Error
}
// build table
type Build struct {
......@@ -142,3 +152,13 @@ func (build *Build) Save() error {
return db.Save(build).Error
}
func (build *Build) FindLastByProjectID(id uint) error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Where("project_id = ?", id).First(build).Order("id desc").Error
}
......@@ -20,12 +20,41 @@ package main
import (
"flag"
"net/http"
"regexp"
)
func init() {
flag.StringVar(&configDir, "config-dir", "./", "Where is config.conf located?")
}
type route struct {
pattern *regexp.Regexp
handler http.Handler
}
type RegexpHandler struct {
routes []*route
}
func (h *RegexpHandler) Handler(pattern *regexp.Regexp, handler http.Handler) {
h.routes = append(h.routes, &route{pattern, handler})
}
func (h *RegexpHandler) HandleFunc(pattern *regexp.Regexp, handler func(http.ResponseWriter, *http.Request)) {
h.routes = append(h.routes, &route{pattern, http.HandlerFunc(handler)})
}
func (h RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, route := range h.routes {
if route.pattern.MatchString(r.URL.Path) {
route.handler.ServeHTTP(w, r)
return
}
}
// no pattern matched; send 404 response
http.NotFound(w, r)
}
func main() {
flag.Parse()
LoadConfig()
......@@ -35,17 +64,18 @@ func main() {
go BuildAgent()
}
http.HandleFunc("/", indexPage)
http.HandleFunc("/images/stats/builds.png", buildsPNG)
http.HandleFunc("/auth/github", authGithubPage)
http.HandleFunc("/auth/gitlab", authGitlabPage)
http.HandleFunc("/result", resultPage)
http.HandleFunc("/hook", webhook)
handler := RegexpHandler{}
handler.HandleFunc(regexp.MustCompile(`^/[^/]*$`), indexPage)
handler.HandleFunc(regexp.MustCompile(`^/images/stats/`), generatePictures)
handler.HandleFunc(regexp.MustCompile("^/auth/github"), authGithubPage)
handler.HandleFunc(regexp.MustCompile("^/auth/gitlab"), authGitlabPage)
handler.HandleFunc(regexp.MustCompile("^/result"), resultPage)
handler.HandleFunc(regexp.MustCompile("^/hook"), webhook)
logMsg := "Running webserver on :8181"
if devMode {
logMsg = "[DEV MODE] " + logMsg
}
logger.Println(logMsg)
logger.Println(http.ListenAndServe(":8181", nil))
logger.Println(http.ListenAndServe(":8181", handler))
}
......@@ -15,6 +15,7 @@
<tr>
<th scope="col">#</th>
<th scope="col">Project</th>
<th scope="col">Badge</th>
<th scope="col">Repository</th>
</tr>
</thead>
......@@ -23,6 +24,11 @@
<tr>
<th scope="row">{{add $i 1}}</th>
<td>{{$project.Name}}</td>
<td>
<a href="/images/stats/{{removeHTTP $project.FQDN}}/{{$project.Slug}}.svg">
<img src="/images/stats/{{removeHTTP $project.FQDN}}/{{$project.Slug}}.svg" alt="Build Status">
</a>
</td>
<td>
<a href="{{$project.FQDN}}/{{$project.Slug}}">{{$project.Slug}}</a>
</td>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment