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 ( "bytes" "flag" "fmt" "io" "log" "net/http" "os" "slices" "strings" "time" "src.opensuse.org/autogits/common" ) const ( AppName = "obs-status-service" ) var obs *common.ObsClient func ProjectStatusSummarySvg(res []*common.BuildResult) []byte { list := common.BuildResultList{ Result: res, } pkgs := list.GetPackageList() maxLen := 0 for _, p := range pkgs { maxLen = max(maxLen, len(p)) } width := float32(len(list.Result))*1.5 + float32(maxLen)*0.8 height := 1.5*float32(maxLen) + 30 ret := bytes.Buffer{} ret.WriteString(``) ret.WriteString(` `) ret.WriteString(``) ret.WriteString(``) return ret.Bytes() } func LinkToBuildlog(R *common.BuildResult, S *common.PackageBuildStatus) string { if R != nil && S != nil { switch S.Code { case "succeeded", "failed", "building": return "/buildlog/" + R.Project + "/" + S.Package + "/" + R.Repository + "/" + R.Arch } } return "" } 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() 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 + `` + buildStatus.Code + `` + endTag + ``) } func main() { cert := flag.String("cert-file", "", "TLS certificates file") key := flag.String("key-file", "", "Private key for the TLS certificate") listen := flag.String("listen", "[::1]:8080", "Listening string") disableTls := flag.Bool("no-tls", false, "Disable TLS") obsUrl := flag.String("obs-url", "https://api.opensuse.org", "OBS API endpoint for package buildlog information") debug := flag.Bool("debug", false, "Enable debug logging") // RabbitMQHost := flag.String("rabbit-mq", "amqps://rabbit.opensuse.org", "RabbitMQ message bus server") // Topic := flag.String("topic", "opensuse.obs", "RabbitMQ topic prefix") flag.Parse() if *debug { common.SetLoggingLevel(common.LogLevelDebug) } // common.PanicOnError(common.RequireObsSecretToken()) var err error if obs, err = common.NewObsClient(*obsUrl); err != nil { log.Fatal(err) } 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 } go func() { for { if err := RescanRepositories(); err != nil { common.LogError("Failed to rescan repositories.", err) } time.Sleep(time.Minute * 5) } }() http.HandleFunc("GET /status/{Project}", func(res http.ResponseWriter, req *http.Request) { obsPrj := req.PathValue("Project") common.LogInfo(" request: GET /status/" + obsPrj) res.WriteHeader(http.StatusBadRequest) }) http.HandleFunc("GET /status/{Project}/{Package}", func(res http.ResponseWriter, req *http.Request) { obsPrj := req.PathValue("Project") obsPkg := req.PathValue("Package") common.LogInfo(" request: GET /status/" + obsPrj + "/" + obsPkg) status := FindAndUpdateProjectResults(obsPrj) if len(status) == 0 { res.WriteHeader(404) return } svg := PackageStatusSummarySvg(obsPkg, status) res.Header().Add("content-type", "image/svg+xml") res.Header().Add("size", fmt.Sprint(len(svg))) res.Write(svg) }) http.HandleFunc("GET /status/{Project}/{Package}/{Repository}", func(res http.ResponseWriter, req *http.Request) { obsPrj := req.PathValue("Project") obsPkg := req.PathValue("Package") repo := req.PathValue("Repository") common.LogInfo(" request: GET /status/" + obsPrj + "/" + obsPkg) status := FindAndUpdateRepoResults(obsPrj, repo) if len(status) == 0 { res.WriteHeader(404) return } svg := PackageStatusSummarySvg(obsPkg, status) res.Header().Add("content-type", "image/svg+xml") res.Header().Add("size", fmt.Sprint(len(svg))) res.Write(svg) }) http.HandleFunc("GET /status/{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") common.LogInfo("GET /status/" + prj + "/" + pkg + "/" + repo + "/" + arch) res.Header().Add("content-type", "image/svg+xml") for _, r := range FindAndUpdateProjectResults(prj) { if r.Arch == arch && r.Repository == repo { if idx, found := slices.BinarySearchFunc(r.Status, &common.PackageBuildStatus{Package: pkg}, common.PackageBuildStatusComp); found { res.Write(BuildStatusSvg(r, r.Status[idx])) return } break } } res.Write(BuildStatusSvg(nil, &common.PackageBuildStatus{Package: pkg, Code: "unknown"})) }) 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", "https://build.opensuse.org/package/live_build_log/"+prj+"/"+pkg+"/"+repo+"/"+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)) } }