From 92b28f43b16133b40f140d24c0a35fefc24cfa18 Mon Sep 17 00:00:00 2001 From: adababys <8872412+adababys@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:13:20 +0800 Subject: [PATCH] feat(api): opt-in shared template cache scope for cross-team layer dedup Layer caching is content-addressed but the storage path is prefixed by cacheScope, which the API hardcodes to teamID. Two teams building identical layers each get a private copy of the index+tar and the buildID-keyed memfile/rootfs. In a single-trust-domain deployment that is pure waste. Add TEMPLATE_CACHE_SCOPE: when set, use it as the scope for every team so identical layers dedup across teams; when unset, fall back to teamID (unchanged upstream behavior). Orchestrator already honors the CacheScope field in TemplateCreateRequest / InitLayerFileUploadRequest, so no change there. Default-off keeps per-team cache isolation intact for multi-tenant. Refs #3748 --- .../internal/template-manager/cache_scope.go | 38 +++++++++++++++++++ .../template-manager/create_template.go | 2 +- .../upload_template_layer_files.go | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 packages/api/internal/template-manager/cache_scope.go diff --git a/packages/api/internal/template-manager/cache_scope.go b/packages/api/internal/template-manager/cache_scope.go new file mode 100644 index 0000000000..524a5ee800 --- /dev/null +++ b/packages/api/internal/template-manager/cache_scope.go @@ -0,0 +1,38 @@ +package template_manager + +import ( + "os" + "sync" + + "github.com/google/uuid" +) + +// templateCacheScopeEnv, when set to a non-empty value, overrides the per-team +// layer cache scope with a single shared scope so that identical content- +// addressed layers (and the buildID-keyed memfile/rootfs they resolve to) are +// stored and reused across teams instead of once per team. +// +// Default (unset): the scope is the teamID, preserving upstream per-team cache +// isolation. Only set this in a single trusted organization / self-hosted +// deployment where cross-team cache sharing is acceptable: a shared scope lets +// any team hit (and thus prove the existence of, and reuse) another team's +// cached layers. +const templateCacheScopeEnv = "TEMPLATE_CACHE_SCOPE" + +var ( + cacheScopeOnce sync.Once + cacheScopeShared string +) + +// cacheScopeFor returns the layer cache scope for a build owned by teamID. +// If TEMPLATE_CACHE_SCOPE is set it returns that shared scope for every team +// (cross-team dedup); otherwise it returns the teamID (per-team isolation). +func cacheScopeFor(teamID uuid.UUID) string { + cacheScopeOnce.Do(func() { + cacheScopeShared = os.Getenv(templateCacheScopeEnv) + }) + if cacheScopeShared != "" { + return cacheScopeShared + } + return teamID.String() +} diff --git a/packages/api/internal/template-manager/create_template.go b/packages/api/internal/template-manager/create_template.go index 66e38301a5..ff00927282 100644 --- a/packages/api/internal/template-manager/create_template.go +++ b/packages/api/internal/template-manager/create_template.go @@ -157,7 +157,7 @@ func (tm *TemplateManager) CreateTemplate( _, err = client.Template.TemplateCreate( ctx, &templatemanagergrpc.TemplateCreateRequest{ Template: template, - CacheScope: new(teamID.String()), + CacheScope: new(cacheScopeFor(teamID)), Version: &version, }, ) diff --git a/packages/api/internal/template-manager/upload_template_layer_files.go b/packages/api/internal/template-manager/upload_template_layer_files.go index 13e3c08fee..d60c755fd5 100644 --- a/packages/api/internal/template-manager/upload_template_layer_files.go +++ b/packages/api/internal/template-manager/upload_template_layer_files.go @@ -18,7 +18,7 @@ func (tm *TemplateManager) InitLayerFileUpload(ctx context.Context, clusterID uu resp, err := client.Template.InitLayerFileUpload( ctx, &templatemanager.InitLayerFileUploadRequest{ - CacheScope: new(teamID.String()), + CacheScope: new(cacheScopeFor(teamID)), TemplateID: templateID, Hash: hash, },