v0.5.1 25d29f5

cache.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package host
4
5import (
6	"encoding/json"
7	"fmt"
8	"net/http"
9	"strings"
10	"time"
11
12	ermite "bnema.dev/ermite-api/gen/ermite"
13
14	"bnema.dev/ermite-runner/internal/cachesvc"
15	"bnema.dev/ermite-runner/internal/resultssvc"
16)
17
18// cacheTokenMargin is added to the job timeout for the life of the cache token:
19// the archive URLs of a lookup expire with it.
20const cacheTokenMargin = 15 * time.Minute
21
22// maxCacheReadRefs bounds Job.cache.read_refs (own ref, base branch, default
23// branch, with room to spare).
24const maxCacheReadRefs = 8
25
26// cacheScope is what the job may do in the cache of the runner.
27//
28// The server says (Job.cache, feature "ci.cache"): the repository, the one ref
29// the job may write and the refs it may read, most specific first. It is used
30// as given once checked: the repository must be the job's own (the
31// "owner/name" of the github context, or its name alone, which is how
32// Job.cache documents it) and the scope always carries the canonical
33// "owner/name", a ref must be a full "refs/..." name, empty refs
34// are dropped, and there are at most maxCacheReadRefs read refs. An empty
35// write_ref makes the job read-only. A scope that fails the checks is an error:
36// the job runs without a cache.
37//
38// A server that sends no Job.cache (an old server) gets the narrowest scope the
39// job's own fields give: the repository of the github context and the ref that
40// triggered the run, for reading and writing, with no fallback to the base or
41// default branch. A job without a repository or a ref has no cache (ok false,
42// no error).
43func cacheScope(job *ermite.Job, contexts string) (sc cachesvc.Scope, ok bool, err error) {
44	repo := githubRepository(contexts)
45	jc := job.GetCache()
46	if jc == nil {
47		if repo == "" || job.Ref == "" {
48			return cachesvc.Scope{}, false, nil
49		}
50		return cachesvc.Scope{Repository: repo, WriteRef: job.Ref, ReadRefs: []string{job.Ref}}, true, nil
51	}
52	name := repo[strings.LastIndex(repo, "/")+1:]
53	if jc.Repository == "" || repo == "" || (jc.Repository != repo && jc.Repository != name) {
54		return cachesvc.Scope{}, false, fmt.Errorf("the cache repository %q is not the job's repository %q", jc.Repository, repo)
55	}
56	// Always the job's canonical "owner/name", whichever way the server spelled
57	// it: entries of two repositories with the same name under different owners
58	// must never share a scope.
59	sc.Repository = repo
60	if w := jc.WriteRef; w != "" {
61		if !fullRef(w) {
62			return cachesvc.Scope{}, false, fmt.Errorf("the cache write ref %q is not a full ref name", w)
63		}
64		sc.WriteRef = w
65	}
66	for _, r := range jc.ReadRefs {
67		if r == "" {
68			continue
69		}
70		if !fullRef(r) {
71			return cachesvc.Scope{}, false, fmt.Errorf("the cache read ref %q is not a full ref name", r)
72		}
73		sc.ReadRefs = append(sc.ReadRefs, r)
74	}
75	if len(sc.ReadRefs) > maxCacheReadRefs {
76		return cachesvc.Scope{}, false, fmt.Errorf("%d cache read refs, at most %d", len(sc.ReadRefs), maxCacheReadRefs)
77	}
78	if sc.WriteRef == "" && len(sc.ReadRefs) == 0 {
79		return cachesvc.Scope{}, false, nil // allowed to do nothing
80	}
81	return sc, true, nil
82}
83
84// fullRef reports whether s is a full ref name: "refs/" and something after it,
85// without control characters or spaces.
86func fullRef(s string) bool {
87	if !strings.HasPrefix(s, "refs/") || len(s) == len("refs/") || len(s) > 1024 {
88		return false
89	}
90	for _, r := range s {
91		if r <= ' ' || r == 0x7f {
92			return false
93		}
94	}
95	return true
96}
97
98// githubRepository is github.repository ("owner/name") of the contexts JSON.
99func githubRepository(contexts string) string {
100	var c struct {
101		Github struct {
102			Repository string `json:"repository"`
103		} `json:"github"`
104	}
105	if json.Unmarshal([]byte(contexts), &c) != nil {
106		return ""
107	}
108	return c.Github.Repository
109}
110
111// runtimeFor returns what the executor needs of the job's runtime channel: the
112// port it has in the guest, the job's ACTIONS_RUNTIME_TOKEN, the cache mode and
113// whether the channel serves the results service. The channel exists when the
114// runner serves a cache and the job has a cache scope or the results service;
115// without it, 0, "", "" and false. A job with the results service and no cache
116// scope gets an empty mode (the executor then sets no ACTIONS_CACHE_URL) and a
117// token that the cache refuses.
118func (j *jobRun) runtimeFor(rt *Runner) (port uint32, token, mode string, results bool) {
119	if rt.Cache == nil {
120		return 0, "", "", false
121	}
122	sc, ok, err := cacheScope(j.job, j.contexts)
123	if err != nil {
124		// A bad scope from the server is not the job's failure: it runs without a
125		// cache.
126		j.log.Warn().Err(err).Msg("cache disabled for the job")
127		ok = false
128	}
129	if !ok {
130		sc = cachesvc.Scope{}
131	} else {
132		mode = "read"
133		if sc.WriteRef != "" {
134			mode = "write"
135		}
136	}
137	if !ok && !j.results {
138		return 0, "", "", false
139	}
140	var scopes []string
141	if j.results {
142		scopes = append(scopes, resultssvc.Scope(j.job.RunId, j.job.JobId, j.job.Attempt))
143	}
144	ttl := time.Duration(j.timeoutMinutes())*time.Minute + cacheTokenMargin
145	return cachesvc.DefaultPort, rt.Cache.Token(j.job.JobId, sc, ttl, scopes...), mode, j.results
146}
147
148// resultsOn says whether the job gets the results service: the runner has one
149// (ERMITE_RUNNER_ARTIFACTS is not 0) with a cache channel to serve it on, and
150// the server has the feature ci.artifacts. Without it artifact actions fail as
151// on a GitHub Enterprise Server without artifacts v4.
152func (j *jobRun) resultsOn(rt *Runner) bool {
153	return rt.Results != nil && rt.Cache != nil && rt.Artifacts != nil && j.features.artifacts
154}
155
156// resultsHandler is the results service of the job's channel, nil without one.
157// It calls ermite with the job token.
158func (j *jobRun) resultsHandler(rt *Runner) http.Handler {
159	if !j.results {
160		return nil
161	}
162	return rt.Results.Handler(resultssvc.Job{ID: j.job.JobId, RunID: j.job.RunId, Attempt: j.job.Attempt, Client: rt.Artifacts(j.job.JobToken)})
163}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close