v0.3.0 8f98fd3

cache.go RawHistoryFolder

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

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