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

barerepo / runner / cmd/barerepo-runner/runner.go
250 lines · 6.6kb · master
log files threads runs releases config jump to file t
96966ee barerepo 1mo
1
package main
96966ee barerepo 1mo
2
96966ee barerepo 1mo
3
import (
96966ee barerepo 1mo
4
"bytes"
96966ee barerepo 1mo
5
"context"
96966ee barerepo 1mo
6
"encoding/json"
96966ee barerepo 1mo
7
"errors"
96966ee barerepo 1mo
8
"flag"
96966ee barerepo 1mo
9
"fmt"
96966ee barerepo 1mo
10
"net/http"
96966ee barerepo 1mo
11
"os"
96966ee barerepo 1mo
12
"os/exec"
96966ee barerepo 1mo
13
"path/filepath"
96966ee barerepo 1mo
14
"runtime"
96966ee barerepo 1mo
15
"strconv"
96966ee barerepo 1mo
16
"strings"
96966ee barerepo 1mo
17
"time"
96966ee barerepo 1mo
18
)
96966ee barerepo 1mo
19
96966ee barerepo 1mo
20
// cmdRunner long-polls outward, needing no inbound port, which is what makes pasting it work.
96966ee barerepo 1mo
21
func cmdRunner(ctx context.Context, args []string) error {
96966ee barerepo 1mo
22
if len(args) == 0 {
96966ee barerepo 1mo
23
return errors.New("usage: forge runner <token> [--labels a,b]")
96966ee barerepo 1mo
24
}
96966ee barerepo 1mo
25
tok := args[0]
96966ee barerepo 1mo
26
fs := flag.NewFlagSet("runner", flag.ContinueOnError)
96966ee barerepo 1mo
27
server := fs.String("server", "", "the forge to attach to")
96966ee barerepo 1mo
28
labels := fs.String("labels", "", "what this machine can build, comma separated")
96966ee barerepo 1mo
29
workdir := fs.String("workdir", "", "where to clone, defaults to a temporary directory")
96966ee barerepo 1mo
30
if err := fs.Parse(args[1:]); err != nil {
96966ee barerepo 1mo
31
return err
96966ee barerepo 1mo
32
}
96966ee barerepo 1mo
33
base := strings.TrimRight(*server, "/")
96966ee barerepo 1mo
34
if base == "" {
96966ee barerepo 1mo
35
base = os.Getenv("BAREREPO_SERVER")
96966ee barerepo 1mo
36
}
96966ee barerepo 1mo
37
if base == "" {
96966ee barerepo 1mo
38
// Appendix E's line has no flag, which works because the last attach is remembered.
96966ee barerepo 1mo
39
base = recallServer(tok)
96966ee barerepo 1mo
40
}
96966ee barerepo 1mo
41
if base == "" {
96966ee barerepo 1mo
42
return errors.New("this token has not attached anywhere yet.\n" +
96966ee barerepo 1mo
43
"paste the line from the add-a-runner page, or pass --server https://barerepo.example")
96966ee barerepo 1mo
44
}
96966ee barerepo 1mo
45
host, _ := os.Hostname()
96966ee barerepo 1mo
46
work := *workdir
96966ee barerepo 1mo
47
if work == "" {
96966ee barerepo 1mo
48
var err error
96966ee barerepo 1mo
49
if work, err = os.MkdirTemp("", "barerepo-runner-"); err != nil {
96966ee barerepo 1mo
50
return err
96966ee barerepo 1mo
51
}
96966ee barerepo 1mo
52
defer os.RemoveAll(work)
96966ee barerepo 1mo
53
}
96966ee barerepo 1mo
54
96966ee barerepo 1mo
55
r := &runner{base: base, token: tok, work: work,
96966ee barerepo 1mo
56
labels: splitCSV(*labels), client: &http.Client{Timeout: 2 * time.Minute}}
96966ee barerepo 1mo
57
if err := r.attach(ctx, host); err != nil {
96966ee barerepo 1mo
58
return err
96966ee barerepo 1mo
59
}
96966ee barerepo 1mo
60
// Remembered after it worked, so a wrong address is never the one recalled.
96966ee barerepo 1mo
61
rememberServer(tok, base)
96966ee barerepo 1mo
62
fmt.Printf("attached as %s. waiting for work.\n", host)
96966ee barerepo 1mo
63
for {
96966ee barerepo 1mo
64
if err := r.once(ctx); err != nil {
96966ee barerepo 1mo
65
if ctx.Err() != nil {
96966ee barerepo 1mo
66
return nil
96966ee barerepo 1mo
67
}
96966ee barerepo 1mo
68
fmt.Fprintln(os.Stderr, "forge: "+err.Error())
96966ee barerepo 1mo
69
time.Sleep(5 * time.Second)
96966ee barerepo 1mo
70
}
96966ee barerepo 1mo
71
}
96966ee barerepo 1mo
72
}
96966ee barerepo 1mo
73
96966ee barerepo 1mo
74
type runner struct {
96966ee barerepo 1mo
75
base string
96966ee barerepo 1mo
76
token string
96966ee barerepo 1mo
77
work string
96966ee barerepo 1mo
78
labels []string
96966ee barerepo 1mo
79
id int64
96966ee barerepo 1mo
80
client *http.Client
96966ee barerepo 1mo
81
}
96966ee barerepo 1mo
82
96966ee barerepo 1mo
83
func (r *runner) attach(ctx context.Context, hostname string) error {
96966ee barerepo 1mo
84
var out struct {
96966ee barerepo 1mo
85
RunnerID int64 `json:"runner_id"`
96966ee barerepo 1mo
86
PollInterval int `json:"poll_interval"`
96966ee barerepo 1mo
87
}
96966ee barerepo 1mo
88
err := r.post(ctx, "/runner/attach", map[string]any{
96966ee barerepo 1mo
89
"token": r.token, "hostname": hostname,
96966ee barerepo 1mo
90
"os": runtime.GOOS, "arch": runtime.GOARCH, "labels": r.labels,
96966ee barerepo 1mo
91
}, &out)
96966ee barerepo 1mo
92
if err != nil {
96966ee barerepo 1mo
93
return err
96966ee barerepo 1mo
94
}
96966ee barerepo 1mo
95
r.id = out.RunnerID
96966ee barerepo 1mo
96
return nil
96966ee barerepo 1mo
97
}
96966ee barerepo 1mo
98
96966ee barerepo 1mo
99
// once polls, and builds whatever comes back.
96966ee barerepo 1mo
100
func (r *runner) once(ctx context.Context) error {
96966ee barerepo 1mo
101
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
96966ee barerepo 1mo
102
fmt.Sprintf("%s/runner/poll?id=%d&token=%s", r.base, r.id, r.token), nil)
96966ee barerepo 1mo
103
if err != nil {
96966ee barerepo 1mo
104
return err
96966ee barerepo 1mo
105
}
96966ee barerepo 1mo
106
resp, err := r.client.Do(req)
96966ee barerepo 1mo
107
if err != nil {
96966ee barerepo 1mo
108
return err
96966ee barerepo 1mo
109
}
96966ee barerepo 1mo
110
defer resp.Body.Close()
96966ee barerepo 1mo
111
if resp.StatusCode == http.StatusNoContent {
96966ee barerepo 1mo
112
return nil
96966ee barerepo 1mo
113
}
96966ee barerepo 1mo
114
if resp.StatusCode != http.StatusOK {
96966ee barerepo 1mo
115
return fmt.Errorf("poll: %s", resp.Status)
96966ee barerepo 1mo
116
}
96966ee barerepo 1mo
117
var job jobOffer
96966ee barerepo 1mo
118
if err := json.NewDecoder(resp.Body).Decode(&job); err != nil {
96966ee barerepo 1mo
119
return err
96966ee barerepo 1mo
120
}
96966ee barerepo 1mo
121
fmt.Printf("building %s at %s\n", job.Repo, short(job.SHA))
96966ee barerepo 1mo
122
96966ee barerepo 1mo
123
started := time.Now()
96966ee barerepo 1mo
124
exit, output := r.build(ctx, job)
96966ee barerepo 1mo
125
if err := r.post(ctx, "/runner/log", map[string]any{
96966ee barerepo 1mo
126
"token": r.token, "job_id": job.JobID, "seq": 0, "chunk": output,
96966ee barerepo 1mo
127
}, nil); err != nil {
96966ee barerepo 1mo
128
return err
96966ee barerepo 1mo
129
}
96966ee barerepo 1mo
130
return r.post(ctx, "/runner/done", map[string]any{
96966ee barerepo 1mo
131
"token": r.token, "job_id": job.JobID,
96966ee barerepo 1mo
132
"exit_code": exit, "duration": int(time.Since(started).Seconds()),
96966ee barerepo 1mo
133
}, nil)
96966ee barerepo 1mo
134
}
96966ee barerepo 1mo
135
96966ee barerepo 1mo
136
// jobOffer is what a poll returns, which is everything a build needs and nothing else.
96966ee barerepo 1mo
137
type jobOffer struct {
96966ee barerepo 1mo
138
JobID int64 `json:"job_id"`
96966ee barerepo 1mo
139
Repo string `json:"repo"`
96966ee barerepo 1mo
140
Ref string `json:"ref"`
96966ee barerepo 1mo
141
SHA string `json:"sha"`
96966ee barerepo 1mo
142
Command string `json:"command"`
96966ee barerepo 1mo
143
Image string `json:"image"`
96966ee barerepo 1mo
144
CloneURL string `json:"clone_url"`
96966ee barerepo 1mo
145
JobToken string `json:"job_token"`
96966ee barerepo 1mo
146
}
96966ee barerepo 1mo
147
96966ee barerepo 1mo
148
// build clones at the commit and captures the whole output, which chapter 16 puts on one page.
96966ee barerepo 1mo
149
func (r *runner) build(ctx context.Context, job jobOffer) (int, string) {
96966ee barerepo 1mo
150
cloneURL, jobToken, sha, command := job.CloneURL, job.JobToken, job.SHA, job.Command
96966ee barerepo 1mo
151
dir := filepath.Join(r.work, short(sha))
96966ee barerepo 1mo
152
os.RemoveAll(dir)
96966ee barerepo 1mo
153
96966ee barerepo 1mo
154
if err := os.MkdirAll(dir, 0o750); err != nil {
96966ee barerepo 1mo
155
return 1, err.Error()
96966ee barerepo 1mo
156
}
96966ee barerepo 1mo
157
96966ee barerepo 1mo
158
var log bytes.Buffer
96966ee barerepo 1mo
159
withAuth := strings.Replace(cloneURL, "://", "://x:"+jobToken+"@", 1)
96966ee barerepo 1mo
160
// Every step runs in the clone, so the log shows the command and not a temporary path.
96966ee barerepo 1mo
161
steps := [][]string{
96966ee barerepo 1mo
162
{"git", "init", "-q"},
96966ee barerepo 1mo
163
{"git", "fetch", "-q", "--depth", "1", withAuth, sha},
96966ee barerepo 1mo
164
{"git", "checkout", "-q", "FETCH_HEAD"},
96966ee barerepo 1mo
165
}
96966ee barerepo 1mo
166
for _, step := range steps {
96966ee barerepo 1mo
167
shown := strings.Join(step, " ")
96966ee barerepo 1mo
168
fmt.Fprintf(&log, "$ %s\n", strings.ReplaceAll(shown, jobToken, "<token>"))
96966ee barerepo 1mo
169
cmd := exec.CommandContext(ctx, step[0], step[1:]...)
96966ee barerepo 1mo
170
cmd.Dir = dir
96966ee barerepo 1mo
171
cmd.Stdout = &log
96966ee barerepo 1mo
172
cmd.Stderr = &log
96966ee barerepo 1mo
173
if err := cmd.Run(); err != nil {
96966ee barerepo 1mo
174
fmt.Fprintf(&log, "%v\n", err)
96966ee barerepo 1mo
175
return 1, log.String()
96966ee barerepo 1mo
176
}
96966ee barerepo 1mo
177
}
96966ee barerepo 1mo
178
96966ee barerepo 1mo
179
fmt.Fprintf(&log, "$ %s\n", command)
96966ee barerepo 1mo
180
cmd := exec.CommandContext(ctx, "sh", "-c", command)
96966ee barerepo 1mo
181
cmd.Dir = dir
96966ee barerepo 1mo
182
cmd.Stdout = &log
96966ee barerepo 1mo
183
cmd.Stderr = &log
96966ee barerepo 1mo
184
// A build attaches a release file by speaking the same plain http the runner does. Chapter 22.5.
96966ee barerepo 1mo
185
cmd.Env = append(os.Environ(),
96966ee barerepo 1mo
186
"BAREREPO_URL="+r.base,
96966ee barerepo 1mo
187
"BAREREPO_REPO="+job.Repo,
96966ee barerepo 1mo
188
"BAREREPO_JOB="+strconv.FormatInt(job.JobID, 10),
96966ee barerepo 1mo
189
"BAREREPO_JOB_TOKEN="+jobToken,
96966ee barerepo 1mo
190
)
96966ee barerepo 1mo
191
err := cmd.Run()
96966ee barerepo 1mo
192
code := 0
96966ee barerepo 1mo
193
if err != nil {
96966ee barerepo 1mo
194
code = 1
96966ee barerepo 1mo
195
var ee *exec.ExitError
96966ee barerepo 1mo
196
if errors.As(err, &ee) {
96966ee barerepo 1mo
197
code = ee.ExitCode()
96966ee barerepo 1mo
198
}
96966ee barerepo 1mo
199
fmt.Fprintf(&log, "$ exit %d\n", code)
96966ee barerepo 1mo
200
}
96966ee barerepo 1mo
201
return code, log.String()
96966ee barerepo 1mo
202
}
96966ee barerepo 1mo
203
96966ee barerepo 1mo
204
func (r *runner) post(ctx context.Context, path string, body any, out any) error {
96966ee barerepo 1mo
205
buf, err := json.Marshal(body)
96966ee barerepo 1mo
206
if err != nil {
96966ee barerepo 1mo
207
return err
96966ee barerepo 1mo
208
}
96966ee barerepo 1mo
209
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.base+path, bytes.NewReader(buf))
96966ee barerepo 1mo
210
if err != nil {
96966ee barerepo 1mo
211
return err
96966ee barerepo 1mo
212
}
96966ee barerepo 1mo
213
req.Header.Set("Content-Type", "application/json")
96966ee barerepo 1mo
214
resp, err := r.client.Do(req)
96966ee barerepo 1mo
215
if err != nil {
96966ee barerepo 1mo
216
return err
96966ee barerepo 1mo
217
}
96966ee barerepo 1mo
218
defer resp.Body.Close()
96966ee barerepo 1mo
219
if resp.StatusCode >= 400 {
96966ee barerepo 1mo
220
msg, _ := readAll(resp.Body, 1<<12)
96966ee barerepo 1mo
221
return fmt.Errorf("%s: %s: %s", path, resp.Status, strings.TrimSpace(msg))
96966ee barerepo 1mo
222
}
222 of 250 lines · the whole file
history · rawbarerepo 0.1.0