-
Notifications
You must be signed in to change notification settings - Fork 0
feat(builds): implement two-tier build cache with per-repo token scopes #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Adds a two-tier caching system for builds that separates global (shared)
cache from tenant-specific cache, with fine-grained JWT token permissions.
Architecture:
- Global cache (cache/global/{runtime}): Shared read-only cache populated
by admin builds, available to all tenants
- Tenant cache (cache/{tenant}): Tenant-specific cache with push access
Token changes:
- New RepoPermission struct with per-repository scope (push/pull)
- GenerateToken() creates tokens with fine-grained repo access
- Backward compatible with legacy token format
Build flow:
- Regular builds: import from global + tenant cache, export to tenant only
- Admin builds: import from global cache, export to global cache
API changes:
- New is_admin_build field (operator-only, enables global cache push)
- New global_cache_runtime field (e.g., "node", "python", "go")
✱ Stainless preview buildsThis PR will update the
|
The "runtime" term was too specific. The global cache can be used for any shared cache category like base OS images, browser VMs, or runtimes. Renamed across all files: - GlobalCacheRuntime -> GlobalCacheKey - global_cache_runtime -> global_cache_key
| Message: "failed to read is_admin_build field", | ||
| }, nil | ||
| } | ||
| isAdminBuild = string(data) == "true" || string(data) == "1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing authorization check for admin build privilege
High Severity
The is_admin_build parameter is accepted from any authenticated user without authorization verification. The OpenAPI description claims this is "operator-only," but no enforcement exists. Any authenticated user can set is_admin_build=true to gain push access to the global cache (cache/global/{key}), potentially poisoning shared cache content that all tenant builds consume.
Additional Locations (1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but the endpoiont itself is protected by api key. I think this is fine for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the point tho
The previous regex `^/v2/([^/]+(?:/[^/]+)?)/` only captured up to 2
path segments, but global cache repos have 3 segments (cache/global/{key}).
Changed to `^/v2/(.+?)/(manifests|blobs)/` which uses non-greedy matching
to correctly extract the repo path before /manifests/ or /blobs/.
Fixes: cache/global/{key} paths were being extracted as cache/global,
causing auth failures since tokens grant access to the full 3-segment path.
| // Scope is the access scope for this repo: "pull" for read-only, "push" for read+write | ||
| Scope string `json:"scope"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we make scope like this
"scope": "admin:*",
to mean it can push to any
and like this for org only
"scope": "org:*",
| // GenerateToken creates a short-lived token with per-repository access permissions. | ||
| // This supports the two-tier cache model where different repos can have different scopes. | ||
| // For example: pull on cache/global/*, push on cache/{tenant} | ||
| func (g *RegistryTokenGenerator) GenerateToken(buildID string, repoAccess []RepoPermission, ttl time.Duration) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be nice to generate admin push token from "gen-jwt" script so when users run "hypeman-token" on the server where it's installed, they are getting an admin token that can push (would help me on something I'm working with)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, there are some follow ups on kernel repo side on the token control. I will keep it as a follow up for now
lib/middleware/oapi_auth.go
Outdated
| // registryPathPattern matches /v2/{repository}/(manifests|blobs)/... paths | ||
| // Supports 1-3 segment repos: builds/id, cache/tenant, cache/global/key | ||
| // Uses non-greedy match to capture repo path before /manifests/ or /blobs/ | ||
| var registryPathPattern = regexp.MustCompile(`^/v2/(.+?)/(manifests|blobs)/`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just fixed a bug that looks like this, maybe should work like this #71
maybe this regex misses specify by sha for example
| is_admin_build: | ||
| type: string | ||
| description: | | ||
| Set to "true" to grant push access to global cache (operator-only). | ||
| Admin builds can populate the shared global cache that all tenant builds read from. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we add the admin claim like "scope": "admin:*", then maybe we can just delete this parameter?
Resolved conflicts by accepting main's changes: - Use docker/distribution router for path extraction - Simplified token format (removed per-repo scopes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
|
|
||
| token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
| return token.SignedString(g.secret) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Middleware missing RepoAccess field breaks new token format
High Severity
The new GenerateToken function creates JWT tokens that only populate the RepoAccess field, leaving Repositories and Scope empty. However, the registry middleware in lib/middleware/oapi_auth.go has a separate RegistryTokenClaims struct (lines 27-32) that lacks the RepoAccess field. When the middleware's validateRegistryToken function parses new-format tokens, claims.Repositories is empty, causing the check len(claims.Repositories) == 0 at line 250 to fail with "not a registry token". This breaks authentication for all builds using the new token format.
The two-tier build cache PR (#70) introduced a new token format using `repo_access` field with per-repository scopes, but the middleware wasn't updated to parse it. This caused 401 Unauthorized errors when builder VMs tried to push images to the registry, as the middleware only checked for the legacy `repos` field which is empty in new tokens. Changes: - Add RepoPermission struct and RepoAccess field to RegistryTokenClaims - Update validateRegistryToken to check both RepoAccess (new) and Repositories (legacy) formats - Add per-repo scope checking for write operations - Add comprehensive tests for both token formats Fixes build failures in production where the new token format was being used but not recognized by the registry auth middleware.
Summary
Implements a two-tier caching system for builds that separates global (shared) cache from tenant-specific cache, with fine-grained JWT token permissions for secure access control.
Architecture
Changes
RepoPermissionstruct with per-repository scope (push/pull)is_admin_buildandglobal_cache_keyfields--import-cacheand--export-cacheflagsBuild Flow
is_admin_build=true)cache/global/{key}cache/global/{key}cache/global/{key}+cache/{tenant}cache/{tenant}Usage Examples
Test Plan
IsRepositoryAllowed,GetRepoScope,IsPushAllowedForRepo,IsPullAllowedForRepoNote
Introduces a two-tier BuildKit cache (global + tenant) and fine-grained registry JWT scopes, with API and builder integration.
is_admin_buildandglobal_cache_keyto config; configures BuildKit to import fromcache/global/{key}and tenantcache/{scope}(regular builds), and exports cache tocache/global/{key}for admin builds orcache/{scope}for regular builds (lib/builds/builder_agent/main.go).RepoPermissionandGenerateToken; extended claims and helpers (IsRepositoryAllowed,GetRepoScope, per-repo push/pull checks). Legacy token format retained (lib/builds/registry_token.go, tests added inlib/builds/registry_token_test.go).IsAdminBuild/GlobalCacheKeyinto builder config; refresh path updated similarly (lib/builds/manager.go).is_admin_buildandglobal_cache_keymultipart fields and passes them to domain request (cmd/api/api/builds.go).openapi.yaml).Written by Cursor Bugbot for commit 044a6ba. This will update automatically on new commits. Configure here.