Compare commits
7 Commits
status_ser
...
fix_crash
Author | SHA256 | Date | |
---|---|---|---|
c84af6286d | |||
8436a49c5d | |||
106e36d6bf | |||
0ec4986163 | |||
fb7f6adc98 | |||
231f29b065 | |||
3f3645a453 |
@@ -583,23 +583,18 @@ type PackageBuildStatus struct {
|
||||
Package string `xml:"package,attr"`
|
||||
Code string `xml:"code,attr"`
|
||||
Details string `xml:"details"`
|
||||
|
||||
LastUpdate int64
|
||||
}
|
||||
|
||||
type BuildResult struct {
|
||||
Project string `xml:"project,attr"`
|
||||
Repository string `xml:"repository,attr"`
|
||||
Arch string `xml:"arch,attr"`
|
||||
|
||||
Code string `xml:"code,attr"`
|
||||
Dirty bool `xml:"dirty,attr"`
|
||||
ScmSync string `xml:"scmsync"`
|
||||
ScmInfo string `xml:"scminfo"`
|
||||
Status []PackageBuildStatus `xml:"status"`
|
||||
Binaries []BinaryList `xml:"binarylist"`
|
||||
|
||||
LastUpdate int64
|
||||
Project string `xml:"project,attr"`
|
||||
Repository string `xml:"repository,attr"`
|
||||
Arch string `xml:"arch,attr"`
|
||||
Code string `xml:"code,attr"`
|
||||
Dirty bool `xml:"dirty,attr"`
|
||||
ScmSync string `xml:"scmsync"`
|
||||
ScmInfo string `xml:"scminfo"`
|
||||
Status []PackageBuildStatus `xml:"status"`
|
||||
Binaries []BinaryList `xml:"binarylist"`
|
||||
}
|
||||
|
||||
type Binary struct {
|
||||
@@ -619,7 +614,6 @@ type BuildResultList struct {
|
||||
Result []BuildResult `xml:"result"`
|
||||
|
||||
isLastBuild bool
|
||||
LastUpdate int64
|
||||
}
|
||||
|
||||
func (r *BuildResultList) GetPackageList() []string {
|
||||
@@ -642,48 +636,6 @@ func (r *BuildResultList) GetPackageList() []string {
|
||||
return pkgList
|
||||
}
|
||||
|
||||
func packageSort(A, B PackageBuildStatus) int {
|
||||
return strings.Compare(A.Package, B.Package)
|
||||
}
|
||||
|
||||
func repoSort(A, B BuildResult) int {
|
||||
eq := strings.Compare(A.Project, B.Project)
|
||||
if eq == 0 {
|
||||
eq = strings.Compare(A.Repository, B.Repository)
|
||||
if eq == 0 {
|
||||
eq = strings.Compare(A.Arch, B.Arch)
|
||||
}
|
||||
}
|
||||
return eq
|
||||
}
|
||||
|
||||
func (r *BuildResultList) MergePackageState(now int64, pkgState *BuildResultList) {
|
||||
for _, nr := range pkgState.Result {
|
||||
idx, found := slices.BinarySearchFunc(r.Result, nr, repoSort)
|
||||
// not found, new repo?
|
||||
if !found {
|
||||
nr.LastUpdate = now
|
||||
r.Result = slices.Insert(r.Result, idx, nr)
|
||||
continue
|
||||
}
|
||||
|
||||
// update current repo
|
||||
repo := &r.Result[idx]
|
||||
|
||||
// update all the packages in the repo
|
||||
for _, p := range nr.Status {
|
||||
p.LastUpdate = now
|
||||
idx, found := slices.BinarySearchFunc(repo.Status, p, packageSort)
|
||||
if !found {
|
||||
repo.Status = slices.Insert(repo.Status, idx, p)
|
||||
continue
|
||||
}
|
||||
|
||||
repo.Status[idx] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *BuildResultList) BuildResultSummary() (success, finished bool) {
|
||||
if r == nil {
|
||||
return false, false
|
||||
@@ -951,11 +903,5 @@ func (c *ObsClient) BuildStatusWithState(project string, opts *BuildResultOption
|
||||
if ret != nil {
|
||||
ret.isLastBuild = opts.LastBuild
|
||||
}
|
||||
|
||||
slices.SortFunc(ret.Result, repoSort)
|
||||
for _, r := range ret.Result {
|
||||
slices.SortFunc(r.Status, packageSort)
|
||||
}
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
@@ -86,38 +86,38 @@ func (l *RabbitConnection) ProcessRabbitMQ(msgCh chan<- RabbitMessage) error {
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
ch, err := connection.Channel()
|
||||
l.ch, err = connection.Channel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot create a channel. Err: %w", err)
|
||||
}
|
||||
defer ch.Close()
|
||||
defer l.ch.Close()
|
||||
|
||||
if err = ch.ExchangeDeclarePassive("pubsub", "topic", true, false, false, false, nil); err != nil {
|
||||
if err = l.ch.ExchangeDeclarePassive("pubsub", "topic", true, false, false, false, nil); err != nil {
|
||||
return fmt.Errorf("Cannot find pubsub exchange? Err: %w", err)
|
||||
}
|
||||
|
||||
var q rabbitmq.Queue
|
||||
if len(queueName) == 0 {
|
||||
q, err = ch.QueueDeclare("", false, true, true, false, nil)
|
||||
q, err = l.ch.QueueDeclare("", false, true, true, false, nil)
|
||||
} else {
|
||||
q, err = ch.QueueDeclarePassive(queueName, true, false, true, false, nil)
|
||||
q, err = l.ch.QueueDeclarePassive(queueName, true, false, true, false, nil)
|
||||
if err != nil {
|
||||
LogInfo("queue not found .. trying to create it:", err)
|
||||
if ch.IsClosed() {
|
||||
ch, err = connection.Channel()
|
||||
if l.ch.IsClosed() {
|
||||
l.ch, err = connection.Channel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Channel cannot be re-opened. Err: %w", err)
|
||||
}
|
||||
}
|
||||
q, err = ch.QueueDeclare(queueName, true, false, true, false, nil)
|
||||
q, err = l.ch.QueueDeclare(queueName, true, false, true, false, nil)
|
||||
|
||||
if err != nil {
|
||||
LogInfo("can't create persistent queue ... falling back to temporaty queue:", err)
|
||||
if ch.IsClosed() {
|
||||
ch, err = connection.Channel()
|
||||
if l.ch.IsClosed() {
|
||||
l.ch, err = connection.Channel()
|
||||
return fmt.Errorf("Channel cannot be re-opened. Err: %w", err)
|
||||
}
|
||||
q, err = ch.QueueDeclare("", false, true, true, false, nil)
|
||||
q, err = l.ch.QueueDeclare("", false, true, true, false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func (l *RabbitConnection) ProcessRabbitMQ(msgCh chan<- RabbitMessage) error {
|
||||
l.topicSubChanges <- "+" + topic
|
||||
}
|
||||
|
||||
msgs, err := ch.Consume(q.Name, "", true, true, false, false, nil)
|
||||
msgs, err := l.ch.Consume(q.Name, "", true, true, false, false, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot start consumer. Err: %w", err)
|
||||
}
|
||||
|
@@ -25,30 +25,28 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
RequestType_CreateBrachTag = "create"
|
||||
RequestType_DeleteBranchTag = "delete"
|
||||
RequestType_Fork = "fork"
|
||||
RequestType_Issue = "issues"
|
||||
RequestType_IssueAssign = "issue_assign"
|
||||
RequestType_IssueComment = "issue_comment"
|
||||
RequestType_IssueLabel = "issue_label"
|
||||
RequestType_IssueMilestone = "issue_milestone"
|
||||
RequestType_Push = "push"
|
||||
RequestType_Repository = "repository"
|
||||
RequestType_Release = "release"
|
||||
RequestType_PR = "pull_request"
|
||||
RequestType_PRAssign = "pull_request_assign"
|
||||
RequestType_PRLabel = "pull_request_label"
|
||||
RequestType_PRComment = "pull_request_comment"
|
||||
RequestType_PRMilestone = "pull_request_milestone"
|
||||
RequestType_PRSync = "pull_request_sync"
|
||||
RequestType_PRReviewAccepted = "pull_request_review_approved"
|
||||
RequestType_PRReviewRejected = "pull_request_review_rejected"
|
||||
RequestType_PRReviewRequest = "pull_request_review_request"
|
||||
RequestType_PRReviewComment = "pull_request_review_comment"
|
||||
RequestType_Wiki = "wiki"
|
||||
)
|
||||
const RequestType_CreateBrachTag = "create"
|
||||
const RequestType_DeleteBranchTag = "delete"
|
||||
const RequestType_Fork = "fork"
|
||||
const RequestType_Issue = "issues"
|
||||
const RequestType_IssueAssign = "issue_assign"
|
||||
const RequestType_IssueComment = "issue_comment"
|
||||
const RequestType_IssueLabel = "issue_label"
|
||||
const RequestType_IssueMilestone = "issue_milestone"
|
||||
const RequestType_Push = "push"
|
||||
const RequestType_Repository = "repository"
|
||||
const RequestType_Release = "release"
|
||||
const RequestType_PR = "pull_request"
|
||||
const RequestType_PRAssign = "pull_request_assign"
|
||||
const RequestType_PRLabel = "pull_request_label"
|
||||
const RequestType_PRComment = "pull_request_comment"
|
||||
const RequestType_PRMilestone = "pull_request_milestone"
|
||||
const RequestType_PRSync = "pull_request_sync"
|
||||
const RequestType_PRReviewAccepted = "pull_request_review_approved"
|
||||
const RequestType_PRReviewRejected = "pull_request_review_rejected"
|
||||
const RequestType_PRReviewRequest = "pull_request_review_request"
|
||||
const RequestType_PRReviewComment = "pull_request_review_comment"
|
||||
const RequestType_Wiki = "wiki"
|
||||
|
||||
type RequestProcessor interface {
|
||||
ProcessFunc(*Request) error
|
||||
|
@@ -1,99 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ObsMessageType_PackageBuildFail = "package.build_fail"
|
||||
ObsMessageType_PackageBuildSuccess = "package.build_success"
|
||||
ObsMessageType_PackageBuildUnchanged = "package.build_unchanged"
|
||||
|
||||
ObsMessageType_RepoBuildFinished = "repo.build_finished"
|
||||
ObsMessageType_RepoBuildStarted = "repo.build_started"
|
||||
)
|
||||
|
||||
type BuildResultMsg struct {
|
||||
Status string
|
||||
Project string `json:"project"`
|
||||
Package string `json:"package"`
|
||||
Repo string `json:"repository"`
|
||||
Arch string `json:"arch"`
|
||||
|
||||
StartTime int32 `json:"starttime"`
|
||||
EndTime int32 `json:"endtime"`
|
||||
WorkerID string `json:"workerid"`
|
||||
Version string `json:"versrel"`
|
||||
Build string `json:"buildtype"`
|
||||
}
|
||||
|
||||
type RepoBuildMsg struct {
|
||||
Status string
|
||||
Project string `json:"project"`
|
||||
Repo string `json:"repo"`
|
||||
Arch string `json:"arch"`
|
||||
BuildId string `json:"buildid"`
|
||||
}
|
||||
|
||||
var ObsRabbitMessageError_UnknownMessageType error = errors.New("Unknown message type")
|
||||
var ObsRabbitMessageError_ParseError error = errors.New("JSON parsing error")
|
||||
|
||||
func ParseObsRabbitMessaege(ObsMessageType string, data []byte) (interface{}, error) {
|
||||
unmarshall := func(data []byte, v any) (interface{}, error) {
|
||||
if err := json.Unmarshal(data, v); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ObsRabbitMessageError_ParseError, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
switch ObsMessageType {
|
||||
case ObsMessageType_PackageBuildSuccess, ObsMessageType_PackageBuildUnchanged:
|
||||
ret := &BuildResultMsg{Status: "succeeded"}
|
||||
return unmarshall(data, ret)
|
||||
case ObsMessageType_PackageBuildFail:
|
||||
ret := &BuildResultMsg{Status: "failed"}
|
||||
return unmarshall(data, ret)
|
||||
case ObsMessageType_RepoBuildFinished:
|
||||
ret := &RepoBuildMsg{Status: "finished"}
|
||||
return unmarshall(data, ret)
|
||||
case ObsMessageType_RepoBuildStarted:
|
||||
ret := &RepoBuildMsg{Status: "building"}
|
||||
return unmarshall(data, ret)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: %s", ObsRabbitMessageError_UnknownMessageType, ObsMessageType)
|
||||
}
|
||||
|
||||
type ObsMessageProcessor func(topic string, data []byte) error
|
||||
|
||||
type RabbitMQObsBuildStatusProcessor struct {
|
||||
Handlers map[string]ObsMessageProcessor
|
||||
|
||||
c *RabbitConnection
|
||||
}
|
||||
|
||||
func (o *RabbitMQObsBuildStatusProcessor) routingKeyPrefix() string {
|
||||
if strings.HasSuffix(o.c.RabbitURL.Hostname(), "opensuse.org") {
|
||||
return "opensuse"
|
||||
}
|
||||
|
||||
return "suse"
|
||||
}
|
||||
|
||||
func (o *RabbitMQObsBuildStatusProcessor) GenerateTopics() []string {
|
||||
prefix := o.routingKeyPrefix()
|
||||
msgs := make([]string, len(o.Handlers))
|
||||
idx := 0
|
||||
for k, _ := range o.Handlers {
|
||||
msgs[idx] = prefix + ".obs." + k
|
||||
idx++
|
||||
}
|
||||
slices.Sort(msgs)
|
||||
return msgs
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (o *RabbitMQObsBuildStatusProcessor) Connection() *RabbitConnection {
|
||||
@@ -105,11 +17,6 @@ func (o *RabbitMQObsBuildStatusProcessor) Connection() *RabbitConnection {
|
||||
}
|
||||
|
||||
func (o *RabbitMQObsBuildStatusProcessor) ProcessRabbitMessage(msg RabbitMessage) error {
|
||||
prefix := o.routingKeyPrefix() + ".obs."
|
||||
topic := strings.TrimPrefix(msg.RoutingKey, prefix)
|
||||
if h, ok := o.Handlers[topic]; ok {
|
||||
return h(topic, msg.Body)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unhandled message received: %s", msg.RoutingKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -173,3 +173,4 @@ func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
|
||||
|
||||
return "", DevelProjectNotFound
|
||||
}
|
||||
|
||||
|
@@ -324,7 +324,8 @@ func importRepos(packages []string) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkgName := range oldPackageNames {
|
||||
for idx := 0; idx < len(oldPackageNames); idx++ {
|
||||
pkgName := oldPackageNames[idx]
|
||||
log.Println("fetching git:", pkgName)
|
||||
remotes := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkgName, "remote", "show", "-n"), "\n")
|
||||
|
||||
@@ -355,7 +356,7 @@ func importRepos(packages []string) {
|
||||
if forceBadPool {
|
||||
log.Println(" *** factory has no branches!!! Treating as a devel package.")
|
||||
develProjectPackages = append(develProjectPackages, pkgName)
|
||||
break
|
||||
continue
|
||||
} else {
|
||||
log.Panicln(" *** factory has no branches", branches)
|
||||
}
|
||||
@@ -413,14 +414,17 @@ func importRepos(packages []string) {
|
||||
if !found {
|
||||
log.Println("*** WARNING: Cannot find same tree for pkg", pkgName, "Will use current import instead")
|
||||
git.GitExecOrPanic(pkgName, "checkout", "-B", "main", "heads/"+import_branch)
|
||||
log.Println("setting main to", "heads/"+import_branch)
|
||||
}
|
||||
} else {
|
||||
log.Println("setting main to", "heads/"+import_branch)
|
||||
git.GitExecOrPanic(pkgName, "checkout", "-B", "main", "heads/"+import_branch)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(develProjectPackages); i++ {
|
||||
pkg := develProjectPackages[i]
|
||||
log.Println("setting main branch for devel package:", pkg)
|
||||
meta, err := obs.GetPackageMeta(prj, pkg)
|
||||
if err != nil {
|
||||
meta, err = obs.GetPackageMeta(prj, pkg)
|
||||
@@ -437,6 +441,7 @@ func importRepos(packages []string) {
|
||||
}
|
||||
git.GitExecOrPanic(pkg, "checkout", "-B", "main")
|
||||
}
|
||||
log.Println("skip for scmsync")
|
||||
continue
|
||||
} else {
|
||||
common.PanicOnError(gitImporter(prj, pkg))
|
||||
@@ -454,6 +459,7 @@ func importRepos(packages []string) {
|
||||
log.Println(" *** pool branch 'devel' ahead. Switching branches.")
|
||||
branch = "devel"
|
||||
}
|
||||
log.Println("setting main to", branch)
|
||||
git.GitExecOrPanic(pkg, "checkout", "-B", "main", branch)
|
||||
}
|
||||
|
||||
@@ -476,6 +482,7 @@ func importRepos(packages []string) {
|
||||
})
|
||||
|
||||
for _, pkg := range factoryRepos {
|
||||
log.Println("factory fork creator for develProjectPackage:", pkg.Name)
|
||||
var repo *models.Repository
|
||||
if repoData, err := client.Repository.RepoGet(repository.NewRepoGetParams().WithOwner(org).WithRepo(pkg.Name), r.DefaultAuthentication); err != nil {
|
||||
// update package
|
||||
@@ -505,7 +512,15 @@ func importRepos(packages []string) {
|
||||
git.GitExecOrPanic(pkg.Name, "lfs", "push", "develorigin", "--all")
|
||||
}
|
||||
git.GitExecOrPanic(pkg.Name, "push", "develorigin", "main", "-f")
|
||||
git.GitExec(pkg.Name, "push", "develorigin", "--delete", "factory", "devel")
|
||||
branches := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkg.Name, "branch", "-r"), "\n")
|
||||
for _, b := range branches {
|
||||
if len(b) > 12 && b[0:12] == "develorigin/" {
|
||||
b = b[12:]
|
||||
if b == "factory" || b == "devel" {
|
||||
git.GitExec(pkg.Name, "push", "develorigin", "--delete", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
// git.GitExecOrPanic(pkg.ame, "checkout", "-B", "main", "devel/main")
|
||||
_, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(giteaPackage(repo.Name)).WithBody(&models.EditRepoOption{
|
||||
DefaultBranch: "main",
|
||||
@@ -531,6 +546,7 @@ func importRepos(packages []string) {
|
||||
}
|
||||
|
||||
for _, pkg := range develProjectPackages {
|
||||
log.Println("repo creator for develProjectPackage:", pkg)
|
||||
var repo *models.Repository
|
||||
if repoData, err := client.Repository.RepoGet(repository.NewRepoGetParams().WithOwner(org).WithRepo(giteaPackage(pkg)), r.DefaultAuthentication); err != nil {
|
||||
giteaPkg := giteaPackage(pkg)
|
||||
@@ -586,7 +602,15 @@ func importRepos(packages []string) {
|
||||
git.GitExecOrPanic(pkg, "lfs", "push", "develorigin", "--all")
|
||||
}
|
||||
git.GitExecOrPanic(pkg, "push", "develorigin", "main", "-f")
|
||||
git.GitExec(pkg, "push", "develorigin", "--delete", "factory", "devel")
|
||||
branches := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(pkg, "branch", "-r"), "\n")
|
||||
for _, b := range branches {
|
||||
if len(b) > 12 && b[0:12] == "develorigin/" {
|
||||
b = b[12:]
|
||||
if b == "factory" || b == "devel" {
|
||||
git.GitExec(pkg, "push", "develorigin", "--delete", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err := client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(giteaPackage(pkg)).WithBody(&models.EditRepoOption{
|
||||
DefaultBranch: "main",
|
||||
@@ -891,7 +915,7 @@ func main() {
|
||||
syncMaintainers := flags.Bool("sync-maintainers-only", false, "Sync maintainers to Gitea and exit")
|
||||
flags.BoolVar(&forceBadPool, "bad-pool", false, "Force packages if pool has no branches due to bad import")
|
||||
flags.BoolVar(&forceNonPoolPackages, "non-pool", false, "Allow packages that are not in pool to be created. WARNING: Can't add to factory later!")
|
||||
specificPackage := flags.String("package", "", "Process specific package only, ignoring the others")
|
||||
specificPackages := flags.String("packages", "", "Process specific package, separated by commas, ignoring the others")
|
||||
|
||||
if help := flags.Parse(os.Args[1:]); help == flag.ErrHelp || flags.NArg() != 2 {
|
||||
printHelp(helpString.String())
|
||||
@@ -993,8 +1017,8 @@ func main() {
|
||||
os.Exit(10)
|
||||
}
|
||||
|
||||
if len(*specificPackage) != 0 {
|
||||
importRepos([]string{*specificPackage})
|
||||
if len(*specificPackages) != 0 {
|
||||
importRepos(common.SplitStringNoEmpty(*specificPackages, ","))
|
||||
return
|
||||
}
|
||||
importRepos(packages)
|
||||
|
3
go.mod
3
go.mod
@@ -16,6 +16,8 @@ require (
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
@@ -28,6 +30,7 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.11.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
|
6
go.sum
6
go.sum
@@ -1,8 +1,12 @@
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -50,6 +54,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
1
obs-status-service/.gitignore
vendored
1
obs-status-service/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
obs-status-service
|
||||
*.svg
|
||||
|
@@ -25,6 +25,10 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
@@ -34,27 +38,18 @@ const (
|
||||
)
|
||||
|
||||
var obs *common.ObsClient
|
||||
var debug bool
|
||||
|
||||
func LogDebug(v ...any) {
|
||||
if debug {
|
||||
log.Println(v...)
|
||||
func ProjectStatusSummarySvg(res []*common.BuildResult) []byte {
|
||||
list := common.BuildResultList{
|
||||
Result: res,
|
||||
}
|
||||
}
|
||||
|
||||
func ProjectStatusSummarySvg(project string) []byte {
|
||||
res := GetCurrentStatus(project)
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkgs := res.GetPackageList()
|
||||
pkgs := list.GetPackageList()
|
||||
maxLen := 0
|
||||
for _, p := range pkgs {
|
||||
maxLen = max(maxLen, len(p))
|
||||
}
|
||||
|
||||
width := float32(len(res.Result))*1.5 + float32(maxLen)*0.8
|
||||
width := float32(len(list.Result))*1.5 + float32(maxLen)*0.8
|
||||
height := 1.5*float32(maxLen) + 30
|
||||
|
||||
ret := bytes.Buffer{}
|
||||
@@ -65,21 +60,78 @@ func ProjectStatusSummarySvg(project string) []byte {
|
||||
ret.WriteString(`em" xmlns="http://www.w3.org/2000/svg">`)
|
||||
ret.WriteString(`<defs>
|
||||
<g id="f"> <!-- failed -->
|
||||
<rect width="1em" height="1em" fill="#800" />
|
||||
<rect width="8em" height="1.5em" fill="#800" />
|
||||
</g>
|
||||
<g id="s"> <!--succeeded-->
|
||||
<rect width="1em" height="1em" fill="#080" />
|
||||
<rect width="8em" height="1.5em" fill="#080" />
|
||||
</g>
|
||||
<g id="buidling"> <!--building-->
|
||||
<rect width="1em" height="1em" fill="#880" />
|
||||
<rect width="8em" height="1.5em" fill="#880" />
|
||||
</g>
|
||||
</defs>`)
|
||||
|
||||
ret.WriteString(`<use href="#f" x="1em" y="2em"/>`)
|
||||
ret.WriteString(`</svg>`)
|
||||
return ret.Bytes()
|
||||
}
|
||||
|
||||
func PackageStatusSummarySvg(status *common.PackageBuildStatus) []byte {
|
||||
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"]
|
||||
@@ -96,12 +148,18 @@ func PackageStatusSummarySvg(status *common.PackageBuildStatus) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
log.Println(status, " -> ", buildStatus)
|
||||
buildlog := LinkToBuildlog(repo, status)
|
||||
startTag := ""
|
||||
endTag := ""
|
||||
|
||||
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 + `"/>
|
||||
<text x="4em" y="1.1em" text-anchor="middle" fill="` + textColor + `">` + buildStatus.Code + `</text>
|
||||
</svg>`)
|
||||
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 + `">` + buildStatus.Code + `</text>` + endTag + `</svg>`)
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -109,51 +167,107 @@ func main() {
|
||||
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")
|
||||
obsHost := flag.String("obs-host", "https://api.opensuse.org", "OBS API endpoint for package status information")
|
||||
flag.BoolVar(&debug, "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")
|
||||
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()
|
||||
|
||||
common.PanicOnError(common.RequireObsSecretToken())
|
||||
if *debug {
|
||||
common.SetLoggingLevel(common.LogLevelDebug)
|
||||
}
|
||||
|
||||
// common.PanicOnError(common.RequireObsSecretToken())
|
||||
|
||||
var err error
|
||||
if obs, err = common.NewObsClient(*obsHost); err != nil {
|
||||
if obs, err = common.NewObsClient(*obsUrl); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
http.HandleFunc("GET /{Project}", func(res http.ResponseWriter, req *http.Request) {
|
||||
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 /{Project}/{Package}", func(res http.ResponseWriter, req *http.Request) {
|
||||
/*
|
||||
obsPrj := req.PathValue("Project")
|
||||
obsPkg := req.PathValue("Package")
|
||||
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, _ := PackageBuildStatus(obsPrj, obsPkg)
|
||||
svg := PackageStatusSummarySvg(status)
|
||||
*/
|
||||
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)
|
||||
res.Header().Add("size", fmt.Sprint(len(svg)))
|
||||
res.Write(svg)
|
||||
})
|
||||
http.HandleFunc("GET /{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
|
||||
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("content-type", "image/svg+xml")
|
||||
|
||||
status := GetDetailedBuildStatus(prj, pkg, repo, arch)
|
||||
res.Write(PackageStatusSummarySvg(status))
|
||||
})
|
||||
http.HandleFunc("GET /{Project}/{Package}/{Repository}/{Arch}/buildlog", 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)
|
||||
@@ -167,8 +281,6 @@ func main() {
|
||||
io.Copy(res, data)
|
||||
})
|
||||
|
||||
go ProcessUpdates()
|
||||
|
||||
if *disableTls {
|
||||
log.Fatal(http.ListenAndServe(*listen, nil))
|
||||
} else {
|
||||
|
82
obs-status-service/main_test.go
Normal file
82
obs-status-service/main_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
func TestStatusSvg(t *testing.T) {
|
||||
os.WriteFile("teststatus.svg", BuildStatusSvg(nil, &common.PackageBuildStatus{
|
||||
Package: "foo",
|
||||
Code: "succeeded",
|
||||
Details: "more success here",
|
||||
}), 0o777)
|
||||
data := []*common.BuildResult{
|
||||
{
|
||||
Project: "project:foo",
|
||||
Repository: "repo1",
|
||||
Arch: "x86_64",
|
||||
Status: []*common.PackageBuildStatus{
|
||||
{
|
||||
Package: "pkg1",
|
||||
Code: "succeeded",
|
||||
},
|
||||
{
|
||||
Package: "pkg2",
|
||||
Code: "failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Project: "project:foo",
|
||||
Repository: "repo1",
|
||||
Arch: "s390x",
|
||||
Status: []*common.PackageBuildStatus{
|
||||
{
|
||||
Package: "pkg1",
|
||||
Code: "succeeded",
|
||||
},
|
||||
{
|
||||
Package: "pkg2",
|
||||
Code: "unresolveable",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Project: "project:foo",
|
||||
Repository: "repo1",
|
||||
Arch: "i586",
|
||||
Status: []*common.PackageBuildStatus{
|
||||
{
|
||||
Package: "pkg1",
|
||||
Code: "succeeded",
|
||||
},
|
||||
{
|
||||
Package: "pkg2",
|
||||
Code: "blocked",
|
||||
Details: "foo bar is why",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Project: "project:foo",
|
||||
Repository: "TW",
|
||||
Arch: "s390",
|
||||
Status: []*common.PackageBuildStatus{
|
||||
{
|
||||
Package: "pkg1",
|
||||
Code: "excluded",
|
||||
},
|
||||
{
|
||||
Package: "pkg2",
|
||||
Code: "failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.WriteFile("testpackage.svg", PackageStatusSummarySvg("pkg2", data), 0o777)
|
||||
os.WriteFile("testproject.svg", ProjectStatusSummarySvg(data), 0o777)
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
package main
|
||||
|
||||
|
214
obs-status-service/redis.go
Normal file
214
obs-status-service/redis.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
var RepoStatus []*common.BuildResult = []*common.BuildResult{}
|
||||
var RepoStatusLock *sync.RWMutex = &sync.RWMutex{}
|
||||
|
||||
var redisClient *redis.Client
|
||||
|
||||
func RedisConnect(RedisUrl string) {
|
||||
opts, err := redis.ParseURL(RedisUrl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
redisClient = redis.NewClient(opts)
|
||||
}
|
||||
|
||||
func UpdateResults(r *common.BuildResult) {
|
||||
RepoStatusLock.Lock()
|
||||
defer RepoStatusLock.Unlock()
|
||||
|
||||
key := "result." + r.Project + "/" + r.Repository + "/" + r.Arch
|
||||
common.LogDebug(" + Updating", key)
|
||||
data, err := redisClient.HGetAll(context.Background(), key).Result()
|
||||
if err != nil {
|
||||
common.LogError("Failed fetching build results for", key, err)
|
||||
}
|
||||
common.LogDebug(" + Update size", len(data))
|
||||
|
||||
reset_time := time.Date(1000, 1, 1, 1, 1, 1, 1, time.Local)
|
||||
for _, pkg := range r.Status {
|
||||
pkg.LastUpdate = reset_time
|
||||
}
|
||||
r.LastUpdate = time.Now()
|
||||
for pkg, result := range data {
|
||||
if strings.HasPrefix(result, "scheduled") {
|
||||
// TODO: lookup where's building
|
||||
result = "building"
|
||||
}
|
||||
|
||||
var idx int
|
||||
var found bool
|
||||
var code string
|
||||
var details string
|
||||
|
||||
if pos := strings.IndexByte(result, ':'); pos > -1 && pos < len(result) {
|
||||
code = result[0:pos]
|
||||
details = result[pos+1:]
|
||||
} else {
|
||||
code = result
|
||||
details = ""
|
||||
}
|
||||
|
||||
if idx, found = slices.BinarySearchFunc(r.Status, &common.PackageBuildStatus{Package: pkg}, common.PackageBuildStatusComp); found {
|
||||
res := r.Status[idx]
|
||||
res.LastUpdate = r.LastUpdate
|
||||
res.Code = code
|
||||
res.Details = details
|
||||
} else {
|
||||
r.Status = slices.Insert(r.Status, idx, &common.PackageBuildStatus{
|
||||
Package: pkg,
|
||||
Code: code,
|
||||
Details: details,
|
||||
LastUpdate: r.LastUpdate,
|
||||
})
|
||||
}
|
||||
}
|
||||
for idx := 0; idx < len(r.Status); {
|
||||
if r.Status[idx].LastUpdate == reset_time {
|
||||
r.Status = slices.Delete(r.Status, idx, idx+1)
|
||||
} else {
|
||||
idx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FindProjectResults(project string) []*common.BuildResult {
|
||||
RepoStatusLock.RLock()
|
||||
defer RepoStatusLock.RUnlock()
|
||||
|
||||
ret := make([]*common.BuildResult, 0, 8)
|
||||
idx, _ := slices.BinarySearchFunc(RepoStatus, &common.BuildResult{Project: project}, common.BuildResultComp)
|
||||
for idx < len(RepoStatus) && RepoStatus[idx].Project == project {
|
||||
ret = append(ret, RepoStatus[idx])
|
||||
idx++
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func FindRepoResults(project, repo string) []*common.BuildResult {
|
||||
RepoStatusLock.RLock()
|
||||
defer RepoStatusLock.RUnlock()
|
||||
|
||||
ret := make([]*common.BuildResult, 0, 8)
|
||||
idx, _ := slices.BinarySearchFunc(RepoStatus, &common.BuildResult{Project: project, Repository: repo}, common.BuildResultComp)
|
||||
for idx < len(RepoStatus) && RepoStatus[idx].Project == project && RepoStatus[idx].Repository == repo {
|
||||
ret = append(ret, RepoStatus[idx])
|
||||
idx++
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func FindAndUpdateProjectResults(project string) []*common.BuildResult {
|
||||
res := FindProjectResults(project)
|
||||
wg := &sync.WaitGroup{}
|
||||
now := time.Now()
|
||||
for _, r := range res {
|
||||
if now.Sub(r.LastUpdate).Abs() < time.Second*10 {
|
||||
// 1 update per 10 second for now
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
UpdateResults(r)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return res
|
||||
}
|
||||
|
||||
func FindAndUpdateRepoResults(project, repo string) []*common.BuildResult {
|
||||
res := FindRepoResults(project, repo)
|
||||
wg := &sync.WaitGroup{}
|
||||
now := time.Now()
|
||||
for _, r := range res {
|
||||
if now.Sub(r.LastUpdate).Abs() < time.Second*10 {
|
||||
// 1 update per 10 second for now
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
UpdateResults(r)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return res
|
||||
}
|
||||
|
||||
func RescanRepositories() error {
|
||||
ctx := context.Background()
|
||||
var cursor uint64
|
||||
var err error
|
||||
|
||||
common.LogDebug("** starting rescanning ...")
|
||||
RepoStatusLock.Lock()
|
||||
for _, repo := range RepoStatus {
|
||||
repo.Dirty = false
|
||||
}
|
||||
RepoStatusLock.Unlock()
|
||||
var count int
|
||||
|
||||
for {
|
||||
var data []string
|
||||
data, cursor, err = redisClient.ScanType(ctx, cursor, "", 1000, "hash").Result()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
RepoStatusLock.Lock()
|
||||
for _, repo := range data {
|
||||
r := strings.Split(repo, "/")
|
||||
if len(r) != 3 || len(r[0]) < 8 || r[0][0:7] != "result." {
|
||||
continue
|
||||
}
|
||||
d := &common.BuildResult{
|
||||
Project: r[0][7:],
|
||||
Repository: r[1],
|
||||
Arch: r[2],
|
||||
}
|
||||
if pos, found := slices.BinarySearchFunc(RepoStatus, d, common.BuildResultComp); found {
|
||||
RepoStatus[pos].Dirty = true
|
||||
} else {
|
||||
d.Dirty = true
|
||||
RepoStatus = slices.Insert(RepoStatus, pos, d)
|
||||
count++
|
||||
}
|
||||
}
|
||||
RepoStatusLock.Unlock()
|
||||
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
common.LogDebug(" added a total", count, "repos")
|
||||
count = 0
|
||||
|
||||
RepoStatusLock.Lock()
|
||||
for i := 0; i < len(RepoStatus); {
|
||||
if !RepoStatus[i].Dirty {
|
||||
RepoStatus = slices.Delete(RepoStatus, i, i+1)
|
||||
count++
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
RepoStatusLock.Unlock()
|
||||
common.LogDebug(" removed", count, "repos")
|
||||
common.LogDebug(" total repos:", len(RepoStatus))
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,127 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
var WatchedRepos []string
|
||||
var mutex sync.Mutex
|
||||
|
||||
var StatusUpdateCh chan StatusUpdateMsg = make(chan StatusUpdateMsg)
|
||||
|
||||
var statusMutex sync.RWMutex
|
||||
var CurrentStatus map[string]*common.BuildResultList = make(map[string]*common.BuildResultList)
|
||||
|
||||
type StatusUpdateMsg struct {
|
||||
ObsProject string
|
||||
Result *common.BuildResultList
|
||||
}
|
||||
|
||||
func GetCurrentStatus(project string) *common.BuildResultList {
|
||||
statusMutex.RLock()
|
||||
|
||||
if ret, found := CurrentStatus[project]; found {
|
||||
statusMutex.RUnlock()
|
||||
return ret
|
||||
}
|
||||
|
||||
res, err := obs.BuildStatus(project)
|
||||
statusMutex.RUnlock()
|
||||
statusMutex.Lock()
|
||||
defer statusMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
CurrentStatus[project] = res
|
||||
|
||||
now := time.Now().Unix()
|
||||
CurrentStatus[project].LastUpdate = now
|
||||
for _, r := range res.Result {
|
||||
r.LastUpdate = now
|
||||
for _, p := range r.Status {
|
||||
p.LastUpdate = now
|
||||
}
|
||||
slices.SortFunc(r.Status, packageSort)
|
||||
}
|
||||
slices.SortFunc(res.Result, repoSort)
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
func updatePrjPackage(prjState *common.BuildResultList, pkg string, now int64, pkgState *common.BuildResultList) {
|
||||
for prjState.
|
||||
Result[0].Status[0].Package
|
||||
}
|
||||
|
||||
func extractPackageBuildStatus(prjState *common.BuildResultList, pkg string) []*common.PackageBuildStatus {
|
||||
|
||||
}
|
||||
|
||||
func GetDetailedPackageBuildStatus(prj, pkg string) []*common.PackageBuildStatus {
|
||||
statusMutex.RLock()
|
||||
now := time.Now().Unix()
|
||||
|
||||
cachedPrj, found := CurrentStatus[prj]
|
||||
if found {
|
||||
statusMutex.Unlock()
|
||||
if now-cachedPrj.LastUpdate < 60 {
|
||||
return extractPackageBuildStatus(cachedPrj, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
ret, err := obs.BuildStatus(prj, pkg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
statusMutex.Lock()
|
||||
defer statusMutex.Unlock()
|
||||
|
||||
updatePrjPackage(cachedPrj, pkg, now, ret)
|
||||
return extractPackageBuildStatus(cachedPrj, pkg)
|
||||
}
|
||||
|
||||
func GetDetailedBuildStatus(prj, pkg, repo, arch string) *common.PackageBuildStatus {
|
||||
prjStatus := GetCurrentStatus(prj)
|
||||
if prjStatus == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, r := range prjStatus.Result {
|
||||
if r.Arch == arch && r.Repository == repo {
|
||||
for _, status := range r.Status {
|
||||
if status.Package == pkg {
|
||||
return &status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProcessUpdates() {
|
||||
for {
|
||||
msg := <-StatusUpdateCh
|
||||
|
||||
statusMutex.Lock()
|
||||
CurrentStatus[msg.ObsProject] = msg.Result
|
||||
|
||||
drainedChannel:
|
||||
for {
|
||||
select {
|
||||
case msg = <-StatusUpdateCh:
|
||||
CurrentStatus[msg.ObsProject] = msg.Result
|
||||
default:
|
||||
statusMutex.Unlock()
|
||||
break drainedChannel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"src.opensuse.org/autogits/common"
|
||||
mock_common "src.opensuse.org/autogits/common/mock"
|
||||
)
|
||||
|
||||
func TestWatchObsProject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
res common.BuildResultList
|
||||
}{
|
||||
{
|
||||
name: "two requests",
|
||||
res: common.BuildResultList{
|
||||
State: "success",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := gomock.NewController(t)
|
||||
obs := mock_common.NewMockObsStatusFetcherWithState(ctl)
|
||||
|
||||
obs.EXPECT().BuildStatusWithState("test:foo", "").Return(&test.res, nil)
|
||||
|
||||
WatchObsProject(obs, "test:foo")
|
||||
})
|
||||
}
|
||||
}
|
119
obs-status-service/svg.go
Normal file
119
obs-status-service/svg.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type SvgWriter struct {
|
||||
ypos float64
|
||||
header []byte
|
||||
out bytes.Buffer
|
||||
}
|
||||
|
||||
func NewSvg() *SvgWriter {
|
||||
svg := &SvgWriter{}
|
||||
svg.header = []byte(`<svg version="2.0" overflow="auto" width="40ex" height="`)
|
||||
svg.out.WriteString(`em" xmlns="http://www.w3.org/2000/svg">`)
|
||||
svg.out.WriteString(`<defs>
|
||||
<g id="s">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="green" fill="#efe" rx="5" />
|
||||
<text x="2.5ex" y="1.1em">succeeded</text>
|
||||
</g>
|
||||
<g id="f">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="red" fill="#fee" rx="5" />
|
||||
<text x="5ex" y="1.1em">failed</text>
|
||||
</g>
|
||||
<g id="b">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="#fbf" rx="5" />
|
||||
<text x="3.75ex" y="1.1em">blocked</text>
|
||||
</g>
|
||||
<g id="broken">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="#fff" rx="5" />
|
||||
<text x="4.5ex" y="1.1em" stroke="red" fill="red">broken</text>
|
||||
</g>
|
||||
<g id="build">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="yellow" fill="#664" rx="5" />
|
||||
<text x="3.75ex" y="1.1em" fill="yellow">building</text>
|
||||
</g>
|
||||
<g id="u">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="yellow" fill="#555" rx="5" />
|
||||
<text x="2ex" y="1.1em" fill="orange">unresolvable</text>
|
||||
</g>
|
||||
<g id="scheduled">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="blue" fill="none" rx="5" />
|
||||
<text x="3ex" y="1.1em" stroke="none" fill="blue">scheduled</text>
|
||||
</g>
|
||||
<g id="d">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="none" rx="5" />
|
||||
<text x="4ex" y="1.1em" stroke="none" fill="grey">disabled</text>
|
||||
</g>
|
||||
<g id="e">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="none" rx="5" />
|
||||
<text x="4ex" y="1.1em" stroke="none" fill="#aaf">excluded</text>
|
||||
</g>
|
||||
<g id="un">
|
||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="none" rx="5" />
|
||||
<text x="4ex" y="1.1em" stroke="none" fill="grey">unknown</text>
|
||||
</g>
|
||||
<rect id="repotitle" width="100%" height="2em" stroke-width="1" stroke="grey" fill="grey" rx="2" />
|
||||
</defs>`)
|
||||
|
||||
return svg
|
||||
}
|
||||
|
||||
func (svg *SvgWriter) WriteTitle(title string) {
|
||||
svg.out.WriteString(`<text stroke="black" fill="black" x="1ex" y="` + fmt.Sprint(svg.ypos-.5) + `em">` + title + "</text>")
|
||||
svg.ypos += 2.5
|
||||
}
|
||||
|
||||
func (svg *SvgWriter) WriteSubtitle(subtitle string) {
|
||||
svg.out.WriteString(`<use href="#repotitle" y="` + fmt.Sprint(svg.ypos-2) + `em"/>`)
|
||||
svg.out.WriteString(`<text stroke="black" fill="black" x="3ex" y="` + fmt.Sprint(svg.ypos-.6) + `em">` + subtitle + `</text>`)
|
||||
svg.ypos += 2
|
||||
}
|
||||
|
||||
func (svg *SvgWriter) WritePackageStatus(loglink, arch, status, detail string) {
|
||||
StatusToSVG := func(S string) string {
|
||||
switch S {
|
||||
case "succeeded":
|
||||
return "s"
|
||||
case "failed":
|
||||
return "f"
|
||||
case "broken", "scheduled":
|
||||
return S
|
||||
case "blocked":
|
||||
return "b"
|
||||
case "building":
|
||||
return "build"
|
||||
case "unresolvable":
|
||||
return "u"
|
||||
case "disabled":
|
||||
return "d"
|
||||
case "excluded":
|
||||
return "e"
|
||||
}
|
||||
return "un"
|
||||
}
|
||||
|
||||
svg.out.WriteString(`<text fill="#113" x="5ex" y="` + fmt.Sprint(svg.ypos-.6) + `em">` + arch + `</text>`)
|
||||
svg.out.WriteString(`<g>`)
|
||||
if len(loglink) > 0 {
|
||||
svg.out.WriteString(`<a href="` + loglink + `">`)
|
||||
}
|
||||
svg.out.WriteString(`<use href="#` + StatusToSVG(status) + `" x="20ex" y="` + fmt.Sprint(svg.ypos-1.7) + `em"/>`)
|
||||
if len(loglink) > 0 {
|
||||
svg.out.WriteString(`</a>`)
|
||||
}
|
||||
if len(detail) > 0 {
|
||||
svg.out.WriteString(`<title>` + fmt.Sprint(detail) + "</title>")
|
||||
}
|
||||
|
||||
svg.out.WriteString("</g>\n")
|
||||
svg.ypos += 2
|
||||
}
|
||||
|
||||
func (svg *SvgWriter) GenerateSvg() []byte {
|
||||
return slices.Concat(svg.header, []byte(fmt.Sprint(svg.ypos)), svg.out.Bytes(), []byte("</svg>"))
|
||||
}
|
Reference in New Issue
Block a user