From 888017736618bc2eda549b05a944d1682d5a31c8 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Mon, 26 Jan 2026 17:19:39 -0500 Subject: [PATCH 1/4] feat(builds): implement two-tier build cache with per-repo token scopes 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") --- cmd/api/api/builds.go | 31 ++- lib/builds/builder_agent/main.go | 63 ++++-- lib/builds/manager.go | 98 +++++++-- lib/builds/registry_token.go | 126 +++++++++++- lib/builds/registry_token_test.go | 120 +++++++++++ lib/builds/types.go | 15 ++ lib/middleware/oapi_auth.go | 57 ++++-- lib/oapi/oapi.go | 326 ++++++++++++++++-------------- openapi.yaml | 11 + 9 files changed, 625 insertions(+), 222 deletions(-) diff --git a/cmd/api/api/builds.go b/cmd/api/api/builds.go index 4a8b153..17022a8 100644 --- a/cmd/api/api/builds.go +++ b/cmd/api/api/builds.go @@ -41,8 +41,9 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe // Parse multipart form fields var sourceData []byte - var baseImageDigest, cacheScope, dockerfile string + var baseImageDigest, cacheScope, dockerfile, globalCacheRuntime string var timeoutSeconds int + var isAdminBuild bool var secrets []builds.SecretRef for { @@ -118,6 +119,24 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe Message: "secrets must be a JSON array of {\"id\": \"...\", \"env_var\": \"...\"} objects", }, nil } + case "is_admin_build": + data, err := io.ReadAll(part) + if err != nil { + return oapi.CreateBuild400JSONResponse{ + Code: "invalid_request", + Message: "failed to read is_admin_build field", + }, nil + } + isAdminBuild = string(data) == "true" || string(data) == "1" + case "global_cache_runtime": + data, err := io.ReadAll(part) + if err != nil { + return oapi.CreateBuild400JSONResponse{ + Code: "invalid_request", + Message: "failed to read global_cache_runtime field", + }, nil + } + globalCacheRuntime = string(data) } part.Close() } @@ -134,10 +153,12 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe // Build domain request domainReq := builds.CreateBuildRequest{ - BaseImageDigest: baseImageDigest, - CacheScope: cacheScope, - Dockerfile: dockerfile, - Secrets: secrets, + BaseImageDigest: baseImageDigest, + CacheScope: cacheScope, + Dockerfile: dockerfile, + Secrets: secrets, + IsAdminBuild: isAdminBuild, + GlobalCacheRuntime: globalCacheRuntime, } // Apply timeout if provided diff --git a/lib/builds/builder_agent/main.go b/lib/builds/builder_agent/main.go index 0c645c6..c08dafe 100644 --- a/lib/builds/builder_agent/main.go +++ b/lib/builds/builder_agent/main.go @@ -37,17 +37,19 @@ const ( // BuildConfig matches the BuildConfig type from lib/builds/types.go type BuildConfig struct { - JobID string `json:"job_id"` - BaseImageDigest string `json:"base_image_digest,omitempty"` - RegistryURL string `json:"registry_url"` - RegistryToken string `json:"registry_token,omitempty"` - CacheScope string `json:"cache_scope,omitempty"` - SourcePath string `json:"source_path"` - Dockerfile string `json:"dockerfile,omitempty"` - BuildArgs map[string]string `json:"build_args,omitempty"` - Secrets []SecretRef `json:"secrets,omitempty"` - TimeoutSeconds int `json:"timeout_seconds"` - NetworkMode string `json:"network_mode"` + JobID string `json:"job_id"` + BaseImageDigest string `json:"base_image_digest,omitempty"` + RegistryURL string `json:"registry_url"` + RegistryToken string `json:"registry_token,omitempty"` + CacheScope string `json:"cache_scope,omitempty"` + SourcePath string `json:"source_path"` + Dockerfile string `json:"dockerfile,omitempty"` + BuildArgs map[string]string `json:"build_args,omitempty"` + Secrets []SecretRef `json:"secrets,omitempty"` + TimeoutSeconds int `json:"timeout_seconds"` + NetworkMode string `json:"network_mode"` + IsAdminBuild bool `json:"is_admin_build,omitempty"` + GlobalCacheRuntime string `json:"global_cache_runtime,omitempty"` } // SecretRef references a secret to inject during build @@ -547,11 +549,40 @@ func runBuild(ctx context.Context, config *BuildConfig, logWriter io.Writer) (st "--metadata-file", "/tmp/build-metadata.json", } - // Add cache if scope is set - if config.CacheScope != "" { - cacheRef := fmt.Sprintf("%s/cache/%s", config.RegistryURL, config.CacheScope) - args = append(args, "--import-cache", fmt.Sprintf("type=registry,ref=%s,registry.insecure=true", cacheRef)) - args = append(args, "--export-cache", fmt.Sprintf("type=registry,ref=%s,mode=max,registry.insecure=true", cacheRef)) + // Two-tier cache implementation: + // 1. Import from global cache (if runtime specified) - always read-only for regular builds + // 2. Import from tenant cache (if cache scope specified) + // 3. Export to appropriate target based on build type + + // Import from global cache (read-only for regular builds, read-write for admin builds) + if config.GlobalCacheRuntime != "" { + globalCacheRef := fmt.Sprintf("%s/cache/global/%s", config.RegistryURL, config.GlobalCacheRuntime) + args = append(args, "--import-cache", fmt.Sprintf("type=registry,ref=%s,registry.insecure=true", globalCacheRef)) + log.Printf("Importing from global cache: %s", globalCacheRef) + } + + // For regular builds, also import from tenant cache if scope is set + if !config.IsAdminBuild && config.CacheScope != "" { + tenantCacheRef := fmt.Sprintf("%s/cache/%s", config.RegistryURL, config.CacheScope) + args = append(args, "--import-cache", fmt.Sprintf("type=registry,ref=%s,registry.insecure=true", tenantCacheRef)) + log.Printf("Importing from tenant cache: %s", tenantCacheRef) + } + + // Export cache based on build type + if config.IsAdminBuild { + // Admin build: export to global cache + if config.GlobalCacheRuntime != "" { + globalCacheRef := fmt.Sprintf("%s/cache/global/%s", config.RegistryURL, config.GlobalCacheRuntime) + args = append(args, "--export-cache", fmt.Sprintf("type=registry,ref=%s,mode=max,registry.insecure=true", globalCacheRef)) + log.Printf("Exporting to global cache (admin build): %s", globalCacheRef) + } + } else { + // Regular build: export to tenant cache + if config.CacheScope != "" { + tenantCacheRef := fmt.Sprintf("%s/cache/%s", config.RegistryURL, config.CacheScope) + args = append(args, "--export-cache", fmt.Sprintf("type=registry,ref=%s,mode=max,registry.insecure=true", tenantCacheRef)) + log.Printf("Exporting to tenant cache: %s", tenantCacheRef) + } } // Add secret mounts diff --git a/lib/builds/manager.go b/lib/builds/manager.go index 72e1b0a..faa209e 100644 --- a/lib/builds/manager.go +++ b/lib/builds/manager.go @@ -191,16 +191,45 @@ func (m *manager) CreateBuild(ctx context.Context, req CreateBuildRequest, sourc } // Generate scoped registry token for this build - // Token grants push access to the build output repo and cache repo - allowedRepos := []string{fmt.Sprintf("builds/%s", id)} - if req.CacheScope != "" { - allowedRepos = append(allowedRepos, fmt.Sprintf("cache/%s", req.CacheScope)) - } + // Token grants per-repo access based on build type: + // - Regular builds: push to builds/{id}, push to cache/{tenant}, pull from cache/global/{runtime} + // - Admin builds: push to builds/{id}, push to cache/global/{runtime} tokenTTL := time.Duration(policy.TimeoutSeconds) * time.Second if tokenTTL < 30*time.Minute { tokenTTL = 30 * time.Minute // Minimum 30 minutes } - registryToken, err := m.tokenGenerator.GeneratePushToken(id, allowedRepos, tokenTTL) + + repoAccess := []RepoPermission{ + {Repo: fmt.Sprintf("builds/%s", id), Scope: "push"}, + } + + if req.IsAdminBuild { + // Admin build: push access to global cache + if req.GlobalCacheRuntime != "" { + repoAccess = append(repoAccess, RepoPermission{ + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Scope: "push", + }) + } + } else { + // Regular tenant build + // Pull access to global cache (if runtime specified) + if req.GlobalCacheRuntime != "" { + repoAccess = append(repoAccess, RepoPermission{ + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Scope: "pull", + }) + } + // Push access to tenant cache (if cache scope specified) + if req.CacheScope != "" { + repoAccess = append(repoAccess, RepoPermission{ + Repo: fmt.Sprintf("cache/%s", req.CacheScope), + Scope: "push", + }) + } + } + + registryToken, err := m.tokenGenerator.GenerateToken(id, repoAccess, tokenTTL) if err != nil { deleteBuild(m.paths, id) return nil, fmt.Errorf("generate registry token: %w", err) @@ -208,17 +237,19 @@ func (m *manager) CreateBuild(ctx context.Context, req CreateBuildRequest, sourc // Write build config for the builder agent buildConfig := &BuildConfig{ - JobID: id, - BaseImageDigest: req.BaseImageDigest, - RegistryURL: m.config.RegistryURL, - RegistryToken: registryToken, - CacheScope: req.CacheScope, - SourcePath: "/src", - Dockerfile: req.Dockerfile, - BuildArgs: req.BuildArgs, - Secrets: req.Secrets, - TimeoutSeconds: policy.TimeoutSeconds, - NetworkMode: policy.NetworkMode, + JobID: id, + BaseImageDigest: req.BaseImageDigest, + RegistryURL: m.config.RegistryURL, + RegistryToken: registryToken, + CacheScope: req.CacheScope, + SourcePath: "/src", + Dockerfile: req.Dockerfile, + BuildArgs: req.BuildArgs, + Secrets: req.Secrets, + TimeoutSeconds: policy.TimeoutSeconds, + NetworkMode: policy.NetworkMode, + IsAdminBuild: req.IsAdminBuild, + GlobalCacheRuntime: req.GlobalCacheRuntime, } if err := writeBuildConfig(m.paths, id, buildConfig); err != nil { deleteBuild(m.paths, id) @@ -1046,14 +1077,37 @@ func (m *manager) refreshBuildToken(buildID string, req *CreateBuildRequest) err tokenTTL = 30 * time.Minute // Minimum 30 minutes } - // Generate allowed repos list - allowedRepos := []string{fmt.Sprintf("builds/%s", buildID)} - if req.CacheScope != "" { - allowedRepos = append(allowedRepos, fmt.Sprintf("cache/%s", req.CacheScope)) + // Generate per-repo access list (same logic as CreateBuild) + repoAccess := []RepoPermission{ + {Repo: fmt.Sprintf("builds/%s", buildID), Scope: "push"}, + } + + if req.IsAdminBuild { + // Admin build: push access to global cache + if req.GlobalCacheRuntime != "" { + repoAccess = append(repoAccess, RepoPermission{ + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Scope: "push", + }) + } + } else { + // Regular tenant build + if req.GlobalCacheRuntime != "" { + repoAccess = append(repoAccess, RepoPermission{ + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Scope: "pull", + }) + } + if req.CacheScope != "" { + repoAccess = append(repoAccess, RepoPermission{ + Repo: fmt.Sprintf("cache/%s", req.CacheScope), + Scope: "push", + }) + } } // Generate fresh registry token - registryToken, err := m.tokenGenerator.GeneratePushToken(buildID, allowedRepos, tokenTTL) + registryToken, err := m.tokenGenerator.GenerateToken(buildID, repoAccess, tokenTTL) if err != nil { return fmt.Errorf("generate registry token: %w", err) } diff --git a/lib/builds/registry_token.go b/lib/builds/registry_token.go index 6c8cf44..3a2f8ab 100644 --- a/lib/builds/registry_token.go +++ b/lib/builds/registry_token.go @@ -8,19 +8,35 @@ import ( "github.com/golang-jwt/jwt/v5" ) +// RepoPermission defines access permissions for a specific repository. +// This enables fine-grained control where different repos can have different scopes. +type RepoPermission struct { + // Repo is the repository path (e.g., "builds/abc123", "cache/tenant-x", "cache/global/node") + Repo string `json:"repo"` + + // Scope is the access scope for this repo: "pull" for read-only, "push" for read+write + Scope string `json:"scope"` +} + // RegistryTokenClaims contains the claims for a scoped registry access token. -// These tokens are issued to builder VMs to grant limited push access to specific repositories. +// These tokens are issued to builder VMs to grant limited access to specific repositories. type RegistryTokenClaims struct { jwt.RegisteredClaims // BuildID is the build job identifier for audit purposes BuildID string `json:"build_id"` - // Repositories is the list of allowed repository paths (e.g., ["builds/abc123", "cache/tenant-x"]) - Repositories []string `json:"repos"` + // RepoAccess defines per-repository access permissions (new two-tier format) + // If present, this takes precedence over the legacy Repositories/Scope fields + RepoAccess []RepoPermission `json:"repo_access,omitempty"` - // Scope is the access scope: "push" for write access, "pull" for read-only - Scope string `json:"scope"` + // Repositories is the list of allowed repository paths (legacy format, kept for backward compat) + // Deprecated: Use RepoAccess for new tokens + Repositories []string `json:"repos,omitempty"` + + // Scope is the access scope (legacy format, kept for backward compat) + // Deprecated: Use RepoAccess for new tokens + Scope string `json:"scope,omitempty"` } // RegistryTokenGenerator creates scoped registry access tokens @@ -37,6 +53,7 @@ func NewRegistryTokenGenerator(secret string) *RegistryTokenGenerator { // GeneratePushToken creates a short-lived token granting push access to specific repositories. // The token expires after the specified duration (typically matching the build timeout). +// Deprecated: Use GenerateToken for new code that needs per-repo scopes. func (g *RegistryTokenGenerator) GeneratePushToken(buildID string, repos []string, ttl time.Duration) (string, error) { if buildID == "" { return "", fmt.Errorf("build ID is required") @@ -62,6 +79,33 @@ func (g *RegistryTokenGenerator) GeneratePushToken(buildID string, repos []strin return token.SignedString(g.secret) } +// 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) { + if buildID == "" { + return "", fmt.Errorf("build ID is required") + } + if len(repoAccess) == 0 { + return "", fmt.Errorf("at least one repository permission is required") + } + + now := time.Now() + claims := RegistryTokenClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "builder-" + buildID, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), + Issuer: "hypeman", + }, + BuildID: buildID, + RepoAccess: repoAccess, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(g.secret) +} + // ValidateToken parses and validates a registry token, returning the claims if valid. func (g *RegistryTokenGenerator) ValidateToken(tokenString string) (*RegistryTokenClaims, error) { token, err := jwt.ParseWithClaims(tokenString, &RegistryTokenClaims{}, func(token *jwt.Token) (interface{}, error) { @@ -86,6 +130,17 @@ func (g *RegistryTokenGenerator) ValidateToken(tokenString string) (*RegistryTok // IsRepositoryAllowed checks if the given repository path is allowed by the token claims. func (c *RegistryTokenClaims) IsRepositoryAllowed(repo string) bool { + // Check new per-repo access format first + if len(c.RepoAccess) > 0 { + for _, perm := range c.RepoAccess { + if perm.Repo == repo { + return true + } + } + return false + } + + // Fall back to legacy format for _, allowed := range c.Repositories { if allowed == repo { return true @@ -94,13 +149,72 @@ func (c *RegistryTokenClaims) IsRepositoryAllowed(repo string) bool { return false } -// IsPushAllowed returns true if the token grants push (write) access. +// GetRepoScope returns the scope for a specific repository. +// Returns empty string if the repository is not allowed. +func (c *RegistryTokenClaims) GetRepoScope(repo string) string { + // Check new per-repo access format first + if len(c.RepoAccess) > 0 { + for _, perm := range c.RepoAccess { + if perm.Repo == repo { + return perm.Scope + } + } + return "" + } + + // Fall back to legacy format - all repos have the same scope + for _, allowed := range c.Repositories { + if allowed == repo { + return c.Scope + } + } + return "" +} + +// IsPushAllowedForRepo returns true if the token grants push (write) access to the given repo. +func (c *RegistryTokenClaims) IsPushAllowedForRepo(repo string) bool { + scope := c.GetRepoScope(repo) + return scope == "push" +} + +// IsPullAllowedForRepo returns true if the token grants pull (read) access to the given repo. +// Push scope also implicitly grants pull access. +func (c *RegistryTokenClaims) IsPullAllowedForRepo(repo string) bool { + scope := c.GetRepoScope(repo) + return scope == "push" || scope == "pull" +} + +// IsPushAllowed returns true if the token grants push (write) access to any repo. +// Deprecated: Use IsPushAllowedForRepo for per-repo scope checking. func (c *RegistryTokenClaims) IsPushAllowed() bool { + // Check new per-repo access format first + if len(c.RepoAccess) > 0 { + for _, perm := range c.RepoAccess { + if perm.Scope == "push" { + return true + } + } + return false + } + + // Fall back to legacy format return c.Scope == "push" } // IsPullAllowed returns true if the token grants pull (read) access. // Push tokens also implicitly grant pull access. +// Deprecated: Use IsPullAllowedForRepo for per-repo scope checking. func (c *RegistryTokenClaims) IsPullAllowed() bool { + // Check new per-repo access format first + if len(c.RepoAccess) > 0 { + for _, perm := range c.RepoAccess { + if perm.Scope == "push" || perm.Scope == "pull" { + return true + } + } + return false + } + + // Fall back to legacy format return c.Scope == "push" || c.Scope == "pull" } diff --git a/lib/builds/registry_token_test.go b/lib/builds/registry_token_test.go index 1231e07..dcb36bb 100644 --- a/lib/builds/registry_token_test.go +++ b/lib/builds/registry_token_test.go @@ -109,3 +109,123 @@ func TestRegistryTokenClaims_IsPushAllowed(t *testing.T) { assert.True(t, claims.IsPullAllowed()) }) } + +func TestRegistryTokenGenerator_GenerateToken(t *testing.T) { + generator := NewRegistryTokenGenerator("test-secret-key") + + t.Run("valid token with per-repo permissions", func(t *testing.T) { + repoAccess := []RepoPermission{ + {Repo: "builds/build-123", Scope: "push"}, + {Repo: "cache/global/node", Scope: "pull"}, + {Repo: "cache/tenant-x", Scope: "push"}, + } + token, err := generator.GenerateToken("build-123", repoAccess, 30*time.Minute) + require.NoError(t, err) + assert.NotEmpty(t, token) + + // Validate the token + claims, err := generator.ValidateToken(token) + require.NoError(t, err) + assert.Equal(t, "build-123", claims.BuildID) + assert.Equal(t, repoAccess, claims.RepoAccess) + assert.Equal(t, "builder-build-123", claims.Subject) + assert.Equal(t, "hypeman", claims.Issuer) + }) + + t.Run("empty build ID", func(t *testing.T) { + _, err := generator.GenerateToken("", []RepoPermission{{Repo: "builds/build-123", Scope: "push"}}, 30*time.Minute) + require.Error(t, err) + assert.Contains(t, err.Error(), "build ID is required") + }) + + t.Run("empty repo access", func(t *testing.T) { + _, err := generator.GenerateToken("build-123", []RepoPermission{}, 30*time.Minute) + require.Error(t, err) + assert.Contains(t, err.Error(), "at least one repository permission is required") + }) +} + +func TestRegistryTokenClaims_RepoAccess(t *testing.T) { + // Test claims with new per-repo access format + claims := &RegistryTokenClaims{ + RepoAccess: []RepoPermission{ + {Repo: "builds/abc123", Scope: "push"}, + {Repo: "cache/global/node", Scope: "pull"}, + {Repo: "cache/tenant-x", Scope: "push"}, + }, + } + + t.Run("IsRepositoryAllowed with RepoAccess", func(t *testing.T) { + assert.True(t, claims.IsRepositoryAllowed("builds/abc123")) + assert.True(t, claims.IsRepositoryAllowed("cache/global/node")) + assert.True(t, claims.IsRepositoryAllowed("cache/tenant-x")) + assert.False(t, claims.IsRepositoryAllowed("builds/other")) + assert.False(t, claims.IsRepositoryAllowed("cache/global/python")) + }) + + t.Run("GetRepoScope", func(t *testing.T) { + assert.Equal(t, "push", claims.GetRepoScope("builds/abc123")) + assert.Equal(t, "pull", claims.GetRepoScope("cache/global/node")) + assert.Equal(t, "push", claims.GetRepoScope("cache/tenant-x")) + assert.Equal(t, "", claims.GetRepoScope("builds/other")) + }) + + t.Run("IsPushAllowedForRepo", func(t *testing.T) { + assert.True(t, claims.IsPushAllowedForRepo("builds/abc123")) + assert.False(t, claims.IsPushAllowedForRepo("cache/global/node")) + assert.True(t, claims.IsPushAllowedForRepo("cache/tenant-x")) + assert.False(t, claims.IsPushAllowedForRepo("builds/other")) + }) + + t.Run("IsPullAllowedForRepo", func(t *testing.T) { + assert.True(t, claims.IsPullAllowedForRepo("builds/abc123")) // push implies pull + assert.True(t, claims.IsPullAllowedForRepo("cache/global/node")) + assert.True(t, claims.IsPullAllowedForRepo("cache/tenant-x")) // push implies pull + assert.False(t, claims.IsPullAllowedForRepo("builds/other")) + }) + + t.Run("IsPushAllowed with mixed scopes", func(t *testing.T) { + assert.True(t, claims.IsPushAllowed()) // At least one repo has push + }) + + t.Run("IsPullAllowed with mixed scopes", func(t *testing.T) { + assert.True(t, claims.IsPullAllowed()) + }) +} + +func TestRegistryTokenClaims_RepoAccessPullOnly(t *testing.T) { + // Test claims with only pull access + claims := &RegistryTokenClaims{ + RepoAccess: []RepoPermission{ + {Repo: "cache/global/node", Scope: "pull"}, + }, + } + + t.Run("IsPushAllowed returns false for pull-only token", func(t *testing.T) { + assert.False(t, claims.IsPushAllowed()) + }) + + t.Run("IsPullAllowed returns true for pull-only token", func(t *testing.T) { + assert.True(t, claims.IsPullAllowed()) + }) +} + +func TestRegistryTokenClaims_LegacyFallback(t *testing.T) { + // Test that legacy format still works when RepoAccess is empty + claims := &RegistryTokenClaims{ + Repositories: []string{"builds/abc123", "cache/tenant-x"}, + Scope: "push", + } + + t.Run("IsRepositoryAllowed uses legacy format", func(t *testing.T) { + assert.True(t, claims.IsRepositoryAllowed("builds/abc123")) + assert.True(t, claims.IsRepositoryAllowed("cache/tenant-x")) + assert.False(t, claims.IsRepositoryAllowed("builds/other")) + }) + + t.Run("GetRepoScope uses legacy format", func(t *testing.T) { + assert.Equal(t, "push", claims.GetRepoScope("builds/abc123")) + assert.Equal(t, "push", claims.GetRepoScope("cache/tenant-x")) + assert.Equal(t, "", claims.GetRepoScope("builds/other")) + }) +} diff --git a/lib/builds/types.go b/lib/builds/types.go index 08b0588..034fb87 100644 --- a/lib/builds/types.go +++ b/lib/builds/types.go @@ -52,6 +52,15 @@ type CreateBuildRequest struct { // Secrets are secret references to inject during build Secrets []SecretRef `json:"secrets,omitempty"` + + // IsAdminBuild grants push access to global cache (operator-only). + // Regular tenant builds only get pull access to global cache. + IsAdminBuild bool `json:"is_admin_build,omitempty"` + + // GlobalCacheRuntime is the runtime category for global cache (e.g., "node", "python"). + // Used with IsAdminBuild to target cache/global/{runtime}. + // Regular builds import from cache/global/{runtime} with pull-only access. + GlobalCacheRuntime string `json:"global_cache_runtime,omitempty"` } // BuildPolicy defines resource limits and network policy for a build @@ -137,6 +146,12 @@ type BuildConfig struct { // NetworkMode is "isolated" or "egress" NetworkMode string `json:"network_mode"` + + // IsAdminBuild indicates this is an admin build with push access to global cache + IsAdminBuild bool `json:"is_admin_build,omitempty"` + + // GlobalCacheRuntime is the runtime category for global cache (e.g., "node", "python") + GlobalCacheRuntime string `json:"global_cache_runtime,omitempty"` } // BuildEvent represents a typed SSE event for build streaming diff --git a/lib/middleware/oapi_auth.go b/lib/middleware/oapi_auth.go index 98c9c6c..4c06955 100644 --- a/lib/middleware/oapi_auth.go +++ b/lib/middleware/oapi_auth.go @@ -20,13 +20,22 @@ const userIDKey contextKey = "user_id" // registryPathPattern matches /v2/{repository}/... paths var registryPathPattern = regexp.MustCompile(`^/v2/([^/]+(?:/[^/]+)?)/`) +// RepoPermission defines access permissions for a specific repository. +// This mirrors the type in lib/builds/registry_token.go to avoid circular imports. +type RepoPermission struct { + Repo string `json:"repo"` + Scope string `json:"scope"` +} + // RegistryTokenClaims contains the claims for a scoped registry access token. // This mirrors the type in lib/builds/registry_token.go to avoid circular imports. type RegistryTokenClaims struct { jwt.RegisteredClaims - BuildID string `json:"build_id"` - Repositories []string `json:"repos"` - Scope string `json:"scope"` + BuildID string `json:"build_id"` + RepoAccess []RepoPermission `json:"repo_access,omitempty"` // New per-repo format + // Legacy fields (kept for backward compat) + Repositories []string `json:"repos,omitempty"` + Scope string `json:"scope,omitempty"` } // OapiAuthenticationFunc creates an AuthenticationFunc compatible with nethttp-middleware @@ -233,8 +242,8 @@ func validateRegistryToken(tokenString, jwtSecret, requestPath, method string) ( return nil, fmt.Errorf("invalid token") } - // Check if this is a registry token (has repos claim) - if len(claims.Repositories) == 0 { + // Check if this is a registry token (has repos claim or repo_access claim) + if len(claims.RepoAccess) == 0 && len(claims.Repositories) == 0 { return nil, fmt.Errorf("not a registry token") } @@ -248,26 +257,42 @@ func validateRegistryToken(tokenString, jwtSecret, requestPath, method string) ( return nil, fmt.Errorf("could not extract repository from path") } - // Check if the repository is allowed by the token - allowed := false - for _, allowedRepo := range claims.Repositories { - if allowedRepo == repo { - allowed = true - break - } - } - if !allowed { + // Check if the repository is allowed and get the scope for this repo + repoScope := getRepoScopeFromClaims(claims, repo) + if repoScope == "" { return nil, fmt.Errorf("repository %s not allowed by token", repo) } // Check scope for write operations - if isWriteOperation(method) && claims.Scope != "push" { - return nil, fmt.Errorf("token does not allow write operations") + if isWriteOperation(method) && repoScope != "push" { + return nil, fmt.Errorf("token does not allow write operations to %s (scope: %s)", repo, repoScope) } return claims, nil } +// getRepoScopeFromClaims returns the scope for a specific repository from the token claims. +// Returns empty string if the repository is not allowed. +func getRepoScopeFromClaims(claims *RegistryTokenClaims, repo string) string { + // Check new per-repo access format first + if len(claims.RepoAccess) > 0 { + for _, perm := range claims.RepoAccess { + if perm.Repo == repo { + return perm.Scope + } + } + return "" + } + + // Fall back to legacy format - all repos have the same scope + for _, allowedRepo := range claims.Repositories { + if allowedRepo == repo { + return claims.Scope + } + } + return "" +} + // JwtAuth creates a chi middleware that validates JWT bearer tokens func JwtAuth(jwtSecret string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index c2cead2..93d6b35 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -821,6 +821,15 @@ type CreateBuildMultipartBody struct { // Dockerfile Dockerfile content. Required if not included in the source tarball. Dockerfile *string `json:"dockerfile,omitempty"` + // GlobalCacheRuntime Runtime category for global cache (e.g., "node", "python", "go"). + // When specified, the build will import from cache/global/{runtime}. + // Admin builds will also export to this location. + GlobalCacheRuntime *string `json:"global_cache_runtime,omitempty"` + + // IsAdminBuild 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. + IsAdminBuild *string `json:"is_admin_build,omitempty"` + // Secrets JSON array of secret references to inject during build. // Each object has "id" (required) for use with --mount=type=secret,id=... // Example: [{"id": "npm_token"}, {"id": "github_token"}] @@ -10237,163 +10246,166 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x97XIbOa7oq7D6nq2Vz0qy/BHH0amtW46dZLwbJ75x7L1nx7kK1U1JHHeTHZIt20nl", - "7zzAPOI8yS2CZH+JLbXy4cQn2dqa2G5+AQRAAASBD0HIk5QzwpQMhh8CGc5IguHHA6VwOLvgcZaQV+Rd", - "RqTSf04FT4lQlECjhGdMjVKsZvq3iMhQ0FRRzoJhcIrVDF3PiCBoDqMgOeNZHKExQdCPREE3IDc4SWMS", - "DIPNhKnNCCscdAN1m+o/SSUomwYfu4EgOOIsvjXTTHAWq2A4wbEk3dq0J3pohCXSXXrQJx9vzHlMMAs+", - "wojvMipIFAx/LYPxJm/Mx7+RUOnJD+aYxngckyMypyFZREOYCUGYGkWCzolYRMWh+R7fojHPWIRMO9Rh", - "WRwjOkGMM7JRQQab04hqTOgmeupgqERGPJiJYE0jGnl24PAYmc/o+Ah1ZuSmOsn2w/F+0DwkwwlZHPSX", - "LMGsp5Grl+XGh7blsZ/v+kamPEmy0VTwLF0c+fjlyck5go+IZcmYiPKI+9v5eJQpMiVCD5iGdISjSBAp", - "/fC7j+W1DQaDwRBvDweD/sC3yjlhEReNKDWf/SjdGkRkyZCtUGrHX0Dpi4vjo+MDdMhFygWGvgsz1Qi7", - "jJ4yXGWyqe6Kj/4fZzSOPFTP9cIUiUZYLQIFnZBtQzlDiiZEKpykQTeYcJHoTkGEFenpL21IPRQEr5hO", - "t2g12SLRZwano0Q2je6aIMpQQuOYShJyFsnyHJSpvd1mYEqkS4TgHlnxRP8ZJURKPCWoowWYlqIMSYVV", - "JhGVaIJpTKKNNijz0bAB5jc+RjQiTNEJrXJaMNYNengcbm3veLk4wVMyiujUngnV4Y/g74hPkB5HIWjt", - "B0ST/G07OGBKQSaL8z0FIQqTCDIhgrDws6dLBZ8ThpkR9v8B8wb/a7M4LDftSbkJyDwtmn/sBu8ykpFR", - "yiU1K1yQIfaLJiNANYIe/jXDp2V7XaIoqbBYzh/Q4gtwollfK9ycmaZ1yQSCxw5T4exGAfRkTpjySSGm", - "7IcqxM/5FMWUEWRbWPxOuEB6gr/HfLoRfBnYukGB0kWG1uv+BIFk/tAwmv7WDQjLEo3MmE/L2JwRLNSY", - "VJDZcEDYgYrVNaL/tMIS1T0YY0lGy6XCKWWMREi3tMxqWqJMgh64AD5wxhVVozkR0stHsKx/UoVsi8ah", - "Yh5eTWhMRjMsZ2bFOIqAB3F8WoHEowtVlEucasHmBoQzWiLF0dkvB9sP9pCdwINDyTMRmhUsQlLqrYc3", - "bZHCYozj2EsbzeS2/rm7SCF+CjjLGaPpPMkp0BGmkV6B3U09fDdIMzkzP4E81quC80yLAU1esf75jQfo", - "QxASRgdvtEj8GtbL1Gw2msZc4/QWZYy+yyrqax8da01cIS38aUSiLsLwQYthnCnemxJGhJZTaCJ4gtSM", - "oJKKiTqkP+130aXWunpax+zh7d5g0BtcBlUlMd7tTdNMowIrRYRe4P/7FffeH/T+Peg9elP8OOr33vzt", - "P3wE0Fbv1eSk12nh7Dje7yK32LIyXF/ockV5ia7pkyJm+44176+7e4fHiwe8WX/Ewysi+pRvxnQssLjd", - "ZFPKboYxVkSqKjTL266ED9a2BDA21aCvCVpN9Qdy68T8mohQS8qYaAKRXS0sqZJdhLX1CEIG6dPsv1CI", - "maZZc7BzgQiL0DVVM4ShXRUDyW0Pp7RHzVKDbpDgm+eETbX5vrezQI+aGDv2h96b/3R/2vjfXpIUWUw8", - "xPiKZ4qyKYLP5vSdUYmKNVBFkpXHrcNuFoOKlVB2bLpt5SvBQuBb/665xS3bPam08GncPsNAHviOnIEt", - "kTXa4EDA4D4BeJ+dnm9qlkyxlGomeDadlXflVycP3pRw0aANOCC7QUTl1Yjy0Tj1rYnKK3S8+RJpaYVi", - "mlBVSKetweDk8aa8DPQvD9wvG310ZPwqsHwNPBdWaMoZFgSO7ghxhg5PzxGOYx5aY2iiNawJnWaCRP2a", - "NQyj+6iFsPlnnMNP2JwKzhKtC82xoJp5Kjb+h+DFy6MnoycvLoKh3skoC63BfPry1etgGOwMBoPAd9Tp", - "nVhBjM9Ozw8BYt1+xlUaZ9ORpO9JxTsV7Dx7HNQXfpDDixKScGH0UTsG6syq4sAc1yimVwRd6vHMpm09", - "qwvqbZhqAWmz25SIOZU+O/OX/Jve70ySMm8aZqiShCRiTkS+17D5/dJZH8Y8i3qlKbvBO5IAWRcL9TTy", - "23qtToEV4h3HKWWkUb53vxeZfM3FVcxx1Nv6wiKZEaXHXgTxhflQ3UxLACTf/6C7oOez6JpGajaK+DXT", - "S/bIHvsF5Y1zAXSjIcHxn7//cXFSKCBbz8aplUZb2w8+UxrV5I8e2mtc5IBkqR+M89QPxMXJn7//4SD5", - "tkAQpukzqggdY69XQfnXjKgZEaVTyW2w/pPRDqE7cvRSmr7iACj7zxcEJ58TEeNbjyDcGngk4b8EVcBf", - "th/SJxrSnVeIQT2aO7wWBeHALwk9i/Ks6bHmbyuX26wkX8jW9on9cbutbJZXNB1NtbIxwtPcgbHsZuPs", - "iqYIevSgh9nGODbMG2V6ZDTmXPUv2b9mhCHYO9hgckNCkFPaQkMHp8cSXdM4BnMHBMGi7L9kr0uiwDSX", - "Sv9XZKyLxplCgiRcEW1rJnpsPUkGa4HGY4Iyht3VSf+SlbFiAazTlUXLFRGMxKMZwRERsiVmTCdkOzUi", - "B0CdYKmIMBI6S6v4OvrnyRnqHN0ynNAQ/dOMesKjLCboLEs1D29Usde9ZKkgc8JA0dUKA7Xz8gnimerx", - "SU8JQtwSExgsNxitX3/+7PTc3gzJjf4le0U0YgmLtL3JBXKnhERqhhWKOPur5lgSVYctz19Dup+Xu8E8", - "TLMqlrfrGH4B9zEanjkVKsOxFlkVjct7PWMu/jwaqrlXLGvKVhTlBIdV1Zvf1lIwI8Mt4KLe7DcOjMLR", - "bBysuAT1+dhzh0OYScWTkqcddWq+BFr1OlSFx5zHvQgrDKpBS/3FLHfx/ii5NUOZTWmSkqPp2OOg0sKQ", - "MjSlUzy+VVVde2uwuPV+RLvxfahuuls15EGikeKeK0NHLcdHGo+ubRsXNtzEjhQfzSfUM3J+aBbOEypR", - "WLvItUSrh+ilIbXs20XXM6qPWYkcEoCDL07KNmD/kvVA5AzRUT5BPmw+pJas4CiDITpclBZBweeJxrcb", - "CKOLkz56na/2rxIxrOicuMvmGZZoTAhDGahnJIL5QZyWF5BJLcOoqne3ssrcS2+Aqcvttz7StkSCrdzX", - "5J1gRUPws41pDR643zAbpWfSAoCVT51Wp8Sym8BXZEqlErV7QNR59fRwZ2fnUV1f2H7QG2z1th683hoM", - "B/r//25/Zfjlr959Yx1U5YX1XJYlyuH58dG2VU6q86j3u/jR/s0NVo/26LV89D4Zi+lvO/hOLuf94umo", - "cLmiTiaJ6DnRp6nK52gt+TMbHKmf7B9dKy7A3cgsO34MdK91y68RSeC7RbN3OOvf9deF4Mp7uBJwC/Do", - "v2r9oKD8km/AurtD6nXsH1F59VgQfKWtSs/5qo9nOTLnjt/XlWk7anyLyI1Wz0iEBOdqIo2/oKqmbO0+", - "3N3f2dvdHww81/aLRMxDOgr1qdJqAS8Pj1GMb4lA0Ad1wNCL0Djm4yrxPtjZ2384eLS13XYdxkxqh4dc", - "i3K9UMdi5G8uGMt9qSxqe/vh3s7OzmBvb3u31aqsgtdqUU4ZrKgOD3ce7m7tb++2woLP7Hziwijq18KR", - "h0gP0jSmxsjuyZSEdEJDBIEYSHdAnQSOJZJbfFWeHONoJKwa6D0PFKaxBw0lr5+ZzLZEHX2mJ1msaBoT", - "8w02pJWmC5AfwUg+DzFljIhRHmWyxkg2+GSlZ8zBkjcBFSUi42w6NTd8BepOqATNolCIKImjoeHQlXIO", - "drNY2JsmOrAwtKSG5/yaiF5M5iQuE4E5jvRiEy4IyunEbFoFKsrmOKbRiLI085JEIyqfZgL0SzMowmOe", - "KWOqw4aVJ4ErM7ARJlpct7uxLXzUC1NrO3NNx18q+ITGHjDAaLVf7ZHuXGLPdwdnva3/A36wlyy+NXKA", - "MmPoJjwi/VrEILRvDd5p05rycE1UXt0CTLlrwuMeza1dhxFrdIeYoTFB9pg0Tl1wmxSTFAL+kU9gTgRO", - "yDibTIgYJR5L66n+jkwD44OiDJ08rgpNLZzbqlunlc0BfWuCQ8qmG62x77HkamB0S9h849+uV8SENTRF", - "EeitEraNDSTooxd5gCx6dnouUeFO8ph4LS/sTme3UhsnZkQTFERZ2TID4mwthk+LjtaG9QjjxCuAHCOg", - "znyaZsCGZ696xy8vNpOIzLuVNYELaMZjote9UdKt5i6WoLhdrFy5zJtUZEMYsi0DlXCVc3BrJJX41YMd", - "xRWORzLmyrOa1/ojgo+oc/HU3CHrFXRRWtlK/fcSFir0veflGC2RmqY9gwnrtnaFwVe6PRJzbJXBq0zq", - "Y5VfCI5NOH2VnouwNLfx/Kq60fxqJffaQXzzHrtbt9rJmXhsl8OTI2OZhZwpTBkRKCEK2+D90s02BFgE", - "3aCnlYEIkwR8opP/Wn7X3eC7ycllmfV/uBAB/FUs/4YoNy3k4jmJUIIZnRCpbJRbZWY5w9sP9oYmvjYi", - "k90He/1+33/Do8RtyqkvvPFJ/q3dVmya+9FeMWZfzj5vH77CHX4bWD4EpwevfwmGwWYmxWbMQxxvyjFl", - "w9Lv+a/FB/jB/DqmzHv33yokm04WQrEr25vqM8v8faghYSTMCZKDlrjSN+k/yV9o0ozpexIhb0SUwlOk", - "9W+guM8LffqMIObiTYsqBS+XrwlaBDLT98vNbacYQRs7Z8YUjYsY70VD+5Oi9OXSoMeFgMeUsDzMMY7N", - "TyFnc80VvpjHigB33xY245qLK8qmo4h6qPNf5iOKqCChgpCS1TwUbOI0XU2KfuUvl2lt47dt9JbndPnm", - "kvxTHK7V2V9O//Hu/8rTh79tvXt+cfHf82f/OHpB//siPn35WSEnywP3vmn03dI7NfAyVqLu2pLHCVah", - "R/GZcakasGa/IMVRojv30SEYaMNL1kPPqSICx0N0GeCU9i0y+yFPLgPUITc4VKYX4gzpoezV8YbufGrC", - "bnTnD84G/FgfI7J3xMIiOQ/nkNk44gmmbOOSXTI7FnKASLi00T9FKMSpygTRO6J1zfgWjQUOi7vhYvIu", - "+oDT9OPGJQNLlNwooSFIsVB5lK+bATbarspcCtnmJEJzHGdEWkv2kuXnB5jmehCFxZSofu5CBEdN7WKm", - "ASleM4OLamzD/qDr2Uek2+mNjKlUhKHcK0ElEC/quCCV/UGF/fcH+6vvH3MaWkJ+QN2LL1wdUbbgD0PA", - "MLURxqOZUunq8AWQN4ZH0C+vX59qNOh/z5AbqMBFvsXGGMNpGlMiza2aikEnsXFBG4Hv5szsbkuAXpvG", - "ulvcIgzjCUyMXj8/Q4qIhDIjvzuhRueEhho+uN+hUmaaFClGB4cnTzb6LZ7oAm7z9S/Zx9c5hLVrBOfc", - "WrQwoUfhNNf47aLjo65WpyyHFooW3Js+5QLFRsAUfD1E55JUoxhgq8wVj9nJ+LbwkBmpfhlsuBHTuqQY", - "ole5fofzpeSvDwpicEMWfAnD2sAWc6m7MHq3ula4rrb2ixVtcIWLFbJObziKm0XBcvb3YBx4nrO673E9", - "3i47LfVkftIo9v6rayA769qS60ZyV4PSSkGIeTD3t43C/pSYardDz07PIXIZy5FkOJUzrpqDMzBybRC5", - "oVLJxTi2VuEEizHc1ePJRGcvCQz8ktHYImMMIiPqYHzxOOtvGWvw/cV4L43K/tzQaqugfaXI6kaB4ItK", - "rsoG8+cvGyP9VZZTiXb2CYPyOeYCwT45wLkbUE8QzIGUdMpIhI5Pi1d/hcPDDV+D6dF2f2tvv781GPS3", - "Bm3cPwkOl8x9cnDYfvLBtjGIh3g8DKMhmXyG+8kStlE4cHyNbyW6dCrhZWB00JLyWWJbqza2utpbjCP/", - "tLDx+iG4KjB8nUDwdhHeS57jn1Uf4rfWKx78+7Pe7JO2x/AZNHa9Rus4RgkKeRZH7K8KjTXnGVOARNZi", - "kUQVOQ6AWc/ZFePXrAq68Y9p/n2XEXGLLk5OKt5UQSb2uXcLwHmaNu4DT9fahu0V6t3K1ZSCre8iwLou", - "CUsn0BcPpy67flxch6G6Fi6gQv3zXpNSZtCt934JTDXjPSLzUZb5FB39yUVonp8fH1U2HOO9rf3B/qPe", - "/nhrr7cbDbZ6eGtnr7f9AA8mO+HDnYbEKO3DJD498qHKoc0R0YB4cISZIPZoqHkoD10YZwrlj9Q0cx5q", - "jRGV9FAT/wu26SujkuoR4HQN9Zf4NldVl3Y+xZpRXd8Uflve42yWKa0GQR85yxTSv8GSNQhW1V8+hOH5", - "IXrBoY9daVcflDWbwTTHLBrfLjav2xcdGwEiiFRckAgmswJsiJ7mQisXe1bMdSSxPxpZaiOlIApswxjU", - "Vr23uxV0A4v1oBsYFAbdwGFG/2gghJ9g8UE3sAvxBlmW6cbnKCY4BhlWBGFkisb0vWE5vXQqFQ2NiYVh", - "N5vYzr5kI9HIHKFNVznmZt8es3knx9UXJ6gD7w7+hqwFpn/byK99yiy0u/1o99Hew+1He62iFosFrpbG", - "hxB3sri4laI5TLORSxDVAPrh6TkcPvpgk1liwiQt7IXtpgVHqLU9ylCRcaqY/FH/UTlYM+LZOC55Gmy0", - "NkQEmg3zxiPngqPhnuMdjed0MmHv3odX278Jmmzd7Mntsdc4yifya5LHZe/YgtlFxj3z1NgfTwcEJWRj", - "yOkrIgECdEYUAvrpaYGlT9Q8XMSSnAtMtRj3Etbuzs7O/sMH263oyq6uxDgjsP8WV3liV1BiMWiJOq/O", - "ztBmieDMmC6GLhVEauDMKwovn6HLbDDYIWhQCa/TtseOj0oaFJaCauzY86QR5RdWY7FAWaRD1EuuzSxw", - "uRfbOzuDh7sP9h+0Y2Nr8YzEzXIJY9vZ22JBQkLnlZ3vgEf19cEp0qOLCQ6rGv7W9s7ug72H+2utSq21", - "KiUwkwlVaq2F7T/ce7C7s73VLnba5zW1rwIqDFuVXR6m8xCFZzc8qFgUvd2m08KnJS6G2i2N7ivCBeux", - "YesEgxYvwaiEUWkpDhF1tBJVVkhLr5k22vgZ/CJSz9OUAFKri23jNJeHZZ5iNTtmE77oFl/H4LPBLu4S", - "ItWKj4SEXBFhlEROduWWn9WlIHwmlgRFGbGYM7qRwBbh2FwNpFjNQFmFjpRNq4HDCxO2McPMGpa/+4N5", - "bcM2HiPpD9B4LTLAlXHoSoSLUI1W3mkqR36rYnFgQaZZjAWqxyIvWbK8TWLKrtqMLm+TMY9piHSHujk/", - "4XHMr0f6k/w7wLLRCjrdYVTcStbMc7M4eydtNqQ2bwHC3zWUG7UoFzj5N03/Tcjw28YB543WfaqNNxOu", - "e87oTYnQq49odrcHTUFNDYNWwpkWQ73Xle2WZH0c76KwD/JkE54rMXNbU7Ngq3pwBV4ftHCrtSyEa1ET", - "QB3n03OPlKp4LT0WanUQf5a6uyT76Wcot/NkiVLbgK2TNprT/oP9R492dh882l5LR3GXDQ2Xj00XDm4F", - "m5KEtbwuNb3pwQD+t9aizHWDf0kNVw7VBVVytHzygj4uYZ/iEUOD5bssB3ixk87UrirB7dTMJRrLQUXt", - "KaXq6pDJhIDzZmTw1isWUwuqabWGEKc4pOrWY4Xha4gzQHmTWjB+i9Fri/Wg1I6N8ERpa31OhMzGxWu0", - "jpsc/aexrmq0sN/64aXMxk2W3Mv6rMaOM4E5Uc1L0MJINxThu4W+zpGJrrGseNb1z6EiUbeUiq1+BWNa", - "tM8062g9TzZbXC77HpT4E8uWt7+2nSXNv6Ko1jG+7BhrZkF9KkPUTxsnt+dU9LxSCVdHMdTkgz0HP63X", - "aFx+Er30zXnl/XTrHHaL05qDaP3lli7D1+lYf+QJZGXXYDFXjN2t7KyPKMxVRVMGkcTVuKi9AaUmV7l9", - "6INKjVGHJKm6dcHszsjbWO/q5CAf0EtTXzj8aPDoSwRAny+NeP4fkpOmfFvlJll5T7Wwp41hhn6t86ge", - "CWLMK/smvxq5UHtpLNWSlPzLCrGYiihgO9kQ32lWf5O0RvGVJmu54ByXa99VX1llBC51TZUgK62keW/M", - "VeVnVqqh0pWo+USUWUtmdcysue7RtmSvnrTBPPkUFEwjiyCDWI2C3NpdNKmXR1Cc4Jt8BjA8sUS1NHcG", - "jlLK2GeP4Rn3K/d4n07cELCMesLCx59XwsdR1eJmLKvp4y7DvYxn5c8SidbEWzXiLOboLi8bpEUXCTNB", - "1e2ZPhBsnBfBgoiDzJAhnBQABPy5mBzixj9+BGtz4lE6nxFGBA3RwekxUEmCGZ7qLbs4QTGdkPA2jIkN", - "+124JoVX6y8Pj3vmvUKeVw7qAChAiEvodHB6DLlkbAb+YNDf7kPaXJ4ShlMaDIOd/hZky9FoABA34TkY", - "/Gh9OpoP4SQ7juyJ+9g00aiVKWfSIGd7MKhVdMBFvo7N36RxVpjjtbVuZ4rXLIYuLESzOk3ALv9jN9gd", - "bK21npUpNnzTnjOcqRkX9D2BZT5YEwmfNOkxM8axS+pLbMOCZoPhr1Vq/fXNxzfdQGZJgrWKaNBV4Crl", - "skmFIRJhxMi1fSf4Gx/30ZkxLSDfRlEWzFj+JNIiCSOFRX/6HmERzuicXDIriU26FCzgUUSCtAQ2IelV", - "MjNTm903LEykesyj2xp28+E29XCgjVQRvHapizz3X9pQ88InHU2KIRlyb24lwjBTRcYak1voisB94ITe", - "eMPKIVTW7zg+yr+54ihV2a7VXcrCOIuKA7BalML7XFmSUBCfkv2Ps5cvEDAelLeAZkWEL2SBpEyLzTxR", - "p962/iV7gsMZMhIVEthdBjS6DIoyBhsg/TJJjFDr9UAk/x3qu5hpujT6e7+vhzLSfoh+/WBGGaLLgKXJ", - "SPErwi6Dj11U+jClapaN829vLpkX4AZb/ayCK9QxlLzh3mRqCEtMbbgAswhxSznxLcKo2KSyLj+mDIvb", - "ppogPFPNMQfmyaptVryn2hsMNlb7lC2onnOu0lBrAx8XxPr2F5NoVpovSrRS/S8tP5h9jxwZOX4HIvUx", - "jtwzmZ9nx4qzwyq9pVMB+lvNYfMDjT4a8o2JiXGriXYoE+NEe4oFToiCRMG/+mkewvuo/t3dAIGRakzA", - "KvF2S+ipa4JvFgh7t7H+Tl7JBmhh9w7oD+YtskTBvI/ual4cmxyleU3Ae0WOsFmOELt+tfUZUd8DxQ3u", - "SpS6ZHbfkH7vC/08I1YTLpBWk2abkB28bBPVw9AFwYm0o5jGWgk+gzX1zghTCCq/yb791+lnEOH7NubT", - "t0NkUBjbunfSpifLnYf6ULS4hE4me0PezyY1CWeYTYlEHXN+/vn7H65215+//2Frd/35+x/A7pu2EiUM", - "l1edeztE/yQk7eGYzokDBqLWyJyIW7QzsPUQ4JMnRYq8ZJfsFVGZYDKP+dBwAU7MgPB2lQE8lGVEIgko", - "hOTFExuMYHwTHtvA8bJB5Z1ydHfBRLIQlADQp6KjAbjZoowqimPEM2XyDMI64GFEsRADc1CevO5mWXC8", - "rZYvitwoQ709s8A1BYyp2ujhO1PI0IyJOmdnTzb6CNR9QxUQcAJ2QzGMtQT6P2XSaplkJEpVoACWjWwq", - "ZcdrdNIc2TZ34aVpypzX7KYRkOabaIvVAfNT7W7hsvHjzblvfD6UI5fNudmJ8unw+oo6trIpv9w+O9pb", - "xLlNVV6g7FtYk6hjs8zmySQq+dC/FdHfiQAupdHPpTDiJoXFnVk4h5xNYhoq1HNrsYX+cqunSiD3RRy8", - "sqtG2MFVj5IuHxWblYCjxkMjjz26y9OjNuk6x0gRyV3Q2s+TZBXpHFEZct23RC29EKeASIvEgk/LVLTK", - "t3MEf8+PnKWKeV560zHk3Xl57NQZq58NdyAUj2oC8RsKwlqqhdLbh/tEzef5LrrKFUucQN8XaQ7uTgu6", - "a4eQj8zvk0coqqFNS8FZnty5ibxs+uevuNF2Bg/gZ0Q4rjYLNU/8C7BMVxTOSHhlALIFVpZpBMeuBsvX", - "1wNMDus1Tn+7/J/HfQvDscDVMmPx2OZ9+Hq2YqWC/B1fP1oC8yAZrvfHzpFqUipgecvCjR/qBvJOToZ6", - "QZR7xEmnWRw7R/ycCFVk8i7L080PWj9ooSc7bluqi5y/et4jLOQQzGFQ16iQuMS9X1ZbNhtmQPlJJm3s", - "K0CVI4xmZfQz9t9Ed6I8o95ftp/anHp/2X5qsur9ZefA5NXb+GrEMrgr0XzX2us9Jj6tvNIq0kA0mfS6", - "q7S9vNWdKHw2j/k6Kl++wJ9aXxutr4yupYpfnlL+K6p+NlP3t7knyInNh2345OLPfjCV725dT5YiS8XX", - "Kr54m1yCiyI7ti3ddP8C5GhOcWX529KHWjDkUu3Ake7xUdcmPjfpyvPI4jvyqLp13LmWaOe9e3fqQTKm", - "04xnspxfGfLcE1kUBa0I4PumvxbHc6MG+x1T6eAuj447V1B/0v1XUp3rG2qEt60jukJ5dq3uRnkurmra", - "a89uhT+151bacwldy7XnPJfm11SfzSTfTH929OZDuH37+iNq0Pft2QazPu7SZW9FxrVWUIv8scvPfksb", - "3+KiP5/87vVSl0fqfoafchNwHjlNsDhrmlXB740eBncr++5eBbzPJPasXLLLr2yZtxcxn65+eZGP5J4Z", - "eJ5eXDJX3+uteQ75FuWEihRHksQkVOh6RsMZPMPQf4PxzSsNnKZv83eXG0P0DMI7yy9BYfKOJILiGJJC", - "89jkM387T5K3w8W3/BcnJ9DJvMAwr/bfDpF7v5/zmNStys8qNBQxlgq9sI9FOnrDBY9jk4D2rcZnCb4N", - "++CieKJ6yXyPLxi5tgPSCXpbeofxtuEhhiPC53qXvhHnd5vzfxtYFEcCEGdqrxEWNTzC0FjzP8HYGnhT", - "0rR8DmKW8ZVfgyws5jmf5u++K6SM07Qt+dplAhXPk2QJDaNOKee5VBHP1N+kiogwJTktdTcRN+rg0Pyi", - "8JUpIFmpoGWy7PtQZZ82e1EVmDK5Ljm/+W2eJIEp55VgX7L9z39WUx9w0R7TO1N6O/PzzFjnVUxV2Jee", - "xdRODlvlAbJAeI23V6bBD6+5uHIY35gM7/4qorQKClUyWDS+hb0t6ozcrzcBsJEFZHDeWbi8POK+NfKI", - "LU/yw/NIQR8/OJeEXEBNY+lqjN2f4K2SxVFi9w4UNSqKBXWd1XtxcrLRxDSmLG4jy4if5rCNo/zhzxSo", - "83T/uMWUOMQ5AMuchZohVKON7mzWSg24Mc/06AtpLaH2gbyViiTGYJ9kMTxsg6h1mx8Al2s7dBFVErIc", - "d8FlVcrrf8nGZKLPw5QIPbfuDmmzCtvDZ9aeKZyz76nhwe/DroVMl2DKYdWEtVoBhTR1SS59tlOel/OT", - "l/QUDNVqbQmJOjG9MgXT0FyiWP+wsdTSNYUnvnT2g0/nrLy0iu9Vq6HZnJh/BAl3XBNrrnTgvRNrz0iZ", - "WZz8gY32izW5Uq6JNWvvOdyVavD1L9kJUUK3wYKgkMcxpHs3+vtmKni4CXXBwpRGpkAYLA4EXvPnBGY8", - "PD2HdiY1d/eS6V8WK1PVF+oKXB1vvlzh+zM1Cf8H6zkGwGVs4d/wn26d9a8CGnlINrAoT5dp4jz9qYjb", - "UqM/zdZ7abbCXWwOTWcqcAhKsbTFZP0mqq3AtPnB/HC86kZf4XB24bL4fx/ark36vWoaB+C9YEoLU0TM", - "q/u750me52W/py+rNOIcCKDElGMT/KeAqffwo1H3lw9DK+NxrSC0O+Utl9Hiu+Gtuz757Brci4oyPu4L", - "mxtKc5BAfvGy90mU60cttc1cXSAoZparlq6sVbdcXc0k0Mx9SEU9j7yQU/+S5ZWrXAJPbV11nWmFIiqv", - "zAjWeuojf4ExY+fZKmOXTHEU4jjMYqwIyittmep4ssH6elWqPvfV+K2YxLPReYkxmZeSuk8mh58mYPfK", - "taqA4qw6tTT8+8K2uYvgb3uYrRH67SD4GSXbIvC7hKw2lTFM8z46y9KUCyWRuuZQalZCuA3kQR3z6HaI", - "8n4MmepkVsTZslK2RASJoLSP7ntSKZdRGsD1TAXppTwF0RGZV3UWx0Y9WizE0VBrI9ePvl4Ee1116K5b", - "vqO0lup+VGFEeW0MW65B49biyw3RqiiDrxZRXh8kzKTiiRv3+Ah1cKZ4b0qYRm5RiiMVfE6jekXH76QM", - "2wm+oUmW5DWMnz2GirDCRGNBbXCIBXQ0RW5CQiIJwVkba5ZsW6zWZvfi08pSfDkh5qRpo075DZ81FPlD", - "9RZrHdMRueIcxVhMycYP83jY8lrxdvj4qPZy+B4+yJg76iv0jJZPMNqZtC0tza/x/CJ3d9zt44uL78cK", - "K6VYvIcvgOe5mtn06uP7IsHB3R0Jd/3a4+Iee+20tTWvoc0MoEf0EcxzHuIYRWROYp5CUVLTNugGmYht", - "icXh5qY202JtyA33B/uD4OObj/8/AAD//6UQLF8U2wAA", + "H4sIAAAAAAAC/+x9a3MbOZLgX0HU7cRQOyRFPSzL3Ji4kCXbrRnL1lmW5nZaPhqsAkm0qoBqAEWJ7fDX", + "+QHzE/uXXCAB1IsosuiHbG17Y2OaVuGZyDcSmR+CkCcpZ4QpGQw/BDKckQTDzyOlcDi74nGWkDfk14xI", + "pf+cCp4SoSiBRgnPmBqlWM30vyIiQ0FTRTkLhsE5VjN0OyOCoDmMguSMZ3GExgRBPxIF3YDc4SSNSTAM", + "thOmtiOscNAN1CLVf5JKUDYNPnYDQXDEWbww00xwFqtgOMGxJN3atGd6aIQl0l160Ccfb8x5TDALPsKI", + "v2ZUkCgY/lzexru8MR//QkKlJz+aYxrjcUxOyJyGZBkMYSYEYWoUCTonYhkUx+Z7vEBjnrEImXaow7I4", + "RnSCGGdkqwIMNqcR1ZDQTfTUwVCJjHggE8GaRjTynMDxKTKf0ekJ6szIXXWS3cfjw6B5SIYTsjzoT1mC", + "WU8DVy/LjQ9ty2O/3PeNTHmSZKOp4Fm6PPLp67OzSwQfEcuSMRHlEQ938/EoU2RKhB4wDekIR5EgUvr3", + "7z6W1zYYDAZDvDscDPoD3yrnhEVcNILUfPaDdGcQkRVDtgKpHX8JpK+uTk9Oj9AxFykXGPouzVRD7DJ4", + "yvsqo031VHz4/zSjceTBeq4Xpkg0wmp5U9AJ2TaUM6RoQqTCSRp0gwkXie4URFiRnv7SBtVDQfCa6XSL", + "VpMtI31mYDpKZNPorgmiDCU0jqkkIWeRLM9BmTrYb95MCXWJENzDK57pP6OESImnBHU0A9NclCGpsMok", + "ohJNMI1JtNUGZD4cNpv5hY8RjQhTdEKrlBaMdYMeHoc7u3teKk7wlIwiOrUyoTr8Cfwd8QnS4ygErf0b", + "0Si/aLcPmFKQyfJ8z4GJwiSCTIggLPzs6VLB54RhZpj9f8C8wf/aLoTltpWU2wDM86L5x27wa0YyMkq5", + "pGaFSzzEftFoBKBG0MO/Zvi06qxLGCUVFqvpA1p8AUo062sFmwvTtM6ZgPHYYSqU3ciAns0JUz4uxJT9", + "UN3xSz5FMWUE2RYWvhMukJ7grzGfbgVfZm/doADpMkHrdX8CQzJ/aBhNf+sGhGWJBmbMp2VozggWakwq", + "wGwQEHagYnWN4D+vkET1DMZYktFqrnBOGSMR0i0tsZqWKJOgBy5tHyjjhqrRnAjppSNY1t+pQrZF41Ax", + "D28mNCajGZYzs2IcRUCDOD6v7MSjC1WUS5xqxuYGBBktkeLo4qej3UcHyE7ggaHkmQjNCpZ3Uuqthzdt", + "kcJijOPYixvN6La53F3GED8GXOSE0SRPcgx0iGm4V2BPUw/fDdJMzswv4Md6VSDPNBvQ6BXr3+88mz4G", + "JmF08EaLxK9hvU7NYaNpzDVMFyhj9Nesor720anWxBXSzJ9GJOoiDB80G8aZ4r0pYURoPoUmgidIzQgq", + "qZioQ/rTfhdda62rp3XMHt7tDQa9wXVQVRLj/d40zTQosFJE6AX+v59x77ej3j8HvSfvip+jfu/dX/7D", + "hwBt9V6NTnqddp8dR/td5BZbVobrC12tKK/QNX1cxBzfqab9TU/v+HRZwJv1Rzy8IaJP+XZMxwKLxTab", + "UnY3jLEiUlV3s7rt2v3B2lZsjE311jfcWk31B3TrxPyWiFBzyphoBJFdzSypkl2EtfUITAZpafZfKMRM", + "46wR7FwgwiJ0S9UMYWhXhUCy6OGU9qhZatANEnz3krCpNt8P9pbwUSNjx/7ovftP96et/+1FSZHFxIOM", + "b3imKJsi+Gyk74xKVKyBKpKsFbcOulkMKlZC2anptpOvBAuBF/5Tc4tbdXpSaebTeHyGgDz7O3EGtkTW", + "aAOBgMF9Avt9cX65rUkyxVKqmeDZdFY+lZ8dP3hXgkWDNuA22Q0iKm9GlI/GqW9NVN6g0+3XSHMrFNOE", + "qoI77QwGZ0+35XWg//HI/WOrj06MXwWWrzfPhWWacoYFAdEdIc7Q8fklwnHMQ2sMTbSGNaHTTJCoX7OG", + "YXQfthA2/ww5/IzNqeAs0brQHAuqiadi438IXr0+eTZ69uoqGOqTjLLQGsznr9+8DYbB3mAwCHyiTp/E", + "GmR8cX55DDvW7WdcpXE2HUn6G6l4p4K9F0+D+sKP8v2ihCRcGH3UjoE6syo7MOIaxfSGoGs9njm0nRd1", + "Rr0LUy0BbbZIiZhT6bMzf8q/6fPOJCnTpiGGKkpIIuZE5GcNh98vyfow5lnUK03ZDX4lCaB1sVBPI7+t", + "10oKrGHvOE4pI438vfu98ORbLm5ijqPezhdmyYwoPfbyFl+ZD9XDtAhA8vMPukt6PotuaaRmo4jfMr1k", + "D++xX1DeOGdAd3onOP79X/++OisUkJ0X49Ryo53dR5/JjWr8Rw/tNS7yjWSpfxuXqX8TV2e//+vfbiff", + "dhOEafyMKkzH2OvVrfxjRtSMiJJUcges/2S0Q+iOHL6Upq84AMr+8yXGyedExHjhYYQ7Aw8n/IegCujL", + "9kNaoiHdeQ0b1KM54bXMCAd+TuhZlGdNTzV9W77cZiX5QnZ2z+zP3ba8Wd7QdDTVysYIT3MHxqqbjYsb", + "miLo0YMe5hjj2BBvlOmR0Zhz1b9m/5gRhuDs4IDJHQmBT2kLDR2dn0p0S+MYzB1gBMu8/5q9LbEC01wq", + "/b8iY100zhQSJOGKaFsz0WPrSTJYCzQeE5Qx7K5O+tesDBW7wTpeWbDcEMFIPJoRHBEhW0LGdEK2UyNw", + "YKsTLBURhkNnaRVeJ38/u0CdkwXDCQ3R382oZzzKYoIuslTT8FYVet1rlgoyJwwUXa0wUDsvnyCeqR6f", + "9JQgxC0xgcFyg9H69ecvzi/tzZDc6l+zN0QDlrBI25tcICclJFIzrFDE2Z81xZKoOmx5/hrQ/bTcDeZh", + "mlWhvFuH8Cu4j9H7mVOhMhxrllXRuLzXM+biz6OhmnvFsqZsWVGOcFhVvfltLQUzMtwCLuvNfuPAKBzN", + "xsGaS1Cfjz13OISZVDwpedpRp+ZLoFWvQ5V5zHnci7DCoBq01F/Mcpfvj5KFGcocShOXHE3HHgeVZoaU", + "oSmd4vFCVXXtncHy0fsB7cb3gbrpbtWgB4lGinuuDB22nJ5oOLq2bVzYcBM7Unw0n1DPyLnQLJwnVKKw", + "dpFrkVYP0UtDasm3i25nVItZiRwQgIKvzso2YP+a9YDlDNFJPkE+bD6k5qzgKIMhOlyUFkHB54nGiy2E", + "0dVZH73NV/tniRhWdE7cZfMMSzQmhKEM1DMSwfzATssLyKTmYVTVu1teZe6lt8DU5fZbH2lbIsGW72v0", + "TrCiIfjZxrS2H7jfMAelZ9IMgJWlTispseom8A2ZUqlE7R4Qdd48P97b23tS1xd2H/UGO72dR293BsOB", + "/v9/tr8y/PJX776xjqr8wnouyxzl+PL0ZNcqJ9V51G/7+Mnh3R1WTw7orXzyWzIW01/28L1czvvZ00nh", + "ckWdTBLRc6xPY5XP0VryZzY4Uj/ZP7pRXIC7kVklfszu3uqWXyOSwHeLZu9wNr/rrzPBtfdwpc0t7Uf/", + "VesHBeaXfAPW3R1Sr2P/hMqbp4LgG21VeuSrFs9yZOSO39eVaTtqvEDkTqtnJEKCczWRxl9QVVN29h/v", + "H+4d7B8OBp5r+2Uk5iEdhVqqtFrA6+NTFOMFEQj6oA4YehEax3xcRd5HeweHjwdPdnbbrsOYSe3gkGtR", + "rhfqWIj8xQVjuS+VRe3uPj7Y29sbHBzs7rdalVXwWi3KKYMV1eHx3uP9ncPd/VZQ8Jmdz1wYRf1aOPIg", + "6VGaxtQY2T2ZkpBOaIggEAPpDqiTgFgiucVXpckxjkbCqoFeeaAwjT1gKHn9zGS2JepomZ5ksaJpTMw3", + "OJBWmi7s/ARG8nmIKWNEjPIokw1GssEnaz1jbi95E1BRIjLOplNzw1eA7oxK0CwKhYiSOBoaCl3L5+A0", + "i4W9a8IDu4eW2PCS3xLRi8mcxGUkMOJILzbhgqAcT8yhVXZF2RzHNBpRlmZelGgE5fNMgH5pBkV4zDNl", + "THU4sPIkcGUGNsJEs+t2N7aFj3ppam1nbuj4SwWf0NizDTBa7Vcr0p1L7OX+4KK383/AD/aaxQvDBygz", + "hm7CI9KvRQxC+9bbO29aUx6uicqrW9pT7prwuEdza9dBxBrdIWZoTJAVk8apC26TYpKCwT/xMcyJwAkZ", + "Z5MJEaPEY2k919+RaWB8UJShs6dVpqmZc1t167xyOKBvTXBI2XSrNfQ9llxtG90SNN/5j+sNMWENTVEE", + "+qiEbWMDCfroVR4gi16cX0pUuJM8Jl7LC7vz2UJq48SMaIKCKCtbZoCcrdnwedHR2rAeZpx4GZAjBNSZ", + "T9MMyPDiTe/09dV2EpF5t7ImcAHNeEz0urdKutXcxRIUt4uVK5d5k4psEEO2JaASrHIKbg2kEr16oKO4", + "wvFIxlx5VvNWf0TwEXWunps7ZL2CLkorR6n/XoJCBb8PvBSjOVLTtBcwYd3WrhD4WrdHYsRWeXuVSX2k", + "8hPBsQmnr+JzEZbmDp7fVA+a36ylXjuIb95Td+tWk5yJx3Y5PjsxllnImcKUEYESorAN3i/dbEOARdAN", + "eloZiDBJwCc6+a/Vd90NvpscXVZZ/8dLEcBfxfJviHLTTC6ekwglmNEJkcpGuVVmljO8++hgaOJrIzLZ", + "f3TQ7/f9NzxKLFJOfeGNz/Jv7Y5i29yP9oox+3L2eefwFe7w2+zlQ3B+9PanYBhsZ1JsxzzE8bYcUzYs", + "/Tv/Z/EBfph/jinz3v23Csmmk6VQ7Mrxplpmmb8P9U4YCXOE5KAlrvVN+iX5K42aMf2NRMgbEaXwFGn9", + "GzDu80KfPiOIuXjTokrBy+VrghaBzPS31ea2U4ygjZ0zY4rGRYz3sqH9SVH6cmXQ41LAY0pYHuYYx+ZX", + "yNlcU4Uv5rHCwN23pcO45eKGsukooh7s/If5iCIqSKggpGQ9DQXbOE3Xo6Jf+ct5Wtv4bRu95ZEu35yT", + "f4rDtTr76+nffv2/8vzxLzu/vry6+u/5i7+dvKL/fRWfv/6skJPVgXvfNPpu5Z0aeBkrUXdt0eMMq9Cj", + "+My4VA1Qs1+Q4ijRnfvoGAy04TXroZdUEYHjIboOcEr7Fpj9kCfXAeqQOxwq0wtxhvRQ9up4S3c+N2E3", + "uvMHZwN+rI8R2TtiYYGch3PIbBzxBFO2dc2umR0LuY1IuLTRvyIU4lRlgugT0bpmvEBjgcPibriYvIs+", + "4DT9uHXNwBIld0roHaRYqDzK180AB21XZS6FbHMSoTmOMyKtJXvNcvkBprkeRGExJaqfuxDBUVO7mGkA", + "itfM4KIa23A46HrOEel2+iBjKhVhKPdKUAnIizouSOVwUCH/w8Hh+vvHHIdWoB9g9/ILV4eULejDIDBM", + "bZjxaKZUuj58AfiNoRH009u35xoM+r8XyA1UwCI/YmOM4TSNKZHmVk3FoJPYuKCtwHdzZk635Ybemsa6", + "W9wiDOMZTIzevrxAioiEMsO/O6EG54SGen9wv0OlzDQqUoyOjs+ebfVbPNEF2ObrX3GOb/Md1q4RnHNr", + "2cKEHoXTXMO3i05PulqdshRaKFpwb/qcCxQbBlPQ9RBdSlKNYoCjMlc85iTjReEhM1z9OthyI6Z1TjFE", + "b3L9DudLyV8fFMjghizoEoa1gS3mUndp9G51rXBdbe0Xy9rgChcrZJ3eIIqbWcFq8vdAHGies7rvcTPa", + "Ljst9WR+1CjO/qtrIHub2pKbRnJXg9JKQYh5MPe3jcL+lJhqd0Ivzi8hchnLkWQ4lTOumoMzMHJtELmj", + "UsnlOLZW4QTLMdxV8WSis1cEBn7JaGyRMQaREfVtfPE4628Za/D9xXivjMr+3NBqq6B9pcjqRobgi0qu", + "8gbz5y8bI/1VllOJdvYxg7Icc4Fgnxzg3A2oJwjmSEo6ZSRCp+fFq7/C4eGGr+3pyW5/5+CwvzMY9HcG", + "bdw/CQ5XzH12dNx+8sGuMYiHeDwMoyGZfIb7ySK2UThwfIsXEl07lfA6MDpoSfkska1VG1td7S3HkX9a", + "2HhdCK4LDN8kELxdhPeK5/gX1Yf4rfWKR//8rDf7pK0YvoDGrtdoE8coQSHP4oj9WaGxpjxjCpDIWiyS", + "qCLHARDrJbth/JZVt278Y5p+f82IWKCrs7OKN1WQiX3u3WLjPE0bz4GnGx3D7hr1bu1qSsHW9xFgXeeE", + "JQn0xcOpy64fF9dhsK6FC6hQ/7zXpJQZcOuzX7GnmvEekfkoy3yKjv7kIjQvL09PKgeO8cHO4eDwSe9w", + "vHPQ248GOz28s3fQ232EB5O98PFeQ2KU9mESnx75UKXQ5ohoADw4wkwQezTUNJSHLowzhfJHapo4j7XG", + "iEp6qIn/Bdv0jVFJ9QggXUP9JV7kqurKzudYE6rrm8K/Vve4mGVKq0HQR84yhfS/YMl6C1bVXz2Eofkh", + "esWhj11pVwvKms1gmmMWjRfLzev2RcdGgAgiFRckgsksAxui5znTytmeZXMdSexPw0ttpBREgW0Zg9qq", + "9/a0gm5goR50AwPCoBs4yOifZofwCxYfdAO7EG+QZRlvfI5igmPgYUUQRqZoTH8zJKeXTqWioTGxMJxm", + "E9nZl2wkGhkR2nSVY272rZjNOzmqvjpDHXh38BdkLTD9r6382qdMQvu7T/afHDzefXLQKmqxWOB6bnwM", + "cSfLi1vLmsM0G7kEUQ1bPz6/BOGjBZvMEhMmafde2G6acYRa26MMFRmnismf9J+UgzUjno3jkqfBRmtD", + "RKA5MG88cs44Gu45fqXxnE4m7NffwpvdXwRNdu4O5O7YaxzlE/k1ydOyd2zJ7CLjnnlq7I+nA4QSsjHk", + "9A2RsAN0QRQC/OlphqUlah4uYlHOBaZaiHsRa39vb+/w8aPdVnhlV1cinBHYf8urPLMrKJEYtESdNxcX", + "aLuEcGZMF0OXCiL15swrCi+doetsMNgjaFAJr9O2x54PSxoUlgJr7NjzpBHkV1ZjsZuyQIeol1ybWaJy", + "L7T39gaP9x8dPmpHxtbiGYm71RzGtrO3xYKEhM4rJ98Bj+rbo3OkRxcTHFY1/J3dvf1HB48PN1qV2mhV", + "SmAmE6rURgs7fHzwaH9vd6dd7LTPa2pfBVQItsq7PETnQQrPaXhAscx6u03SwqclLofarYzuK8IF67Fh", + "mwSDFi/BqIRRaSkOEXW0ElVWSEuvmbba+Bn8LFLP05QAUquLbeM0V4dlnmM1O2UTvuwW38Tgs8Eu7hIi", + "1YqPhIRcEWGURI535Zaf1aUgfCaWBEUZsZAzupHAFuDYXA2kWM1AWYWOlE2rgcNLE7Yxw8waVr/7g3lt", + "wzYeI+kP0HgrMoCVcehKhItQjVbeaSpHfqtieWBBplmMBarHIq9YslwkMWU3bUaXi2TMYxoi3aFuzk94", + "HPPbkf4k/wp72Wq1O91hVNxK1sxzszh7J20OpDZvsYW/6l1u1aJcQPJvm/7bkOG3jQPOG637XBtvJlz3", + "ktG7EqJXH9Hs7w6agpoaBq2EMy2Hem/K2y3K+ijeRWEf5ckmPFdi5ramZsFW9eDKfn27hVutVSFcy5oA", + "6jifnnukVIVr6bFQK0H8Weruiuynn6HczpMVSm0DtM7aaE6Hjw6fPNnbf/RkdyMdxV02NFw+Nl04uBVs", + "SxLW8rrU9KZHA/i/jRZlrhv8S2q4cqguqJKj5ZMX9HEF+RSPGBos31U5wIuTdKZ2VQlup2au0FiOKmpP", + "KVVXh0wmBJw3IwO3XrGYWlBNqzWEOMUhVQuPFYZvIc4A5U1qwfgtRq8t1gNSOzbCE6Wt9TkRMhsXr9E6", + "bnL0n8a6quHCYeuHlzIbN1lyr+uzGjvOBOZENS9BCyPdYITvFvo2Bya6xbLiWde/Q0WibikVW/0KxrRo", + "n2nW4XqebLa4XPY9KPEnli0ff+04S5p/RVGtQ3yVGGsmQS2VIeqnjZPbIxU9r1TC9VEMNf5g5eCn9RqN", + "y0+iV745r7yfbp3DbnlaI4g2X27pMnyTjvVHnoBWdg0WcsXY3crJ+pDCXFU0ZRBJXI2L2htQanKV24c+", + "qNQYdUiSqoULZndG3tZmVydH+YBenPrC4UeDJ18iAPpyZcTz/5CcNOXbKjfJ2nuqpTNtDDP0a50n9UgQ", + "Y17ZN/nVyIXaS2OpVqTkX1WIxVREAdvJhvhOs/qbpA2KrzRZywXluFz7rvrKOiNwpWuqtLPSSprPxlxV", + "fmalGipdiZpPBJm1ZNbHzJrrHm1L9upJG8yTT0HBNLIAMoDVIMit3WWTenUExRm+y2cAwxNLVEtzZ/ZR", + "Shn74ik8437jHu/TiRsCllFPWPj080r4OKxaPoxVNX3cZbiX8Cz/WcHRmmirhpzFHN3VZYM06yJhJqha", + "XGiBYOO8CBZEHGUGDUFSwCbgz8XkEDf+8SNYmxOP0vmCMCJoiI7OTwFLEszwVB/Z1RmK6YSEizAmNux3", + "6ZoUXq2/Pj7tmfcKeV45qAOgACAuodPR+SnkkrEZ+INBf7cPaXN5ShhOaTAM9vo7kC1HgwG2uA3PweCn", + "9eloOgRJdhpZifvUNNGglSln0gBndzCoVXTARb6O7V+kcVYY8dpatzPFa5ZDF5aiWZ0mYJf/sRvsD3Y2", + "Ws/aFBu+aS8ZztSMC/obgWU+2hAInzTpKTPGsUvqS2zDAmeD4c9VbP353cd33UBmSYK1imjAVcAq5bJJ", + "hSESYcTIrX0n+Asf99GFMS0g30ZRFsxY/iTSLAkjhUV/+hvCIpzROblmlhObdClYwKOIBGkObELSq2hm", + "pjanb0iYSPWUR4sadPPhtvVwoI1UAbxxqYs891/aUPPCxx1NiiEZcm9uJcIwU0XGGpNb6IbAfeCE3nnD", + "yiFU1u84Psm/ueIoVd6u1V3KwjiLCgFYLUrhfa5siivYbEkiY6ByLtuV5gPSlt/UZcA2XV3SJCd3GI+I", + "iRdNF2rGmfk95VoY2acNFiba9NWrNCgGeehoAs8LzGNIPe62mWT7g13ax/41O4oS94DVpkTFseQ2j5S5", + "7KcS5Yl5r5lXhZYjrMcZjV2hrJqmSmCo60DLyutA/54KrHWyTM4QDuFyX/+xAgSDzlyAvNuqrzXEDKU8", + "zWKtPcD5mERTlTHgpRiOY6QAgVxfLUUBLg37kSQUxGcs/e3i9SsEDBTKlECzIlIb9kCZFn95wlU9Yf+a", + "PcPhDBnJCIkIrwMaXQdFOYotQIJMEiOcej0QrX+FOj1mmi6N/trv66GM1B6inz+YUYYaUdJkpPgNYdfB", + "xy4qfZhSNcvG+bd3DRtu8LlcVHAedQxH2nJva/UOS8zZcDPMIsQtB4gXCKOC2Mo22ZgyLBZNtV14pppj", + "R8zTY9useBd3MBhsrb8bsFv16CuVhhpTPy6J590vJpmsVF6WTKU6bloOMPuuPDLy+B5E41McuedOP3SA", + "NTqANV5K0h36Ww1w+wONPhr0jYmJVayJaCj340R0igVOiIKEzz/7cR7CNKn+t7vJA2eDMeWryNstgaeu", + "0b9bQuz9xjpKeUUiwIX9e8A/mLfI9gXzPrmveXFscs3mtR0fFDrCYTlE7PrNjxdEfQ8YN7gvVuqSEn5D", + "/H0o+POCWBWpAFqNm21DlveybVt/TiAITqQdxTTWxswFrKl3QZhCUMFP9u1/nZ4NkdrvYz59P0QGhLGt", + "XyhtmrncCayFooUldDJZOPJ+NjlNOMNsSiTqGPn5+7/+7Wqw/f6vf9sabL//699A7tu2oigMl1cPfD9E", + "fyck7eGYzonbDEQfkjkRC7Q3sHUt4JMn1Y28ZtfsDVGZYDKP3dH7ApiYAUFtZ7AfyjIikQQQQhLqiQ0q", + "MT4mj43naNmA8l4purtk6todlDagpaLDAbihpIwqimPEM2XyRcI64IFLsRCz56A8ed1dtuRAXc9fFLlT", + "Bnt7ZoEbMhhTfdNDd6YgpRkTdS4unm31Eaj7BisgcAjshmIYawn0f/Ck9TzJcJQqQwEoG95UynLY6Gw7", + "sW3uw9vWlAGx2d0mIF070bar28wPtbuF680PN+eG8/nCTlxW7mZn2Kfv11ecs5VN+eXO2eHeMsxtyvkC", + "ZN/CmkQdmy04TwpSyWv/rZD+XhhwqRxCzoURN6lI7s3COeZsEtNQoZ5biy3YmFs9VQR5KOzgjV01wm5f", + "9Wj3sqjYrgSONQqNPIbsPqVHbdJNxEgRkV/g2g9Jsg51TqgMue5bwpZeiFMApAViQadlLFrn2zmBv+ci", + "Z6VinpdQdQR5f14eO3XG6rLhHpjiSY0hfkNGWEuZUXrD8pCw+TI/RVeBZIUT6PtCzcH9aUH37RDyoflD", + "8ghFNbBpLjjLk3Q3oZdN4/0VD9rO4Nn4BRGOqs1CTaqGYlumKwpnJLwxG7KFclZpBKeuls7X1wNMLvIN", + "pL9d/g9x38JwLGC1ylg8tfk7vp6tCDNsZCp+uetHi2AeIEOYxtg5Uk1qDCwXLNz6Q91A3otkqBe2eUCU", + "dJ7FsXPEz4lQRUb2Mj/d/qD1gxZ6sqO2lbrI5ZuXPcJCDkE5BnSNColLwPxltWVzYGYrP9CkjX0FoHKI", + "0ayMfsb5m2gplGdG/NPuc5sb8U+7z012xD/tHZn8iFtfDVkG98Wa71t7fcDIp5VXWgUasCaTJnmdtpe3", + "uheFz+aj30Tlyxf4Q+tro/WVwbVS8ctLA3xF1c9mXP829wQ5svmgDZ9c/NkfTOW7X9eTxchSEb2KL94m", + "CeGiyHJuS3A9vAA5mmNcmf+29KEWBLlSO3Coe3rStQnsTdr5PEL8njyqbh33riXaee/fnXqUjOk045ks", + "58mGegVEFsVdKwz4oemvhXhu1GC/Yywd3KfouHcF9QfefyXVuX6ghnnberBrlGfX6n6U5+Kqpr327Fb4", + "Q3tupT2XwLVae85zon5N9dlM8s30Z4dvPoDbN8x/RA36oT3bYNbHXbrsrfC41gpqkQd4tey3uPEtLvrz", + "ye9fL3X5wB5m+Ck3AeeR0wQLWdOsCn5v+DC4X953/yrgQ0axF+XSa35ly7y9iPl0/cuLfCT3zMDz9OKa", + "uTpt781zyPcoR1SkOJIkJqFCtzMazuAZhv4bjG9eaeA0fZ+/u9waohcQ3ll+CQqTdyQRFMeQ3JvHJi/9", + "+3mSvB8u52S4OjuDTuYFhsm+8H6IXB6GnMakblV+VqF3EWOp0Cv7WKSjD1zwODaJhN9reJb2t2UfXBRP", + "VK+Z7/EFI7d2QDpB70vvMN43PMRwSPhSn9I3ovxucx53sxfFkQDAmWfhhEUNjzA01PxPMHYG3tRCLZ+D", + "mGV85dcgS4t5yaf5+/0KKuM0bYu+dpmAxfMkWYHDqFPKXS9VxDP1F6kiIkxpVYvdTciNOjg0/1D4xhQC", + "rVRCM9USfKCyT5u9oApMuWNXZMH8a54kgSnLlmBf0YTPf1ZTH3DZHtMnU3o780NmbPIqpsrsS89iapLD", + "VuuAbB5e4+2NafCH11xcWZNvjIb3fxVRWgWFaicsGi/gbIt6MQ/rTQAcZLEzkHd2X14acd8aacSWmfnD", + "00iBH39wKgm5gNrU0tWKezjBWyWLo0TuHShOVRR96jqr9+rsbKuJaEx540aSET/MYRtH+YeXKVCv6+FR", + "iylVifMNrHIWaoJQjTa6s1krtfzGPNOjL6UnhRoWciEVSYzBPslieNgGUes2PwAu1+joIqokZKvugsuq", + "VJ/hmo3JRMvDlAg9t+4O6c8K28Nn1l4onJPvuaHB78OuhYylYMph1QS1WiGMNHXJSn22U55f9ZOX9BwM", + "1WqNEIk6Mb0xhe/QXKJY/9haaemaAiJfOvvBp1NWXiLH96rV4GyOzH8EDndaY2uuBOSDY2svSJlYHP+B", + "g/azNbmWr4kNayg62JVqKfav2RlRQrfBgqCQxzGk7Tf6+3YqeLgN9d3ClEam0BssDhhe8+cEZjw+v4R2", + "JsV695rpfyxXGKsv1BUqO91+vcb3Z2pL/g/Wc8wGV5GF/8B/uHU2vwpopCHZQKI8XaWJ8/SHIm5Lxv4w", + "Wx+k2Qp3sfluOlOBQ1CKpS0K7DdRbSWt7Q/mx+m6G32Fw9mVq8bwfWi7Nnn7umncBh8EUdo9RcS8ur9/", + "muR5fv0H+rJKA85tAZSYcmyCXwqYuh1/NOz+8mFoZThuFIR2r7TlMlp8N7R135LPrsG9qCjD46GQucE0", + "txPIL172PolyHbCVtpmr7wRF6XLV0pUn65ar5JkEmrkPqajLkhfk6l+zvAKZS+CprauuM61QROWNGcFa", + "T33kLxRn7DxbLe6aKY5CHIcmrXteMc1UOZQN1tebUhXBr0ZvxSSeg85Lxcm8JNhDMjn8OAGnV645Bhhn", + "1amV4d9Xts19BH9bYbZB6LfbwY8o2RaB3yVgtalwYpr30UWWplwoidQth5LBEsJtIA/qmEeLIcr7MWSq", + "zFkWZ8uD5WUtoEST7ntWKXtSGsD1TAXpuYoQkXlVZ2Fs1KPlgioNNVNy/ejrRbDXVYfupmVYSmupnkd1", + "jyivcWLLNWjYWni5IVoVZfDVlMrrvISZVDxx456eoA7OFO9NCdPALUqqpILPaVSvzPmdlNM7w3c0yZK8", + "FvWLp1DZV5hoLKjxDrGADqfIXUhIJCE4a2vD0nvLVffsWXxaWYovx8QcN23UKb/hs4Yif6g+Yq1jOiRX", + "nKMYiynZ+sM8Hra0VrwdPj2pvRx+gA8y5g77Cj2j5ROMdiZtS0vzazy/yN0d9/v44ur7scJKKRYf4Avg", + "ea5mNr36+L5QcHB/IuG+X3tcPWCvnba25jWwmQH0iD6EeclDHKOIzEnMUygua9oG3SATsS2VOdze1mZa", + "rA254eHgcBB8fPfx/wcAAP//vl9tstzcAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index dce1ca3..20b7792 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2315,6 +2315,17 @@ paths: JSON array of secret references to inject during build. Each object has "id" (required) for use with --mount=type=secret,id=... Example: [{"id": "npm_token"}, {"id": "github_token"}] + 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. + global_cache_runtime: + type: string + description: | + Runtime category for global cache (e.g., "node", "python", "go"). + When specified, the build will import from cache/global/{runtime}. + Admin builds will also export to this location. responses: 202: description: Build created and queued From ffc8c58f21d0c522ea16d993eaabe509587a3a79 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Mon, 26 Jan 2026 17:35:55 -0500 Subject: [PATCH 2/4] refactor: rename global_cache_runtime to global_cache_key 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 --- cmd/api/api/builds.go | 10 +- dump.rdb | Bin 0 -> 89 bytes lib/builds/builder_agent/main.go | 10 +- lib/builds/manager.go | 18 +- lib/builds/types.go | 12 +- lib/oapi/oapi.go | 326 +++++++++++++++---------------- openapi.yaml | 6 +- 7 files changed, 191 insertions(+), 191 deletions(-) create mode 100644 dump.rdb diff --git a/cmd/api/api/builds.go b/cmd/api/api/builds.go index 17022a8..1cd88a8 100644 --- a/cmd/api/api/builds.go +++ b/cmd/api/api/builds.go @@ -41,7 +41,7 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe // Parse multipart form fields var sourceData []byte - var baseImageDigest, cacheScope, dockerfile, globalCacheRuntime string + var baseImageDigest, cacheScope, dockerfile, globalCacheKey string var timeoutSeconds int var isAdminBuild bool var secrets []builds.SecretRef @@ -128,15 +128,15 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe }, nil } isAdminBuild = string(data) == "true" || string(data) == "1" - case "global_cache_runtime": + case "global_cache_key": data, err := io.ReadAll(part) if err != nil { return oapi.CreateBuild400JSONResponse{ Code: "invalid_request", - Message: "failed to read global_cache_runtime field", + Message: "failed to read global_cache_key field", }, nil } - globalCacheRuntime = string(data) + globalCacheKey = string(data) } part.Close() } @@ -158,7 +158,7 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe Dockerfile: dockerfile, Secrets: secrets, IsAdminBuild: isAdminBuild, - GlobalCacheRuntime: globalCacheRuntime, + GlobalCacheKey: globalCacheKey, } // Apply timeout if provided diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000000000000000000000000000000000000..e45aa9f125c09808c954871d11f6fdcb5056fbd4 GIT binary patch literal 89 zcmWG?b@2=~FfcUu#aWb^l3A=5xUrsf{Ha9Wt*7e``#nr>2Jaq0nv|4Uy?YTU;9u?Yb22PL5Z literal 0 HcmV?d00001 diff --git a/lib/builds/builder_agent/main.go b/lib/builds/builder_agent/main.go index c08dafe..843aca2 100644 --- a/lib/builds/builder_agent/main.go +++ b/lib/builds/builder_agent/main.go @@ -49,7 +49,7 @@ type BuildConfig struct { TimeoutSeconds int `json:"timeout_seconds"` NetworkMode string `json:"network_mode"` IsAdminBuild bool `json:"is_admin_build,omitempty"` - GlobalCacheRuntime string `json:"global_cache_runtime,omitempty"` + GlobalCacheKey string `json:"global_cache_key,omitempty"` } // SecretRef references a secret to inject during build @@ -555,8 +555,8 @@ func runBuild(ctx context.Context, config *BuildConfig, logWriter io.Writer) (st // 3. Export to appropriate target based on build type // Import from global cache (read-only for regular builds, read-write for admin builds) - if config.GlobalCacheRuntime != "" { - globalCacheRef := fmt.Sprintf("%s/cache/global/%s", config.RegistryURL, config.GlobalCacheRuntime) + if config.GlobalCacheKey != "" { + globalCacheRef := fmt.Sprintf("%s/cache/global/%s", config.RegistryURL, config.GlobalCacheKey) args = append(args, "--import-cache", fmt.Sprintf("type=registry,ref=%s,registry.insecure=true", globalCacheRef)) log.Printf("Importing from global cache: %s", globalCacheRef) } @@ -571,8 +571,8 @@ func runBuild(ctx context.Context, config *BuildConfig, logWriter io.Writer) (st // Export cache based on build type if config.IsAdminBuild { // Admin build: export to global cache - if config.GlobalCacheRuntime != "" { - globalCacheRef := fmt.Sprintf("%s/cache/global/%s", config.RegistryURL, config.GlobalCacheRuntime) + if config.GlobalCacheKey != "" { + globalCacheRef := fmt.Sprintf("%s/cache/global/%s", config.RegistryURL, config.GlobalCacheKey) args = append(args, "--export-cache", fmt.Sprintf("type=registry,ref=%s,mode=max,registry.insecure=true", globalCacheRef)) log.Printf("Exporting to global cache (admin build): %s", globalCacheRef) } diff --git a/lib/builds/manager.go b/lib/builds/manager.go index faa209e..6d6e744 100644 --- a/lib/builds/manager.go +++ b/lib/builds/manager.go @@ -205,18 +205,18 @@ func (m *manager) CreateBuild(ctx context.Context, req CreateBuildRequest, sourc if req.IsAdminBuild { // Admin build: push access to global cache - if req.GlobalCacheRuntime != "" { + if req.GlobalCacheKey != "" { repoAccess = append(repoAccess, RepoPermission{ - Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheKey), Scope: "push", }) } } else { // Regular tenant build // Pull access to global cache (if runtime specified) - if req.GlobalCacheRuntime != "" { + if req.GlobalCacheKey != "" { repoAccess = append(repoAccess, RepoPermission{ - Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheKey), Scope: "pull", }) } @@ -249,7 +249,7 @@ func (m *manager) CreateBuild(ctx context.Context, req CreateBuildRequest, sourc TimeoutSeconds: policy.TimeoutSeconds, NetworkMode: policy.NetworkMode, IsAdminBuild: req.IsAdminBuild, - GlobalCacheRuntime: req.GlobalCacheRuntime, + GlobalCacheKey: req.GlobalCacheKey, } if err := writeBuildConfig(m.paths, id, buildConfig); err != nil { deleteBuild(m.paths, id) @@ -1084,17 +1084,17 @@ func (m *manager) refreshBuildToken(buildID string, req *CreateBuildRequest) err if req.IsAdminBuild { // Admin build: push access to global cache - if req.GlobalCacheRuntime != "" { + if req.GlobalCacheKey != "" { repoAccess = append(repoAccess, RepoPermission{ - Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheKey), Scope: "push", }) } } else { // Regular tenant build - if req.GlobalCacheRuntime != "" { + if req.GlobalCacheKey != "" { repoAccess = append(repoAccess, RepoPermission{ - Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheRuntime), + Repo: fmt.Sprintf("cache/global/%s", req.GlobalCacheKey), Scope: "pull", }) } diff --git a/lib/builds/types.go b/lib/builds/types.go index 034fb87..a970722 100644 --- a/lib/builds/types.go +++ b/lib/builds/types.go @@ -57,10 +57,10 @@ type CreateBuildRequest struct { // Regular tenant builds only get pull access to global cache. IsAdminBuild bool `json:"is_admin_build,omitempty"` - // GlobalCacheRuntime is the runtime category for global cache (e.g., "node", "python"). - // Used with IsAdminBuild to target cache/global/{runtime}. - // Regular builds import from cache/global/{runtime} with pull-only access. - GlobalCacheRuntime string `json:"global_cache_runtime,omitempty"` + // GlobalCacheKey is the global cache identifier (e.g., "node", "python", "ubuntu", "browser"). + // Used with IsAdminBuild to target cache/global/{key}. + // Regular builds import from cache/global/{key} with pull-only access. + GlobalCacheKey string `json:"global_cache_key,omitempty"` } // BuildPolicy defines resource limits and network policy for a build @@ -150,8 +150,8 @@ type BuildConfig struct { // IsAdminBuild indicates this is an admin build with push access to global cache IsAdminBuild bool `json:"is_admin_build,omitempty"` - // GlobalCacheRuntime is the runtime category for global cache (e.g., "node", "python") - GlobalCacheRuntime string `json:"global_cache_runtime,omitempty"` + // GlobalCacheKey is the global cache identifier (e.g., "node", "python", "ubuntu", "browser") + GlobalCacheKey string `json:"global_cache_key,omitempty"` } // BuildEvent represents a typed SSE event for build streaming diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 93d6b35..4317397 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -821,10 +821,10 @@ type CreateBuildMultipartBody struct { // Dockerfile Dockerfile content. Required if not included in the source tarball. Dockerfile *string `json:"dockerfile,omitempty"` - // GlobalCacheRuntime Runtime category for global cache (e.g., "node", "python", "go"). - // When specified, the build will import from cache/global/{runtime}. + // GlobalCacheKey Global cache identifier (e.g., "node", "python", "ubuntu", "browser"). + // When specified, the build will import from cache/global/{key}. // Admin builds will also export to this location. - GlobalCacheRuntime *string `json:"global_cache_runtime,omitempty"` + GlobalCacheKey *string `json:"global_cache_key,omitempty"` // IsAdminBuild 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. @@ -10246,166 +10246,166 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "rA254eHgcBB8fPfx/wcAAP//vl9tstzcAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index 20b7792..3d0f744 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2320,11 +2320,11 @@ paths: 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. - global_cache_runtime: + global_cache_key: type: string description: | - Runtime category for global cache (e.g., "node", "python", "go"). - When specified, the build will import from cache/global/{runtime}. + Global cache identifier (e.g., "node", "python", "ubuntu", "browser"). + When specified, the build will import from cache/global/{key}. Admin builds will also export to this location. responses: 202: From 94de5d700b92639cd2e39354a2add533c4877e85 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Mon, 26 Jan 2026 17:36:03 -0500 Subject: [PATCH 3/4] chore: remove accidentally committed dump.rdb --- dump.rdb | Bin 89 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dump.rdb diff --git a/dump.rdb b/dump.rdb deleted file mode 100644 index e45aa9f125c09808c954871d11f6fdcb5056fbd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89 zcmWG?b@2=~FfcUu#aWb^l3A=5xUrsf{Ha9Wt*7e``#nr>2Jaq0nv|4Uy?YTU;9u?Yb22PL5Z From 476f7d7d0879c767742473a4381fdca196992a37 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Mon, 26 Jan 2026 17:52:03 -0500 Subject: [PATCH 4/4] fix: update registry path regex to support 3-segment repos 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. --- lib/middleware/oapi_auth.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/middleware/oapi_auth.go b/lib/middleware/oapi_auth.go index 4c06955..4ba08a6 100644 --- a/lib/middleware/oapi_auth.go +++ b/lib/middleware/oapi_auth.go @@ -17,8 +17,10 @@ type contextKey string const userIDKey contextKey = "user_id" -// registryPathPattern matches /v2/{repository}/... paths -var registryPathPattern = regexp.MustCompile(`^/v2/([^/]+(?:/[^/]+)?)/`) +// 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)/`) // RepoPermission defines access permissions for a specific repository. // This mirrors the type in lib/builds/registry_token.go to avoid circular imports.