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.

Let the CLI cancel a run, not only the browser

deb024d49d894a5a51b1a5bdc07e66c672f113ab · Keenan Nemetz · 6d ago · parent 0c14f078

Let the CLI cancel a run, not only the browser

#168 promised the same account the same actions through either transport, and
canceling a running run had just become a browser-only action. The queue is
instance-side, so the CLI asks over the key it already pushes with:
`vole run cancel <id>` retires a queued run or asks a running one to stop, checks
the same maintainer capability the page checks, and writes the same audit record.
The server verb takes one repository and one run id and answers one line; the id
is the 26-character one in the run page URL.

The audit action is now `run.cancel` for both transports, because it is the same
action: an operator looking for canceled runs should not have to know which
surface asked.

Part of #162.

server/ssh/cancel_test.go +81

@@ -6,6 +6,9 @@ package ssh
6 6 // back keeps the canceled status.
7 7
8 8 import (
9 "os"
10 "path/filepath"
11 "strings"
9 12 "testing"
10 13 "time"
11 14
@@ -51,3 +54,81 @@ func TestAHeartbeatCarriesTheStopRequest(t *testing.T) {
51 54 t.Fatalf("run after the stopped attempt = %+v", got)
52 55 }
53 56 }
57
58 // The CLI cancel asks the instance, not a clone: the queue and the access rules are
59 // both server-side. It is the same action the browser offers, over the key the
60 // account pushes with, and it writes the same audit record.
61 func TestTheCLICancelsARunningRun(t *testing.T) {
62 t.Parallel()
63 h := newHarness(t)
64 runner := h.runner(t, h.keyPath)
65 run, job := h.leaseRun(t, runner, "build")
66
67 client := h.dial(t, h.keyPath)
68 sess, err := client.NewSession()
69 if err != nil {
70 t.Fatalf("new session: %v", err)
71 }
72 defer sess.Close()
73 out, err := sess.CombinedOutput("vole-run-cancel /demo.git " + run.ID)
74 if err != nil {
75 t.Fatalf("cancel: %v: %s", err, out)
76 }
77 if !strings.Contains(string(out), "stop") {
78 t.Fatalf("cancel said %q, want the runner's next-heartbeat outcome", out)
79 }
80
81 got, err := h.runs.Get(run.ID)
82 if err != nil {
83 t.Fatal(err)
84 }
85 if got.Status != runs.StatusRunning || !got.CancelRequested || got.CancelActor != h.writerID {
86 t.Fatalf("run after the CLI cancel = %+v", got)
87 }
88
89 // The runner learns from the reply it already asks for.
90 if rep := runner.call(runnerRequest{Op: "heartbeat", Run: job.ID}); !rep.OK || rep.Cancel != h.writerID {
91 t.Fatalf("heartbeat after the CLI cancel = %+v", rep)
92 }
93
94 raw, err := os.ReadFile(filepath.Join(h.dir, "audit.log"))
95 if err != nil {
96 t.Fatalf("reading the audit log: %v", err)
97 }
98 audit := string(raw)
99 for _, want := range []string{"run.cancel", h.writerID, h.repoName, run.ID} {
100 if !strings.Contains(audit, want) {
101 t.Fatalf("audit log does not name %q:\n%s", want, audit)
102 }
103 }
104 }
105
106 // Canceling is a maintainer capability, and the CLI asks the same rule the page
107 // does: a reader may look at a run and may not stop it.
108 func TestTheCLIWillNotCancelAsAReader(t *testing.T) {
109 t.Parallel()
110 h := newHarness(t)
111 runner := h.runner(t, h.keyPath)
112 run, _ := h.leaseRun(t, runner, "build")
113
114 client := h.dial(t, h.roKey)
115 sess, err := client.NewSession()
116 if err != nil {
117 t.Fatalf("new session: %v", err)
118 }
119 defer sess.Close()
120 out, err := sess.CombinedOutput("vole-run-cancel /demo.git " + run.ID)
121 if err == nil {
122 t.Fatalf("a reader canceled a run: %s", out)
123 }
124 if !strings.Contains(string(out), "may not cancel runs") {
125 t.Fatalf("refusal = %q, want the missing capability", out)
126 }
127 got, err := h.runs.Get(run.ID)
128 if err != nil {
129 t.Fatal(err)
130 }
131 if got.CancelRequested {
132 t.Fatalf("a reader's cancel was applied: %+v", got)
133 }
134 }