vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 18m ago
4b8c9e1d
| 1 | package ssh |
| 2 | |
| 3 | // Managing a run over the SSH channel. |
| 4 | // |
| 5 | // vole-run-cancel <repository> <run-id> |
| 6 | // vole-run-rerun <repository> <run-id> |
| 7 | // |
| 8 | // Canceling and rerunning are writes to the queue, and the queue is instance-side, |
| 9 | // so the CLI asks the server the same way `vole change merge` does. They are the |
| 10 | // counterparts of the browser's buttons on a run page: the same account that may act |
| 11 | // from the page may act here, the same status decides what the command means, and |
| 12 | // the audit record names the account, the repository, and the run. |
| 13 | // |
| 14 | // The verbs are not a shell. Each takes one repository and one run id and answers one |
| 15 | // line: a queued run is retired, a running one is asked to stop at its runner's next |
| 16 | // heartbeat, and a finished one can be queued again with the same pinned job. |
| 17 | |
| 18 | import ( |
| 19 | "fmt" |
| 20 | "strings" |
| 21 | "time" |
| 22 | |
| 23 | xssh "golang.org/x/crypto/ssh" |
| 24 | |
| 25 | "rodent.software/vole/ci/runs" |
| 26 | "rodent.software/vole/core/id" |
| 27 | "rodent.software/vole/internal/audit" |
| 28 | ) |
| 29 | |
| 30 | // openRunFor resolves the target of a run-management verb. It writes the refusal to |
| 31 | // ch and reports false when the caller should stop. Both verbs ask the same |
| 32 | // questions in the same order, so a name that is refused for one is refused for the |
| 33 | // other with the same words: the account, the repository, the maintainer capability, |
| 34 | // and then the run. |
| 35 | func (s *Server) openRunFor(ch xssh.Channel, argv []string, user, usage string) (*runs.Run, string, bool) { |
| 36 | if len(argv) != 3 { |
| 37 | fmt.Fprintf(ch, "vole: usage: %s\n", usage) |
| 38 | return nil, "", false |
| 39 | } |
| 40 | name, ok := repoName(argv[1]) |
| 41 | if !ok { |
| 42 | fmt.Fprint(ch, "vole: invalid repository name\n") |
| 43 | return nil, "", false |
| 44 | } |
| 45 | runID := strings.TrimSpace(argv[2]) |
| 46 | if !id.Valid(runID) { |
| 47 | fmt.Fprintf(ch, "vole: %q is not a run id; use the 26-character id from the run page\n", runID) |
| 48 | return nil, "", false |
| 49 | } |
| 50 | if s.cfg.Runs == nil { |
| 51 | fmt.Fprint(ch, "vole: this instance has no CI queue\n") |
| 52 | return nil, "", false |
| 53 | } |
| 54 | m, err := s.cfg.Repos.Open(name) |
| 55 | if err != nil { |
| 56 | fmt.Fprintf(ch, "vole: repository %q not found\n", name) |
| 57 | return nil, "", false |
| 58 | } |
| 59 | a, err := m.ACL() |
| 60 | if err != nil { |
| 61 | fmt.Fprintf(ch, "vole: cannot read access rules for %s\n", name) |
| 62 | return nil, "", false |
| 63 | } |
| 64 | // Managing a run is the maintainer capability, not triage of one item. This is |
| 65 | // the same check the browser's actions make, on the account this key resolved to. |
| 66 | if !a.CanTriage(user) { |
| 67 | fmt.Fprintf(ch, "vole: %s may not manage runs in %s; only a maintainer or admin may manage CI\n", user, name) |
| 68 | return nil, "", false |
| 69 | } |
| 70 | run, err := s.cfg.Runs.Get(runID) |
| 71 | if err != nil { |
| 72 | fmt.Fprintf(ch, "vole: cannot read run %s: %v\n", runID, err) |
| 73 | return nil, "", false |
| 74 | } |
| 75 | if run == nil || run.Repo != name { |
| 76 | fmt.Fprintf(ch, "vole: no run %s in %s\n", runID, name) |
| 77 | return nil, "", false |
| 78 | } |
| 79 | return run, name, true |
| 80 | } |
| 81 | |
| 82 | func (s *Server) serveRunCancel(ch xssh.Channel, argv []string, user string) int { |
| 83 | run, name, ok := s.openRunFor(ch, argv, user, "vole-run-cancel <repository> <run-id>") |
| 84 | if !ok { |
| 85 | return 129 |
| 86 | } |
| 87 | now := time.Now().UTC() |
| 88 | switch run.Status { |
| 89 | case runs.StatusQueued: |
| 90 | canceled, err := s.cfg.Runs.CancelQueued(run.ID, name, user, now) |
| 91 | if err != nil { |
| 92 | fmt.Fprintf(ch, "vole: cannot cancel %s: %v\n", run.ID, err) |
| 93 | return 1 |
| 94 | } |
| 95 | if !canceled { |
| 96 | fmt.Fprintf(ch, "vole: %s started before it could be canceled; try again\n", run.ID) |
| 97 | return 1 |
| 98 | } |
| 99 | s.auditRun("run.cancel", user, name, run.ID) |
| 100 | fmt.Fprintf(ch, "canceled %s while it was waiting for a runner\n", run.ID) |
| 101 | return 0 |
| 102 | case runs.StatusRunning: |
| 103 | canceled, err := s.cfg.Runs.CancelRunning(run.ID, name, user, now) |
| 104 | if err != nil { |
| 105 | fmt.Fprintf(ch, "vole: cannot stop %s: %v\n", run.ID, err) |
| 106 | return 1 |
| 107 | } |
| 108 | if !canceled { |
| 109 | fmt.Fprintf(ch, "vole: %s finished before it could be stopped\n", run.ID) |
| 110 | return 1 |
| 111 | } |
| 112 | s.auditRun("run.cancel", user, name, run.ID) |
| 113 | fmt.Fprintf(ch, "asked %s to stop; its runner stops at the next heartbeat\n", run.ID) |
| 114 | return 0 |
| 115 | default: |
| 116 | fmt.Fprintf(ch, "vole: %s is %s and cannot be canceled; only a queued or running run can be canceled\n", |
| 117 | run.ID, run.Status) |
| 118 | return 1 |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func (s *Server) serveRunRerun(ch xssh.Channel, argv []string, user string) int { |
| 123 | run, name, ok := s.openRunFor(ch, argv, user, "vole-run-rerun <repository> <run-id>") |
| 124 | if !ok { |
| 125 | return 129 |
| 126 | } |
| 127 | if run.Status == runs.StatusQueued || run.Status == runs.StatusRunning { |
| 128 | fmt.Fprintf(ch, "vole: %s is %s and has not finished yet; only a finished run can be rerun\n", |
| 129 | run.ID, run.Status) |
| 130 | return 1 |
| 131 | } |
| 132 | queued, err := s.cfg.Runs.Requeue(run.ID, name, time.Now().UTC()) |
| 133 | if err != nil { |
| 134 | fmt.Fprintf(ch, "vole: cannot queue %s again: %v\n", run.ID, err) |
| 135 | return 1 |
| 136 | } |
| 137 | if !queued { |
| 138 | fmt.Fprintf(ch, "vole: %s changed before it could be rerun; try again\n", run.ID) |
| 139 | return 1 |
| 140 | } |
| 141 | s.auditRun("run.rerun", user, name, run.ID) |
| 142 | fmt.Fprintf(ch, "queued %s again with the same pinned job\n", run.ID) |
| 143 | return 0 |
| 144 | } |
| 145 | |
| 146 | // auditRun records the decision. It is the same action name the browser writes, |
| 147 | // because it is the same action: the transport is not what an operator looking for |
| 148 | // canceled or rerun work wants to filter on. A failure is reported in the server log |
| 149 | // rather than refusing the action, which has already happened in the queue — the |
| 150 | // rule the browser's audit follows for the same reason. |
| 151 | func (s *Server) auditRun(action, user, repo, runID string) { |
| 152 | if err := audit.Append(s.cfg.AuditPath, audit.Record{ |
| 153 | Action: action, |
| 154 | Actor: user, |
| 155 | Subject: repo, |
| 156 | Detail: runID, |
| 157 | }); err != nil { |
| 158 | s.cfg.Log.Error("audit log write failed", "account", user, "repo", repo, "run", runID, "error", err) |
| 159 | } |
| 160 | } |
| 161 |