440 lines
12 KiB
Go
440 lines
12 KiB
Go
package main
|
|
|
|
/*
|
|
* This file is part of Autogits.
|
|
*
|
|
* Copyright © 2024 SUSE LLC
|
|
*
|
|
* Autogits 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 2 of the License, or (at your option) any later
|
|
* version.
|
|
*
|
|
* Autogits 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
|
|
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"flag"
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
"log"
|
|
"maps"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"src.opensuse.org/autogits/common"
|
|
)
|
|
|
|
const (
|
|
AppName = "obs-status-service"
|
|
)
|
|
|
|
var obs *common.ObsClient
|
|
|
|
type RepoBuildCounters struct {
|
|
Repository, Arch string
|
|
Status string
|
|
BuildStatusCounter map[string]int
|
|
}
|
|
|
|
func ProjectStatusSummarySvg(res []*common.BuildResult) []byte {
|
|
if len(res) == 0 {
|
|
return nil
|
|
}
|
|
|
|
list := common.BuildResultList{
|
|
Result: res,
|
|
}
|
|
package_names := list.GetPackageList()
|
|
maxLen := 0
|
|
for _, p := range package_names {
|
|
maxLen = max(maxLen, len(p))
|
|
}
|
|
|
|
// width := float32(len(list.Result))*1.5 + float32(maxLen)*0.8
|
|
// height := 1.5*float32(maxLen) + 30
|
|
ret := NewSvg(SvgType_Project)
|
|
|
|
status := make([]RepoBuildCounters, len(res))
|
|
|
|
for i, repo := range res {
|
|
status[i].Arch = repo.Arch
|
|
status[i].Repository = repo.Repository
|
|
status[i].Status = repo.Code
|
|
status[i].BuildStatusCounter = make(map[string]int)
|
|
|
|
for _, pkg := range repo.Status {
|
|
status[i].BuildStatusCounter[pkg.Code]++
|
|
}
|
|
}
|
|
slices.SortFunc(status, func(a, b RepoBuildCounters) int {
|
|
if r := strings.Compare(a.Repository, b.Repository); r != 0 {
|
|
return r
|
|
}
|
|
return strings.Compare(a.Arch, b.Arch)
|
|
})
|
|
repoName := ""
|
|
ret.ypos = 3.0
|
|
for _, repo := range status {
|
|
if repo.Repository != repoName {
|
|
repoName = repo.Repository
|
|
ret.WriteTitle(repoName)
|
|
}
|
|
|
|
ret.WriteSubtitle(repo.Arch)
|
|
statuses := slices.Sorted(maps.Keys(repo.BuildStatusCounter))
|
|
for _, status := range statuses {
|
|
ret.WriteProjectStatus(res[0].Project, repo.Repository, repo.Arch, status, repo.BuildStatusCounter[status])
|
|
}
|
|
}
|
|
|
|
return ret.GenerateSvg()
|
|
}
|
|
|
|
func LinkToBuildlog(R *common.BuildResult, S *common.PackageBuildStatus) string {
|
|
if R != nil && S != nil {
|
|
switch S.Code {
|
|
case "succeeded", "failed", "building":
|
|
return "/buildlog/" + url.PathEscape(R.Project) + "/" + url.PathEscape(S.Package) + "/" + url.PathEscape(R.Repository) + "/" + url.PathEscape(R.Arch)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func DeleteExceptPkg(pkg string) func(*common.PackageBuildStatus) bool {
|
|
return func(item *common.PackageBuildStatus) bool {
|
|
multibuild_prefix := pkg + ":"
|
|
return item.Package != pkg && !strings.HasPrefix(item.Package, multibuild_prefix)
|
|
}
|
|
}
|
|
|
|
func PackageStatusSummarySvg(pkg string, res []*common.BuildResult) []byte {
|
|
// per repo, per arch status bins
|
|
repo_names := []string{}
|
|
package_names := []string{}
|
|
multibuild_prefix := pkg + ":"
|
|
for _, r := range res {
|
|
if pos, found := slices.BinarySearchFunc(repo_names, r.Repository, strings.Compare); !found {
|
|
repo_names = slices.Insert(repo_names, pos, r.Repository)
|
|
}
|
|
|
|
for _, p := range r.Status {
|
|
if p.Package == pkg || strings.HasPrefix(p.Package, multibuild_prefix) {
|
|
if pos, found := slices.BinarySearchFunc(package_names, p.Package, strings.Compare); !found {
|
|
package_names = slices.Insert(package_names, pos, p.Package)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ret := NewSvg(SvgType_Package)
|
|
for _, pkg = range package_names {
|
|
// if len(package_names) > 1 {
|
|
ret.WriteTitle(pkg)
|
|
// }
|
|
|
|
for _, name := range repo_names {
|
|
ret.WriteSubtitle(name)
|
|
// print all repo arches here and build results
|
|
for _, r := range res {
|
|
if r.Repository != name {
|
|
continue
|
|
}
|
|
|
|
for _, s := range r.Status {
|
|
if s.Package == pkg {
|
|
link := LinkToBuildlog(r, s)
|
|
ret.WritePackageStatus(link, r.Arch, s.Code, s.Details)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret.GenerateSvg()
|
|
}
|
|
|
|
func BuildStatusSvg(repo *common.BuildResult, status *common.PackageBuildStatus) []byte {
|
|
buildStatus, ok := common.ObsBuildStatusDetails[status.Code]
|
|
if !ok {
|
|
buildStatus = common.ObsBuildStatusDetails["error"]
|
|
}
|
|
fillColor := "#480" // orange
|
|
textColor := "#888"
|
|
if buildStatus.Finished {
|
|
textColor = "#fff"
|
|
|
|
if buildStatus.Success {
|
|
fillColor = "#080"
|
|
} else {
|
|
fillColor = "#800"
|
|
}
|
|
}
|
|
|
|
buildlog := LinkToBuildlog(repo, status)
|
|
startTag := ""
|
|
endTag := ""
|
|
|
|
if len(buildlog) > 0 {
|
|
startTag = "<a href=\"" + buildlog + "\">"
|
|
endTag = "</a>"
|
|
}
|
|
|
|
return []byte(`<svg version="2.0" width="8em" height="1.5em" xmlns="http://www.w3.org/2000/svg">` +
|
|
`<rect width="100%" height="100%" fill="` + fillColor + `"/>` + startTag +
|
|
`<text x="4em" y="1.1em" text-anchor="middle" fill="` + textColor + `">` + html.EscapeString(buildStatus.Code) + `</text>` + endTag + `</svg>`)
|
|
}
|
|
|
|
func WriteJson(data any, res http.ResponseWriter) {
|
|
if jsonArray, err := json.MarshalIndent(data, "", " "); err != nil {
|
|
res.WriteHeader(500)
|
|
} else {
|
|
res.Header().Add("size", fmt.Sprint(len(jsonArray)))
|
|
res.Write(jsonArray)
|
|
}
|
|
}
|
|
|
|
func WriteXml(data any, res http.ResponseWriter) {
|
|
if xmlData, err := xml.MarshalIndent(data, "", " "); err != nil {
|
|
res.WriteHeader(500)
|
|
} else {
|
|
res.Header().Add("size", fmt.Sprint(len(xmlData)))
|
|
res.Write([]byte("<resultlist>"))
|
|
res.Write(xmlData)
|
|
res.Write([]byte("</resultlist>"))
|
|
}
|
|
}
|
|
|
|
var ObsUrl *string
|
|
|
|
func main() {
|
|
obsUrlDef := os.Getenv("OBS_STATUS_SERVICE_OBS_URL")
|
|
if len(obsUrlDef) == 0 {
|
|
obsUrlDef = "https://build.opensuse.org"
|
|
}
|
|
listenDef := os.Getenv("OBS_STATUS_SERVICE_LISTEN")
|
|
if len(listenDef) == 0 {
|
|
listenDef = "[::1]:8080"
|
|
}
|
|
certDef := os.Getenv("OBS_STATUS_SERVICE_CERT")
|
|
if len(certDef) == 0 {
|
|
certDef = "/run/obs-status-service.pem"
|
|
}
|
|
keyDef := os.Getenv("OBS_STATUS_SERVICE_KEY")
|
|
if len(keyDef) == 0 {
|
|
keyDef = certDef
|
|
}
|
|
|
|
cert := flag.String("cert-file", certDef, "TLS certificates file")
|
|
key := flag.String("key-file", keyDef, "Private key for the TLS certificate")
|
|
listen := flag.String("listen", listenDef, "Listening string")
|
|
disableTls := flag.Bool("no-tls", false, "Disable TLS")
|
|
ObsUrl = flag.String("obs-url", obsUrlDef, "OBS API endpoint for package buildlog information")
|
|
debug := flag.Bool("debug", false, "Enable debug logging")
|
|
flag.Parse()
|
|
|
|
if *debug {
|
|
common.SetLoggingLevel(common.LogLevelDebug)
|
|
}
|
|
|
|
if redisUrl := os.Getenv("REDIS"); len(redisUrl) > 0 {
|
|
RedisConnect(redisUrl)
|
|
} else {
|
|
common.LogError("REDIS needs to contains URL of the OBS Redis instance with login information")
|
|
return
|
|
}
|
|
|
|
var rescanRepoError error
|
|
go func() {
|
|
for {
|
|
if rescanRepoError = RescanRepositories(); rescanRepoError != nil {
|
|
common.LogError("Failed to rescan repositories.", rescanRepoError)
|
|
}
|
|
time.Sleep(time.Minute * 5)
|
|
}
|
|
}()
|
|
|
|
http.HandleFunc("GET /", func(res http.ResponseWriter, req *http.Request) {
|
|
if rescanRepoError != nil {
|
|
res.WriteHeader(500)
|
|
return
|
|
}
|
|
res.WriteHeader(404)
|
|
res.Write([]byte("404 page not found\n"))
|
|
})
|
|
http.HandleFunc("GET /status/{Project}", func(res http.ResponseWriter, req *http.Request) {
|
|
mime := ParseMimeHeader(req)
|
|
obsPrj := req.PathValue("Project")
|
|
common.LogInfo(" GET /status/"+obsPrj, "["+mime.MimeType()+"]")
|
|
|
|
status := FindAndUpdateProjectResults(obsPrj)
|
|
if len(status) == 0 {
|
|
res.WriteHeader(404)
|
|
return
|
|
}
|
|
res.Header().Add("content-type", mime.MimeHeader)
|
|
if mime.IsSvg() {
|
|
svg := ProjectStatusSummarySvg(status)
|
|
res.Header().Add("size", fmt.Sprint(len(svg)))
|
|
res.Write(svg)
|
|
} else if mime.IsJson() {
|
|
WriteJson(status, res)
|
|
} else if mime.IsXml() {
|
|
WriteXml(status, res)
|
|
}
|
|
})
|
|
http.HandleFunc("GET /status/{Project}/{Package}", func(res http.ResponseWriter, req *http.Request) {
|
|
mime := ParseMimeHeader(req)
|
|
obsPrj := req.PathValue("Project")
|
|
obsPkg := req.PathValue("Package")
|
|
common.LogInfo(" GET /status/"+obsPrj+"/"+obsPkg, "["+mime.MimeType()+"]")
|
|
|
|
status := slices.Clone(FindAndUpdateProjectResults(obsPrj))
|
|
for i, s := range status {
|
|
f := *s
|
|
f.Status = slices.DeleteFunc(slices.Clone(s.Status), DeleteExceptPkg(obsPkg))
|
|
status[i] = &f
|
|
}
|
|
if len(status) == 0 {
|
|
res.WriteHeader(404)
|
|
return
|
|
}
|
|
|
|
res.Header().Add("content-type", mime.MimeHeader)
|
|
if mime.IsSvg() {
|
|
svg := PackageStatusSummarySvg(obsPkg, status)
|
|
|
|
res.Header().Add("size", fmt.Sprint(len(svg)))
|
|
res.Write(svg)
|
|
} else if mime.IsJson() {
|
|
WriteJson(status, res)
|
|
} else if mime.IsXml() {
|
|
WriteXml(status, res)
|
|
}
|
|
|
|
})
|
|
http.HandleFunc("GET /status/{Project}/{Package}/{Repository}", func(res http.ResponseWriter, req *http.Request) {
|
|
mime := ParseMimeHeader(req)
|
|
obsPrj := req.PathValue("Project")
|
|
obsPkg := req.PathValue("Package")
|
|
repo := req.PathValue("Repository")
|
|
common.LogInfo(" GET /status/"+obsPrj+"/"+obsPkg, "["+mime.MimeType()+"]")
|
|
|
|
status := slices.Clone(FindAndUpdateRepoResults(obsPrj, repo))
|
|
for _, s := range status {
|
|
s.Status = slices.DeleteFunc(slices.Clone(s.Status), DeleteExceptPkg(obsPkg))
|
|
}
|
|
if len(status) == 0 {
|
|
res.WriteHeader(404)
|
|
return
|
|
}
|
|
|
|
if mime.IsSvg() {
|
|
svg := PackageStatusSummarySvg(obsPkg, status)
|
|
res.Header().Add("content-type", mime.MimeHeader)
|
|
res.Header().Add("size", fmt.Sprint(len(svg)))
|
|
res.Write(svg)
|
|
} else if mime.IsJson() {
|
|
WriteJson(status, res)
|
|
} else if mime.IsXml() {
|
|
WriteXml(status, res)
|
|
}
|
|
})
|
|
http.HandleFunc("GET /status/{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
|
|
mime := ParseMimeHeader(req)
|
|
prj := req.PathValue("Project")
|
|
pkg := req.PathValue("Package")
|
|
repo := req.PathValue("Repository")
|
|
arch := req.PathValue("Arch")
|
|
common.LogInfo(" GET /status/"+prj+"/"+pkg+"/"+repo+"/"+arch, "["+mime.MimeType()+"]")
|
|
|
|
res.Header().Add("content-type", mime.MimeHeader)
|
|
for _, r := range FindAndUpdateRepoResults(prj, repo) {
|
|
if r.Arch == arch {
|
|
if idx, found := slices.BinarySearchFunc(r.Status, &common.PackageBuildStatus{Package: pkg}, common.PackageBuildStatusComp); found {
|
|
status := r.Status[idx]
|
|
if mime.IsSvg() {
|
|
res.Write(BuildStatusSvg(r, status))
|
|
} else if mime.IsJson() {
|
|
WriteJson(status, res)
|
|
} else if mime.IsXml() {
|
|
WriteXml(status, res)
|
|
}
|
|
return
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if mime.IsSvg() {
|
|
res.Write(BuildStatusSvg(nil, &common.PackageBuildStatus{Package: pkg, Code: "unknown"}))
|
|
}
|
|
})
|
|
http.HandleFunc("GET /search", func(res http.ResponseWriter, req *http.Request) {
|
|
common.LogInfo("GET /search?" + req.URL.RawQuery)
|
|
queries := req.URL.Query()
|
|
if !queries.Has("q") {
|
|
res.WriteHeader(400)
|
|
return
|
|
}
|
|
|
|
names := queries["q"]
|
|
if len(names) != 1 {
|
|
res.WriteHeader(400)
|
|
return
|
|
}
|
|
|
|
packages := FindPackages(names[0])
|
|
data, err := json.MarshalIndent(packages, "", " ")
|
|
if err != nil {
|
|
res.WriteHeader(500)
|
|
common.LogError("Error in marshalling data.", err)
|
|
return
|
|
}
|
|
|
|
res.Write(data)
|
|
res.Header().Add("content-type", "application/json")
|
|
res.WriteHeader(200)
|
|
})
|
|
|
|
http.HandleFunc("GET /buildlog/{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
|
|
prj := req.PathValue("Project")
|
|
pkg := req.PathValue("Package")
|
|
repo := req.PathValue("Repository")
|
|
arch := req.PathValue("Arch")
|
|
|
|
res.Header().Add("location", *ObsUrl+"/package/live_build_log/"+url.PathEscape(prj)+"/"+url.PathEscape(pkg)+"/"+url.PathEscape(repo)+"/"+url.PathEscape(arch))
|
|
res.WriteHeader(307)
|
|
return
|
|
|
|
// status := GetDetailedBuildStatus(prj, pkg, repo, arch)
|
|
data, err := obs.BuildLog(prj, pkg, repo, arch)
|
|
if err != nil {
|
|
res.WriteHeader(http.StatusInternalServerError)
|
|
common.LogError("Failed to fetch build log for:", prj, pkg, repo, arch, err)
|
|
return
|
|
}
|
|
defer data.Close()
|
|
|
|
io.Copy(res, data)
|
|
})
|
|
|
|
if *disableTls {
|
|
log.Fatal(http.ListenAndServe(*listen, nil))
|
|
} else {
|
|
log.Fatal(http.ListenAndServeTLS(*listen, *cert, *key, nil))
|
|
}
|
|
}
|