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 . */ 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 = "" endTag = "" } return []byte(`` + `` + startTag + `` + html.EscapeString(buildStatus.Code) + `` + endTag + ``) } 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("")) res.Write(xmlData) res.Write([]byte("")) } } 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 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 } 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)) } }