Documentation
¶
Overview ¶
Package githubcache gives the dashboard's GitHub clients an HTTP cache that spends no rate-limit budget on answers it already has.
── WHY, IN ONE PARAGRAPH ───────────────────────────────────────────────────
Every GitHub REST response carries an `ETag` (and usually a `Last-Modified`). If a later request repeats it in `If-None-Match`, GitHub answers `304 Not Modified` with no body — and **a 304 does not count against the rate limit** (https://docs.github.com/rest/using-the-rest-api/best-practices#use-conditional-requests-if-appropriate). So the cheap request is not "the one we skipped", it is "the one we asked conditionally". That is what this package makes every GET do.
── FOLLOWING THE ORG'S PATTERN, NOT INVENTING ONE ──────────────────────────
`environment-controller` already caches GitHub through `sigs.k8s.io/prow`'s `ghcache` (`cmd/main.go`: `ghcache.NewMemCache(baseTransport, 10, …)`), which is built on `github.com/cjwagner/httpcache`. This package uses the same library and reproduces ghcache's two load-bearing decisions:
- **Partition the cache by `Authorization`.** ghcache hashes the header and keeps one cache per hash. The dashboard needs this even more than a controller does: `/api/rollouts/*/commits` acts as the VIEWING USER, so a shared cache would let one operator read a private repo through another operator's cached bytes. Partitioning makes that structurally impossible.
- **Rewrite the upstream `Cache-Control` so entries are stored but never served blind.** GitHub says `private, max-age=60`, which would let a cache answer for a minute WITHOUT asking. ghcache overwrites it with `no-cache` ("store it, always revalidate"), and errors with `no-store` ("never store it — an error cannot be revalidated into a saving").
What this package does NOT take from ghcache is prow's dependency tree (prometheus, redis, diskv, logrus) and its request coalescing/throttling, which exist for a fleet of bots sharing one token budget. This is a single-process dashboard with one cache per human.
── THE ONE PLACE STALENESS IS ALLOWED, AND WHY IT IS SAFE ──────────────────
A cache that serves a stale answer for MUTABLE data is worse than no cache. So `no-cache` (revalidate every time) is the default for everything — `/user`, a compare against a branch name, anything whose answer can change under a fixed URL.
`GET /repos/{owner}/{repo}/compare/{base}...{head}` where BOTH refs are commit shas is the exception: the set of commits between two immutable objects, and the diffstat over them, cannot change. That range is given a real freshness window (`ImmutableTTL`), so a repeat inside the window costs no request at all — not even a conditional one. The window is deliberately short rather than infinite because the ANSWER is immutable while the READER'S RIGHT TO SEE IT is not: a revoked collaborator keeps their own already-fetched bytes for at most one window.
── WHERE IT SITS IN THE TRANSPORT CHAIN, AND WHY IT MATTERS ────────────────
It goes in `http.Client.Transport`, UNDER go-github's auth round tripper — i.e. `github.NewClient(WithTransport(githubcache.Shared()), WithAuthToken(t))`. `newClient` applies `WithTransport` first and then wraps whatever is there with the token setter, so requests reach this package WITH their final `Authorization` header. Both properties depend on that ordering:
- the partition key exists at all (the header is set by the layer above), and
- a client that REFRESHES its credential (a GitHub App installation token is rotated roughly hourly) lands in a new partition instead of reusing an entry fetched under a grant it no longer holds.
Putting the cache ABOVE the auth layer would key every identity's responses together under one unauthenticated-looking request. That is the security bug this comment exists to prevent.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsCommitSHA ¶
IsCommitSHA reports whether a git ref is a commit sha — i.e. names an object that can never change — as opposed to a branch or tag name, which moves. This is the whole basis on which anything here is allowed to skip asking GitHub, so it is deliberately strict: hex only, 7 to 40 characters.
func IsImmutableRequest ¶
IsImmutableRequest reports whether a request's answer is fixed for all time: a compare of two commit shas. A compare against a branch or tag name is NOT immutable — the ref moves — and neither is any other endpoint.
func New ¶
func New(base http.RoundTripper, opts Options) http.RoundTripper
New builds a caching round tripper on top of base (nil means http.DefaultTransport).
func PartitionKey ¶
PartitionKey is the cache partition a request belongs to: a hash of its credential, so the key can never leak the credential itself.
func Shared ¶
func Shared() http.RoundTripper
Shared is the process-wide transport. One cache for the whole dashboard means two browser tabs asking the same question spend one conditional request.
Types ¶
type Options ¶
type Options struct {
// Maximum number of Authorization partitions kept at once. Each connected
// user is one partition; the least recently used is dropped past this.
MaxPartitions int
// Byte budget for one partition's stored responses.
MaxBytesPerPartition int
// Freshness window granted to an immutable sha-to-sha compare. Zero means
// "no window" — revalidate everything, always.
ImmutableTTL time.Duration
}
Options tunes the shared transport. The zero value is filled in by New.