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.

Blame · server/ssh/run.go

Line attribution on main · 160 line(s)

deb024d4
Keenan Nemetz
6d ago
1 package ssh
2
70291860
Keenan Nemetz
6d ago
3 // Managing a run over the SSH channel.
deb024d4
Keenan Nemetz
6d ago
4 //
5 // vole-run-cancel <repository> <run-id>
70291860
Keenan Nemetz
6d ago
6 // vole-run-rerun <repository> <run-id>
deb024d4
Keenan Nemetz
6d ago
7 //
70291860
Keenan Nemetz
6d ago
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.
deb024d4
Keenan Nemetz
6d ago
13 //
70291860
Keenan Nemetz
6d ago
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.
deb024d4
Keenan Nemetz
6d ago
17
18 import (
19 "fmt"
20 "strings"
21 "time"
22
23 xssh "golang.org/x/crypto/ssh"
24
d37d301f
Keenan Nemetz
15h ago
25 "rodent.software/vole/ci/runs"
26 "rodent.software/vole/core/id"
27 "rodent.software/vole/internal/audit"
deb024d4
Keenan Nemetz
6d ago
28 )
29
70291860
Keenan Nemetz
6d ago
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) {
deb024d4
Keenan Nemetz
6d ago
36 if len(argv) != 3 {
70291860
Keenan Nemetz
6d ago
37 fmt.Fprintf(ch, "vole: usage: %s\n", usage)
38 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
39 }
40 name, ok := repoName(argv[1])
41 if !ok {
42 fmt.Fprint(ch, "vole: invalid repository name\n")
70291860
Keenan Nemetz
6d ago
43 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
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)
70291860
Keenan Nemetz
6d ago
48 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
49 }
50 if s.cfg.Runs == nil {
51 fmt.Fprint(ch, "vole: this instance has no CI queue\n")
70291860
Keenan Nemetz
6d ago
52 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
53 }
54 m, err := s.cfg.Repos.Open(name)
55 if err != nil {
56 fmt.Fprintf(ch, "vole: repository %q not found\n", name)
70291860
Keenan Nemetz
6d ago
57 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
58 }
59 a, err := m.ACL()
60 if err != nil {
61 fmt.Fprintf(ch, "vole: cannot read access rules for %s\n", name)
70291860
Keenan Nemetz
6d ago
62 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
63 }
64 // Managing a run is the maintainer capability, not triage of one item. This is
70291860
Keenan Nemetz
6d ago
65 // the same check the browser's actions make, on the account this key resolved to.
deb024d4
Keenan Nemetz
6d ago
66 if !a.CanTriage(user) {
70291860
Keenan Nemetz
6d ago
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
deb024d4
Keenan Nemetz
6d ago
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)
70291860
Keenan Nemetz
6d ago
73 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
74 }
75 if run == nil || run.Repo != name {
76 fmt.Fprintf(ch, "vole: no run %s in %s\n", runID, name)
70291860
Keenan Nemetz
6d ago
77 return nil, "", false
deb024d4
Keenan Nemetz
6d ago
78 }
70291860
Keenan Nemetz
6d ago
79 return run, name, true
80 }
deb024d4
Keenan Nemetz
6d ago
81
70291860
Keenan Nemetz
6d ago
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 }
deb024d4
Keenan Nemetz
6d ago
87 now := time.Now().UTC()
88 switch run.Status {
89 case runs.StatusQueued:
70291860
Keenan Nemetz
6d ago
90 canceled, err := s.cfg.Runs.CancelQueued(run.ID, name, user, now)
deb024d4
Keenan Nemetz
6d ago
91 if err != nil {
92 fmt.Fprintf(ch, "vole: cannot cancel %s: %v\n", run.ID, err)
93 return 1
94 }
70291860
Keenan Nemetz
6d ago
95 if !canceled {
deb024d4
Keenan Nemetz
6d ago
96 fmt.Fprintf(ch, "vole: %s started before it could be canceled; try again\n", run.ID)
97 return 1
98 }
70291860
Keenan Nemetz
6d ago
99 s.auditRun("run.cancel", user, name, run.ID)
deb024d4
Keenan Nemetz
6d ago
100 fmt.Fprintf(ch, "canceled %s while it was waiting for a runner\n", run.ID)
101 return 0
102 case runs.StatusRunning:
70291860
Keenan Nemetz
6d ago
103 canceled, err := s.cfg.Runs.CancelRunning(run.ID, name, user, now)
deb024d4
Keenan Nemetz
6d ago
104 if err != nil {
105 fmt.Fprintf(ch, "vole: cannot stop %s: %v\n", run.ID, err)
106 return 1
107 }
70291860
Keenan Nemetz
6d ago
108 if !canceled {
deb024d4
Keenan Nemetz
6d ago
109 fmt.Fprintf(ch, "vole: %s finished before it could be stopped\n", run.ID)
110 return 1
111 }
70291860
Keenan Nemetz
6d ago
112 s.auditRun("run.cancel", user, name, run.ID)
deb024d4
Keenan Nemetz
6d ago
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
70291860
Keenan Nemetz
6d ago
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) {
deb024d4
Keenan Nemetz
6d ago
152 if err := audit.Append(s.cfg.AuditPath, audit.Record{
70291860
Keenan Nemetz
6d ago
153 Action: action,
deb024d4
Keenan Nemetz
6d ago
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 }