| @@ -1,10 +1,17 @@ |
| 1 | 1 | package store |
| 2 | 2 | |
| 3 | | // Runner is one runner account as the instance admin sees it. |
| 3 | import "sort" |
| 4 | |
| 5 | // Runner is one runner key as the instance admin sees it. |
| 4 | 6 | type Runner struct { |
| 5 | | Username string `json:"username"` |
| 6 | | LastSeen string `json:"last_seen"` |
| 7 | | Scope string `json:"scope,omitempty"` // comma-joined owner/name, "" for any |
| 7 | Username string `json:"username"` |
| 8 | Fingerprint string `json:"fingerprint"` |
| 9 | KeyID int64 `json:"-"` |
| 10 | LastSeen string `json:"last_seen"` |
| 11 | // Scope is what the runner asked for: comma-joined owner/name, "" |
| 12 | // for any. admin runners replaces it with the attachments for a |
| 13 | // runner key. |
| 14 | Scope string `json:"scope,omitempty"` |
| 8 | 15 | // The build it holds, if any. |
| 9 | 16 | BuildRepo string `json:"build_repo,omitempty"` |
| 10 | 17 | BuildNumber int64 `json:"build_number,omitempty"` |
| @@ -12,32 +19,147 @@ type Runner struct { |
| 12 | 19 | StartedAt string `json:"started_at,omitempty"` |
| 13 | 20 | } |
| 14 | 21 | |
| 15 | | // TouchRunner records a poll: the time, the scope the runner asked for, |
| 16 | | // and the build it just claimed (0 for none). |
| 17 | | func (s *Store) TouchRunner(userID int64, scope string, buildID int64) error { |
| 18 | | _, err := s.DB.Exec(`INSERT INTO runner_seen (user_id, last_seen, scope, build_id) |
| 19 | | VALUES (?1, strftime('%Y-%m-%dT%H:%M:%fZ','now'), ?2, NULLIF(?3, 0)) |
| 20 | | ON CONFLICT (user_id) DO UPDATE SET |
| 22 | // RepoRunner is one key attached to a repository, as repo runner list |
| 23 | // shows it. |
| 24 | type RepoRunner struct { |
| 25 | Fingerprint string `json:"fingerprint"` |
| 26 | Algo string `json:"algo"` |
| 27 | Username string `json:"username"` |
| 28 | AddedAt string `json:"added_at"` |
| 29 | LastSeen string `json:"last_seen,omitempty"` |
| 30 | BuildRepo string `json:"build_repo,omitempty"` |
| 31 | BuildNumber int64 `json:"build_number,omitempty"` |
| 32 | BuildJob string `json:"build_job,omitempty"` |
| 33 | StartedAt string `json:"started_at,omitempty"` |
| 34 | } |
| 35 | |
| 36 | // AttachRunner lets a key claim a repository's builds. Attaching twice is |
| 37 | // one row. |
| 38 | func (s *Store) AttachRunner(keyID, repoID int64) error { |
| 39 | _, err := s.DB.Exec("INSERT OR IGNORE INTO runner_repos (key_id, repo_id) VALUES (?, ?)", keyID, repoID) |
| 40 | return err |
| 41 | } |
| 42 | |
| 43 | // DetachRunner removes one attachment by fingerprint. The key itself stays. |
| 44 | func (s *Store) DetachRunner(repoID int64, fingerprint string) error { |
| 45 | res, err := s.DB.Exec(`DELETE FROM runner_repos WHERE repo_id = ? |
| 46 | AND key_id = (SELECT id FROM ssh_keys WHERE fingerprint = ?)`, repoID, fingerprint) |
| 47 | if err != nil { |
| 48 | return err |
| 49 | } |
| 50 | if n, _ := res.RowsAffected(); n == 0 { |
| 51 | return ErrNotFound |
| 52 | } |
| 53 | return nil |
| 54 | } |
| 55 | |
| 56 | // RunnerRepoIDs is every repository a key is attached to. |
| 57 | func (s *Store) RunnerRepoIDs(keyID int64) ([]int64, error) { |
| 58 | rows, err := s.DB.Query("SELECT repo_id FROM runner_repos WHERE key_id = ? ORDER BY repo_id", keyID) |
| 59 | if err != nil { |
| 60 | return nil, err |
| 61 | } |
| 62 | defer rows.Close() |
| 63 | var ids []int64 |
| 64 | for rows.Next() { |
| 65 | var id int64 |
| 66 | if err := rows.Scan(&id); err != nil { |
| 67 | return nil, err |
| 68 | } |
| 69 | ids = append(ids, id) |
| 70 | } |
| 71 | return ids, rows.Err() |
| 72 | } |
| 73 | |
| 74 | // RunnerRepoPaths is RunnerRepoIDs as owner/name, sorted. |
| 75 | func (s *Store) RunnerRepoPaths(keyID int64) ([]string, error) { |
| 76 | rows, err := s.DB.Query(`SELECT COALESCE(u.username, o.name) || '/' || r.name |
| 77 | FROM runner_repos rr JOIN repos r ON r.id = rr.repo_id |
| 78 | LEFT JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id |
| 79 | LEFT JOIN orgs o ON r.owner_kind = 'org' AND o.id = r.owner_id |
| 80 | WHERE rr.key_id = ?`, keyID) |
| 81 | if err != nil { |
| 82 | return nil, err |
| 83 | } |
| 84 | defer rows.Close() |
| 85 | var paths []string |
| 86 | for rows.Next() { |
| 87 | var p string |
| 88 | if err := rows.Scan(&p); err != nil { |
| 89 | return nil, err |
| 90 | } |
| 91 | paths = append(paths, p) |
| 92 | } |
| 93 | sort.Strings(paths) |
| 94 | return paths, rows.Err() |
| 95 | } |
| 96 | |
| 97 | // RunnerAttached reports whether a key may claim a repository's builds. |
| 98 | func (s *Store) RunnerAttached(keyID, repoID int64) (bool, error) { |
| 99 | var n int |
| 100 | err := s.DB.QueryRow("SELECT count(*) FROM runner_repos WHERE key_id = ? AND repo_id = ?", keyID, repoID).Scan(&n) |
| 101 | return n > 0, err |
| 102 | } |
| 103 | |
| 104 | // ListRepoRunners is every key attached to a repository with its last |
| 105 | // poll and the build it holds, oldest attachment first. |
| 106 | func (s *Store) ListRepoRunners(repoID int64) ([]RepoRunner, error) { |
| 107 | rows, err := s.DB.Query(`SELECT k.fingerprint, k.algo, u.username, rr.added_at, |
| 108 | COALESCE(rs.last_seen, ''), |
| 109 | COALESCE(COALESCE(bu.username, bo.name) || '/' || br.name, ''), |
| 110 | COALESCE(b.number, 0), COALESCE(b.job, ''), COALESCE(b.started_at, '') |
| 111 | FROM runner_repos rr |
| 112 | JOIN ssh_keys k ON k.id = rr.key_id |
| 113 | JOIN users u ON u.id = k.user_id |
| 114 | LEFT JOIN runner_seen rs ON rs.key_id = rr.key_id |
| 115 | LEFT JOIN builds b ON b.id = rs.build_id AND b.status = 'running' |
| 116 | LEFT JOIN repos br ON br.id = b.repo_id |
| 117 | LEFT JOIN users bu ON br.owner_kind = 'user' AND bu.id = br.owner_id |
| 118 | LEFT JOIN orgs bo ON br.owner_kind = 'org' AND bo.id = br.owner_id |
| 119 | WHERE rr.repo_id = ? ORDER BY rr.added_at, k.id`, repoID) |
| 120 | if err != nil { |
| 121 | return nil, err |
| 122 | } |
| 123 | defer rows.Close() |
| 124 | var out []RepoRunner |
| 125 | for rows.Next() { |
| 126 | var r RepoRunner |
| 127 | if err := rows.Scan(&r.Fingerprint, &r.Algo, &r.Username, &r.AddedAt, &r.LastSeen, |
| 128 | &r.BuildRepo, &r.BuildNumber, &r.BuildJob, &r.StartedAt); err != nil { |
| 129 | return nil, err |
| 130 | } |
| 131 | out = append(out, r) |
| 132 | } |
| 133 | return out, rows.Err() |
| 134 | } |
| 135 | |
| 136 | // TouchRunner records a poll by one key: the time, the scope the runner |
| 137 | // asked for, and the build it just claimed (0 for none). |
| 138 | func (s *Store) TouchRunner(keyID, userID int64, scope string, buildID int64) error { |
| 139 | _, err := s.DB.Exec(`INSERT INTO runner_seen (key_id, user_id, last_seen, scope, build_id) |
| 140 | VALUES (?1, ?2, strftime('%Y-%m-%dT%H:%M:%fZ','now'), ?3, NULLIF(?4, 0)) |
| 141 | ON CONFLICT (key_id) DO UPDATE SET |
| 21 | 142 | last_seen = excluded.last_seen, scope = excluded.scope, |
| 22 | 143 | build_id = COALESCE(excluded.build_id, runner_seen.build_id)`, |
| 23 | | userID, scope, buildID) |
| 144 | keyID, userID, scope, buildID) |
| 24 | 145 | return err |
| 25 | 146 | } |
| 26 | 147 | |
| 27 | | // RunnerDone records that the runner reported and holds nothing now. |
| 28 | | func (s *Store) RunnerDone(userID int64) error { |
| 148 | // RunnerDone records that the key reported and holds nothing now. |
| 149 | func (s *Store) RunnerDone(keyID int64) error { |
| 29 | 150 | _, err := s.DB.Exec(`UPDATE runner_seen SET last_seen = strftime('%Y-%m-%dT%H:%M:%fZ','now'), |
| 30 | | build_id = NULL WHERE user_id = ?`, userID) |
| 151 | build_id = NULL WHERE key_id = ?`, keyID) |
| 31 | 152 | return err |
| 32 | 153 | } |
| 33 | 154 | |
| 34 | | // ListRunners lists every account that has ever polled as a runner, |
| 35 | | // most recently seen first. |
| 155 | // ListRunners lists every key that has ever polled as a runner, most |
| 156 | // recently seen first. |
| 36 | 157 | func (s *Store) ListRunners() ([]Runner, error) { |
| 37 | | rows, err := s.DB.Query(`SELECT u.username, r.last_seen, r.scope, |
| 158 | rows, err := s.DB.Query(`SELECT u.username, k.fingerprint, k.id, r.last_seen, r.scope, |
| 38 | 159 | COALESCE(COALESCE(bu.username, bo.name) || '/' || br.name, ''), |
| 39 | 160 | COALESCE(b.number, 0), COALESCE(b.job, ''), COALESCE(b.started_at, '') |
| 40 | 161 | FROM runner_seen r JOIN users u ON u.id = r.user_id |
| 162 | JOIN ssh_keys k ON k.id = r.key_id |
| 41 | 163 | LEFT JOIN builds b ON b.id = r.build_id AND b.status = 'running' |
| 42 | 164 | LEFT JOIN repos br ON br.id = b.repo_id |
| 43 | 165 | LEFT JOIN users bu ON br.owner_kind = 'user' AND bu.id = br.owner_id |
| @@ -50,7 +172,8 @@ func (s *Store) ListRunners() ([]Runner, error) { |
| 50 | 172 | var out []Runner |
| 51 | 173 | for rows.Next() { |
| 52 | 174 | var r Runner |
| 53 | | if err := rows.Scan(&r.Username, &r.LastSeen, &r.Scope, &r.BuildRepo, &r.BuildNumber, &r.BuildJob, &r.StartedAt); err != nil { |
| 175 | if err := rows.Scan(&r.Username, &r.Fingerprint, &r.KeyID, &r.LastSeen, &r.Scope, |
| 176 | &r.BuildRepo, &r.BuildNumber, &r.BuildJob, &r.StartedAt); err != nil { |
| 54 | 177 | return nil, err |
| 55 | 178 | } |
| 56 | 179 | out = append(out, r) |