Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 18m ago 4b8c9e1d
history blame raw 5.5 KiB
1package 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
18import (
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.
35func (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
82func (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
122func (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.
151func (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