File view with blame information shown in the left gutter beside each line.

barerepo / server / e2e/runner_protocol_test.go
126 lines · 4.2kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package e2e
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"bytes"
133728e barerepo 1mo
5
"encoding/json"
133728e barerepo 1mo
6
"net/http"
133728e barerepo 1mo
7
"net/url"
133728e barerepo 1mo
8
"os/exec"
133728e barerepo 1mo
9
"strconv"
133728e barerepo 1mo
10
"strings"
133728e barerepo 1mo
11
"testing"
133728e barerepo 1mo
12
133728e barerepo 1mo
13
"github.com/barerepo/server/internal/token"
133728e barerepo 1mo
14
)
133728e barerepo 1mo
15
133728e barerepo 1mo
16
// Chapter 24 says the protocol is plain http so anyone can write their own runner. This is anyone.
133728e barerepo 1mo
17
func TestARunnerCanBeWrittenFromTheFourEndpoints(t *testing.T) {
133728e barerepo 1mo
18
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
19
t.Skip("git is not installed")
133728e barerepo 1mo
20
}
133728e barerepo 1mo
21
in := newInstance(t)
133728e barerepo 1mo
22
john := in.account("john")
133728e barerepo 1mo
23
work := seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
24
133728e barerepo 1mo
25
tok, _, err := in.db.CreateToken(t.Context(), token.Runner, "john", "john/johnbot", "a runner")
133728e barerepo 1mo
26
if err != nil {
133728e barerepo 1mo
27
t.Fatal(err)
133728e barerepo 1mo
28
}
133728e barerepo 1mo
29
133728e barerepo 1mo
30
// POST /runner/attach. The machine says what it is and gets an id back.
133728e barerepo 1mo
31
var attached struct {
133728e barerepo 1mo
32
RunnerID int64 `json:"runner_id"`
133728e barerepo 1mo
33
PollInterval int `json:"poll_interval"`
133728e barerepo 1mo
34
}
133728e barerepo 1mo
35
post := postJSON(t, in, "/runner/attach", map[string]any{
133728e barerepo 1mo
36
"token": tok, "hostname": "uproar.local", "os": "linux", "arch": "amd64",
133728e barerepo 1mo
37
"labels": []string{"build", "test"},
133728e barerepo 1mo
38
})
133728e barerepo 1mo
39
if post.StatusCode != http.StatusOK {
133728e barerepo 1mo
40
t.Fatalf("attach answered %d", post.StatusCode)
133728e barerepo 1mo
41
}
133728e barerepo 1mo
42
if err := json.Unmarshal([]byte(post.body), &attached); err != nil {
133728e barerepo 1mo
43
t.Fatal(err)
133728e barerepo 1mo
44
}
133728e barerepo 1mo
45
if attached.RunnerID == 0 || attached.PollInterval == 0 {
133728e barerepo 1mo
46
t.Fatalf("attach said %+v, which tells a runner nothing", attached)
133728e barerepo 1mo
47
}
133728e barerepo 1mo
48
133728e barerepo 1mo
49
// The page updates the moment a runner attaches, which is what the runners page is for.
133728e barerepo 1mo
50
if _, _, page := get(t, in.http.URL+"/john/johnbot/runners"); !strings.Contains(page, "uproar.local") {
133728e barerepo 1mo
51
t.Errorf("the runners page does not list a machine that just attached:\n%s", page)
133728e barerepo 1mo
52
}
133728e barerepo 1mo
53
133728e barerepo 1mo
54
// A build to take. The workflow-free path is [build] command in the repository's own file.
133728e barerepo 1mo
55
write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n[build]\ncommand = \"echo hi\"\n")
133728e barerepo 1mo
56
run(t, work, "git", "commit", "-qam", "turn builds on")
133728e barerepo 1mo
57
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
58
133728e barerepo 1mo
59
// GET /runner/poll. It holds the request open, and answers with one job.
133728e barerepo 1mo
60
var job struct {
133728e barerepo 1mo
61
JobID int64 `json:"job_id"`
133728e barerepo 1mo
62
Repo string `json:"repo"`
133728e barerepo 1mo
63
SHA string `json:"sha"`
133728e barerepo 1mo
64
Command string `json:"command"`
133728e barerepo 1mo
65
CloneURL string `json:"clone_url"`
133728e barerepo 1mo
66
JobToken string `json:"job_token"`
133728e barerepo 1mo
67
}
133728e barerepo 1mo
68
poll := getBody(t, in, "/runner/poll?token="+url.QueryEscape(tok)+
133728e barerepo 1mo
69
"&id="+strconv.FormatInt(attached.RunnerID, 10))
133728e barerepo 1mo
70
if poll.StatusCode != http.StatusOK {
133728e barerepo 1mo
71
t.Fatalf("poll answered %d, so the push queued nothing: %s", poll.StatusCode, poll.body)
133728e barerepo 1mo
72
}
133728e barerepo 1mo
73
if err := json.Unmarshal([]byte(poll.body), &job); err != nil {
133728e barerepo 1mo
74
t.Fatal(err)
133728e barerepo 1mo
75
}
133728e barerepo 1mo
76
if job.Command != "echo hi" || job.Repo != "john/johnbot" || job.JobToken == "" {
133728e barerepo 1mo
77
t.Fatalf("the job is not what the repository asked for: %+v", job)
133728e barerepo 1mo
78
}
133728e barerepo 1mo
79
133728e barerepo 1mo
80
// POST /runner/log, then POST /runner/done. The log is the whole page, per chapter 16.
133728e barerepo 1mo
81
if r := postJSON(t, in, "/runner/log", map[string]any{
133728e barerepo 1mo
82
"token": tok, "job_id": job.JobID, "seq": 0, "chunk": "$ echo hi\nhi\n",
133728e barerepo 1mo
83
}); r.StatusCode != http.StatusNoContent {
133728e barerepo 1mo
84
t.Fatalf("log answered %d", r.StatusCode)
133728e barerepo 1mo
85
}
133728e barerepo 1mo
86
if r := postJSON(t, in, "/runner/done", map[string]any{
133728e barerepo 1mo
87
"token": tok, "job_id": job.JobID, "exit_code": 0, "duration": 1,
133728e barerepo 1mo
88
}); r.StatusCode != http.StatusNoContent {
133728e barerepo 1mo
89
t.Fatalf("done answered %d", r.StatusCode)
133728e barerepo 1mo
90
}
133728e barerepo 1mo
91
133728e barerepo 1mo
92
// And the run is on the page, with the log, which is the point of all four calls.
133728e barerepo 1mo
93
code, _, page := get(t, in.http.URL+"/john/johnbot/run/"+job.SHA)
133728e barerepo 1mo
94
if code != http.StatusOK {
133728e barerepo 1mo
95
t.Fatalf("the run page answered %d", code)
133728e barerepo 1mo
96
}
133728e barerepo 1mo
97
for _, want := range []string{"uproar.local", "$ echo hi", "build ok"} {
133728e barerepo 1mo
98
if !strings.Contains(page, want) {
133728e barerepo 1mo
99
t.Errorf("the run page is missing %q:\n%s", want, page)
133728e barerepo 1mo
100
}
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
}
133728e barerepo 1mo
103
133728e barerepo 1mo
104
func postJSON(t *testing.T, in *instance, path string, body map[string]any) formResult {
133728e barerepo 1mo
105
t.Helper()
133728e barerepo 1mo
106
raw, err := json.Marshal(body)
133728e barerepo 1mo
107
if err != nil {
133728e barerepo 1mo
108
t.Fatal(err)
133728e barerepo 1mo
109
}
133728e barerepo 1mo
110
resp, err := http.Post(in.http.URL+path, "application/json", bytes.NewReader(raw))
133728e barerepo 1mo
111
if err != nil {
133728e barerepo 1mo
112
t.Fatal(err)
133728e barerepo 1mo
113
}
133728e barerepo 1mo
114
defer resp.Body.Close()
133728e barerepo 1mo
115
return formResult{resp, readAll(t, resp)}
133728e barerepo 1mo
116
}
133728e barerepo 1mo
117
133728e barerepo 1mo
118
func getBody(t *testing.T, in *instance, path string) formResult {
133728e barerepo 1mo
119
t.Helper()
133728e barerepo 1mo
120
resp, err := http.Get(in.http.URL + path)
133728e barerepo 1mo
121
if err != nil {
133728e barerepo 1mo
122
t.Fatal(err)
133728e barerepo 1mo
123
}
133728e barerepo 1mo
124
defer resp.Body.Close()
133728e barerepo 1mo
125
return formResult{resp, readAll(t, resp)}
133728e barerepo 1mo
126
}
history · rawbarerepo 0.1.0