vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Expand the admin status page into an operational dashboard
4506a4d3d279556435fdd414265e7499c3dada16 · Keenan Nemetz · 14d ago · parent 2e024cc9
server/web/runners.go +40−52
| @@ -1,20 +1,15 @@ | |||
| 1 | 1 | package web | |
| 2 | 2 | ||
| 3 | // The runners page. | ||
| 3 | // Runner presence and queue depth for the admin status page. | ||
| 4 | 4 | // | |
| 5 | 5 | // A runner is not an entity the forge stores: it is an SSH channel that leases jobs, | |
| 6 | // authenticated by a key. So this page is built from two derived facts in the queue — | ||
| 7 | // the presence rows a session writes while it is connected, and the work it is | ||
| 8 | // holding — plus the queue depth per repository. Nothing here is authoritative; it | ||
| 9 | // answers "is my build waiting because nobody is listening", which is otherwise a | ||
| 10 | // question about a process on somebody else's machine. | ||
| 11 | // | ||
| 12 | // Visibility follows the clone rule, as every page does: a session building a private | ||
| 13 | // repository is not shown at all, because naming it would confirm that the repository | ||
| 14 | // exists, and a private repository's queue depth is not counted. | ||
| 6 | // authenticated by a key. So this is built from two derived facts in the queue — the | ||
| 7 | // presence rows a session writes while it is connected, and the work it is holding — | ||
| 8 | // plus the queue depth per repository. Nothing here is authoritative; it answers "is | ||
| 9 | // my build waiting because nobody is listening", which is otherwise a question about | ||
| 10 | // a process on somebody else's machine. | ||
| 15 | 11 | ||
| 16 | 12 | import ( | |
| 17 | "net/http" | ||
| 18 | 13 | "sort" | |
| 19 | 14 | "strings" | |
| 20 | 15 | "time" | |
| @@ -50,29 +45,30 @@ type queueView struct { | |||
| 50 | 45 | Cap int | |
| 51 | 46 | } | |
| 52 | 47 | ||
| 53 | // RunnersPage serves GET /runners: who is connected, what they are building, and how | ||
| 54 | // much work is waiting per repository. | ||
| 55 | func (w *Web) RunnersPage(rw http.ResponseWriter, r *http.Request) { | ||
| 48 | // runnerStatus reads the queue for the status page. The status page is an | ||
| 49 | // instance-admin surface, so every repository is visible, including private ones. | ||
| 50 | // Problems are returned alongside whatever could still be read. | ||
| 51 | func (w *Web) runnerStatus() (sessions []runnerView, queue []queueView, problems []string) { | ||
| 52 | if w.Runs == nil { | ||
| 53 | return nil, nil, nil | ||
| 54 | } | ||
| 56 | 55 | now := time.Now().UTC() | |
| 57 | var sessions []runnerView | ||
| 58 | var queue []queueView | ||
| 59 | 56 | ||
| 60 | if w.Runs != nil { | ||
| 61 | rows, err := w.Runs.Sessions(now.Add(-presenceWindow)) | ||
| 62 | if err != nil { | ||
| 63 | w.Logger("runners page: sessions: %v", err) | ||
| 64 | w.serverError(rw, r, "cannot read the queue: "+err.Error()) | ||
| 65 | return | ||
| 66 | } | ||
| 57 | rows, err := w.Runs.Sessions(now.Add(-presenceWindow)) | ||
| 58 | if err != nil { | ||
| 59 | problems = append(problems, "sessions: "+err.Error()) | ||
| 60 | } else { | ||
| 67 | 61 | for _, s := range rows { | |
| 68 | 62 | v := runnerView{ | |
| 69 | 63 | Account: s.Name, Repo: s.Repo, RunID: s.RunID, Seen: s.LastSeen, | |
| 70 | 64 | Idle: s.RunID == "", Labels: strings.Join(s.Labels, ", "), | |
| 71 | 65 | } | |
| 72 | 66 | if s.Repo != "" { | |
| 73 | m, err := w.Repos.Open(s.Repo) | ||
| 74 | if err != nil || !w.canRead(r, m) { | ||
| 75 | continue // a private repository's build is not an anonymous reader's business | ||
| 67 | if w.Repos == nil { | ||
| 68 | continue | ||
| 69 | } | ||
| 70 | if _, err := w.Repos.Open(s.Repo); err != nil { | ||
| 71 | continue // a stale row for a deleted repository | ||
| 76 | 72 | } | |
| 77 | 73 | if run, err := w.Runs.Get(s.RunID); err == nil && run != nil { | |
| 78 | 74 | v.Job = run.Job | |
| @@ -80,34 +76,26 @@ func (w *Web) RunnersPage(rw http.ResponseWriter, r *http.Request) { | |||
| 80 | 76 | } | |
| 81 | 77 | sessions = append(sessions, v) | |
| 82 | 78 | } | |
| 79 | } | ||
| 83 | 80 | ||
| 84 | depths, err := w.Runs.QueueByRepo() | ||
| 85 | if err != nil { | ||
| 86 | w.Logger("runners page: queue depth: %v", err) | ||
| 81 | depths, err := w.Runs.QueueByRepo() | ||
| 82 | if err != nil { | ||
| 83 | problems = append(problems, "queue: "+err.Error()) | ||
| 84 | return sessions, nil, problems | ||
| 85 | } | ||
| 86 | perRepoCap := 0 | ||
| 87 | if w.Cfg != nil { | ||
| 88 | perRepoCap = w.Cfg.MaxJobsPerRepo | ||
| 89 | } | ||
| 90 | for name, d := range depths { | ||
| 91 | if w.Repos == nil { | ||
| 92 | continue | ||
| 87 | 93 | } | |
| 88 | perRepoCap := 0 | ||
| 89 | if w.Cfg != nil { | ||
| 90 | perRepoCap = w.Cfg.MaxJobsPerRepo | ||
| 94 | if _, err := w.Repos.Open(name); err != nil { | ||
| 95 | continue // a queue row whose repository is gone | ||
| 91 | 96 | } | |
| 92 | for name, d := range depths { | ||
| 93 | m, err := w.Repos.Open(name) | ||
| 94 | if err != nil || !w.canRead(r, m) { | ||
| 95 | continue | ||
| 96 | } | ||
| 97 | queue = append(queue, queueView{Name: name, Queued: d.Queued, Running: d.Running, Cap: perRepoCap}) | ||
| 98 | } | ||
| 99 | sort.Slice(queue, func(i, j int) bool { return queue[i].Name < queue[j].Name }) | ||
| 97 | queue = append(queue, queueView{Name: name, Queued: d.Queued, Running: d.Running, Cap: perRepoCap}) | ||
| 100 | 98 | } | |
| 101 | |||
| 102 | w.render(rw, "runners", struct { | ||
| 103 | Page | ||
| 104 | Sessions []runnerView | ||
| 105 | Queue []queueView | ||
| 106 | Window string | ||
| 107 | }{ | ||
| 108 | Page: w.page(r, "Runners"), | ||
| 109 | Sessions: sessions, | ||
| 110 | Queue: queue, | ||
| 111 | Window: presenceWindow.String(), | ||
| 112 | }, 0) | ||
| 99 | sort.Slice(queue, func(i, j int) bool { return queue[i].Name < queue[j].Name }) | ||
| 100 | return sessions, queue, problems | ||
| 113 | 101 | } | |