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

barerepo / server / internal/httpd/runner.go
349 lines · 10kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package httpd
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"encoding/json"
133728e barerepo 1mo
5
"errors"
133728e barerepo 1mo
6
"net/http"
133728e barerepo 1mo
7
"strconv"
133728e barerepo 1mo
8
"strings"
133728e barerepo 1mo
9
"time"
133728e barerepo 1mo
10
133728e barerepo 1mo
11
"github.com/barerepo/server/internal/artifact"
133728e barerepo 1mo
12
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
13
"github.com/barerepo/server/internal/proposal"
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/repo"
133728e barerepo 1mo
15
"github.com/barerepo/server/internal/run"
133728e barerepo 1mo
16
"github.com/barerepo/server/internal/store"
133728e barerepo 1mo
17
"github.com/barerepo/server/internal/token"
133728e barerepo 1mo
18
)
133728e barerepo 1mo
19
133728e barerepo 1mo
20
// pollWait holds a poll open, since the runner dials out and needs no inbound port. 15.
133728e barerepo 1mo
21
const pollWait = 30 * time.Second
133728e barerepo 1mo
22
133728e barerepo 1mo
23
// pollTick is how often a held poll looks for work.
133728e barerepo 1mo
24
const pollTick = time.Second
133728e barerepo 1mo
25
133728e barerepo 1mo
26
// runnerAuth reads the token and names its repository, since chapter 15 scopes one to each.
133728e barerepo 1mo
27
func (s *Server) runnerAuth(r *http.Request, tok string) (*store.Token, error) {
133728e barerepo 1mo
28
if tok == "" {
133728e barerepo 1mo
29
return nil, errors.New("no token")
133728e barerepo 1mo
30
}
133728e barerepo 1mo
31
return s.DB.AccountForToken(r.Context(), token.Runner, tok)
133728e barerepo 1mo
32
}
133728e barerepo 1mo
33
133728e barerepo 1mo
34
type attachRequest struct {
133728e barerepo 1mo
35
Token string `json:"token"`
133728e barerepo 1mo
36
Hostname string `json:"hostname"`
133728e barerepo 1mo
37
OS string `json:"os"`
133728e barerepo 1mo
38
Arch string `json:"arch"`
133728e barerepo 1mo
39
Labels []string `json:"labels"`
133728e barerepo 1mo
40
}
133728e barerepo 1mo
41
133728e barerepo 1mo
42
type attachResponse struct {
133728e barerepo 1mo
43
RunnerID int64 `json:"runner_id"`
133728e barerepo 1mo
44
PollInterval int `json:"poll_interval"`
133728e barerepo 1mo
45
}
133728e barerepo 1mo
46
133728e barerepo 1mo
47
func (s *Server) serveRunnerAttach(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
48
var req attachRequest
133728e barerepo 1mo
49
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil {
133728e barerepo 1mo
50
http.Error(w, "malformed request", http.StatusBadRequest)
133728e barerepo 1mo
51
return
133728e barerepo 1mo
52
}
133728e barerepo 1mo
53
t, err := s.runnerAuth(r, req.Token)
133728e barerepo 1mo
54
if err != nil {
133728e barerepo 1mo
55
http.Error(w, "that token is not valid", http.StatusUnauthorized)
133728e barerepo 1mo
56
return
133728e barerepo 1mo
57
}
133728e barerepo 1mo
58
if req.Hostname == "" {
133728e barerepo 1mo
59
http.Error(w, "a runner needs a hostname", http.StatusBadRequest)
133728e barerepo 1mo
60
return
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
runner, err := s.DB.AttachRunner(r.Context(), t.ID, t.Scope,
133728e barerepo 1mo
63
req.Hostname, req.OS, req.Arch, req.Labels)
133728e barerepo 1mo
64
if err != nil {
133728e barerepo 1mo
65
s.oops(w, r, err)
133728e barerepo 1mo
66
return
133728e barerepo 1mo
67
}
133728e barerepo 1mo
68
writeJSON(w, attachResponse{RunnerID: runner.ID, PollInterval: int(pollWait.Seconds())})
133728e barerepo 1mo
69
}
133728e barerepo 1mo
70
133728e barerepo 1mo
71
type jobResponse struct {
133728e barerepo 1mo
72
JobID int64 `json:"job_id"`
133728e barerepo 1mo
73
Repo string `json:"repo"`
133728e barerepo 1mo
74
Ref string `json:"ref"`
133728e barerepo 1mo
75
SHA string `json:"sha"`
133728e barerepo 1mo
76
Command string `json:"command"`
133728e barerepo 1mo
77
Image string `json:"image,omitempty"`
133728e barerepo 1mo
78
CloneURL string `json:"clone_url"`
133728e barerepo 1mo
79
JobToken string `json:"job_token"`
133728e barerepo 1mo
80
}
133728e barerepo 1mo
81
133728e barerepo 1mo
82
// serveRunnerPoll holds the request open until there is work or the wait ends.
133728e barerepo 1mo
83
func (s *Server) serveRunnerPoll(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
84
t, err := s.runnerAuth(r, r.URL.Query().Get("token"))
133728e barerepo 1mo
85
if err != nil {
133728e barerepo 1mo
86
http.Error(w, "that token is not valid", http.StatusUnauthorized)
133728e barerepo 1mo
87
return
133728e barerepo 1mo
88
}
133728e barerepo 1mo
89
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
133728e barerepo 1mo
90
// The id is the caller's to say, so it has to name a machine this token attached. 15.
133728e barerepo 1mo
91
runner, err := s.runnerOf(r, t, id)
133728e barerepo 1mo
92
if err != nil || runner.Repo != t.Scope {
133728e barerepo 1mo
93
http.Error(w, "attach first", http.StatusNotFound)
133728e barerepo 1mo
94
return
133728e barerepo 1mo
95
}
133728e barerepo 1mo
96
if err := s.DB.SeeRunner(r.Context(), runner.ID); err != nil {
133728e barerepo 1mo
97
s.oops(w, r, err)
133728e barerepo 1mo
98
return
133728e barerepo 1mo
99
}
133728e barerepo 1mo
100
133728e barerepo 1mo
101
deadline := time.After(pollWait)
133728e barerepo 1mo
102
tick := time.NewTicker(pollTick)
133728e barerepo 1mo
103
defer tick.Stop()
133728e barerepo 1mo
104
for {
133728e barerepo 1mo
105
job, err := s.DB.TakeJob(r.Context(), runner.Repo, *runner)
133728e barerepo 1mo
106
if err != nil {
133728e barerepo 1mo
107
s.oops(w, r, err)
133728e barerepo 1mo
108
return
133728e barerepo 1mo
109
}
133728e barerepo 1mo
110
if job != nil {
133728e barerepo 1mo
111
s.handOut(w, r, t, job)
133728e barerepo 1mo
112
return
133728e barerepo 1mo
113
}
133728e barerepo 1mo
114
select {
133728e barerepo 1mo
115
case <-r.Context().Done():
133728e barerepo 1mo
116
return
133728e barerepo 1mo
117
case <-deadline:
133728e barerepo 1mo
118
w.WriteHeader(http.StatusNoContent)
133728e barerepo 1mo
119
return
133728e barerepo 1mo
120
case <-tick.C:
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
}
133728e barerepo 1mo
123
}
133728e barerepo 1mo
124
133728e barerepo 1mo
125
// handOut gives a job to a runner, with a token scoped to that one job.
133728e barerepo 1mo
126
func (s *Server) handOut(w http.ResponseWriter, r *http.Request, t *store.Token, job *store.Job) {
133728e barerepo 1mo
127
// The long-lived token stays on the runner, and a job carries one that dies with it.
133728e barerepo 1mo
128
jobToken, _, err := s.DB.CreateToken(r.Context(), token.Git, t.Account, job.Repo, jobLabel(job.ID))
133728e barerepo 1mo
129
if err != nil {
133728e barerepo 1mo
130
s.oops(w, r, err)
133728e barerepo 1mo
131
return
133728e barerepo 1mo
132
}
133728e barerepo 1mo
133
owner, name, _ := strings.Cut(job.Repo, "/")
133728e barerepo 1mo
134
writeJSON(w, jobResponse{
133728e barerepo 1mo
135
JobID: job.ID,
133728e barerepo 1mo
136
Repo: job.Repo,
133728e barerepo 1mo
137
Ref: job.Ref,
133728e barerepo 1mo
138
SHA: job.SHA,
133728e barerepo 1mo
139
Command: job.Command,
133728e barerepo 1mo
140
Image: job.Image,
133728e barerepo 1mo
141
CloneURL: s.Cfg.Server.ExternalURL + "/" + owner + "/" + name,
133728e barerepo 1mo
142
JobToken: jobToken,
133728e barerepo 1mo
143
})
133728e barerepo 1mo
144
}
133728e barerepo 1mo
145
133728e barerepo 1mo
146
type logRequest struct {
133728e barerepo 1mo
147
Token string `json:"token"`
133728e barerepo 1mo
148
JobID int64 `json:"job_id"`
133728e barerepo 1mo
149
Seq int `json:"seq"`
133728e barerepo 1mo
150
Chunk string `json:"chunk"`
133728e barerepo 1mo
151
}
133728e barerepo 1mo
152
133728e barerepo 1mo
153
func (s *Server) serveRunnerLog(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
154
var req logRequest
133728e barerepo 1mo
155
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
133728e barerepo 1mo
156
http.Error(w, "malformed request", http.StatusBadRequest)
133728e barerepo 1mo
157
return
133728e barerepo 1mo
158
}
133728e barerepo 1mo
159
_, runner, ok := s.jobRunner(w, r, req.Token, req.JobID)
133728e barerepo 1mo
160
if !ok {
133728e barerepo 1mo
161
return
133728e barerepo 1mo
162
}
133728e barerepo 1mo
163
if err := s.DB.AppendLog(r.Context(), req.JobID, runner.ID, req.Chunk); err != nil {
133728e barerepo 1mo
164
http.Error(w, "that job is not running", http.StatusConflict)
133728e barerepo 1mo
165
return
133728e barerepo 1mo
166
}
133728e barerepo 1mo
167
w.WriteHeader(http.StatusNoContent)
133728e barerepo 1mo
168
}
133728e barerepo 1mo
169
133728e barerepo 1mo
170
type doneRequest struct {
133728e barerepo 1mo
171
Token string `json:"token"`
133728e barerepo 1mo
172
JobID int64 `json:"job_id"`
133728e barerepo 1mo
173
ExitCode int `json:"exit_code"`
133728e barerepo 1mo
174
Duration int `json:"duration"`
133728e barerepo 1mo
175
}
133728e barerepo 1mo
176
133728e barerepo 1mo
177
// serveRunnerArtifact attaches one file to a release, which is chapter 22.5's job token permission.
133728e barerepo 1mo
178
func (s *Server) serveRunnerArtifact(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
179
tok := r.Header.Get("Barerepo-Token")
133728e barerepo 1mo
180
jobID, _ := strconv.ParseInt(r.Header.Get("Barerepo-Job"), 10, 64)
133728e barerepo 1mo
181
tag := r.Header.Get("Barerepo-Tag")
133728e barerepo 1mo
182
file := r.Header.Get("Barerepo-File")
133728e barerepo 1mo
183
job, ok := s.jobBearer(r, tok, jobID)
133728e barerepo 1mo
184
if !ok {
133728e barerepo 1mo
185
http.Error(w, "that token is not this job's", http.StatusUnauthorized)
133728e barerepo 1mo
186
return
133728e barerepo 1mo
187
}
133728e barerepo 1mo
188
owner, name, ok := strings.Cut(job.Repo, "/")
133728e barerepo 1mo
189
if !ok {
133728e barerepo 1mo
190
http.Error(w, "that job has no repository", http.StatusConflict)
133728e barerepo 1mo
191
return
133728e barerepo 1mo
192
}
133728e barerepo 1mo
193
// The token is scoped to one job and the job to one repository, so the tag must be in it. 22.5.
133728e barerepo 1mo
194
dir, err := repo.Dir(s.Cfg.Paths.Repos, owner, name)
133728e barerepo 1mo
195
if err != nil {
133728e barerepo 1mo
196
s.oops(w, r, err)
133728e barerepo 1mo
197
return
133728e barerepo 1mo
198
}
133728e barerepo 1mo
199
if _, err := gitx.ResolveRef(dir, "refs/tags/"+tag); err != nil {
133728e barerepo 1mo
200
http.Error(w, "there is no release tagged "+tag, http.StatusNotFound)
133728e barerepo 1mo
201
return
133728e barerepo 1mo
202
}
133728e barerepo 1mo
203
n, err := artifact.Put(s.Cfg.Paths.Artifacts, owner, name, tag, file, r.Body)
133728e barerepo 1mo
204
if err != nil {
133728e barerepo 1mo
205
http.Error(w, err.Error(), http.StatusBadRequest)
133728e barerepo 1mo
206
return
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
if s.Log != nil {
133728e barerepo 1mo
209
s.Log.Info("attached a release artifact",
133728e barerepo 1mo
210
"repo", job.Repo, "tag", tag, "file", file, "bytes", n)
133728e barerepo 1mo
211
}
133728e barerepo 1mo
212
w.WriteHeader(http.StatusNoContent)
133728e barerepo 1mo
213
}
133728e barerepo 1mo
214
133728e barerepo 1mo
215
// jobBearer authorizes a job's own token, which chapter 22.5 scopes to one repository and one job.
133728e barerepo 1mo
216
func (s *Server) jobBearer(r *http.Request, tok string, jobID int64) (*store.Job, bool) {
133728e barerepo 1mo
217
if tok == "" || jobID == 0 {
133728e barerepo 1mo
218
return nil, false
133728e barerepo 1mo
219
}
219 of 349 lines · the whole file
history · rawbarerepo 0.1.0