From 280cdc4f0c9de42146ca8727a2e551746c3533fc Mon Sep 17 00:00:00 2001 From: Anis Khan <2815766+aniskhan001@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:45:34 +0200 Subject: [PATCH] fix(downloader): serialize downloadAll per ChartPath safeMoveDeps reads the destination charts/ directory and then moves in new charts and deletes ones missing from that snapshot. Separate Manager instances calling downloadAll concurrently against the same ChartPath can race on this read-then-write, e.g. one call's freshly moved-in chart being deleted by another call's "delete outdated charts" pass because it wasn't in that call's earlier snapshot. Signed-off-by: Anis Khan <2815766+aniskhan001@users.noreply.github.com> --- pkg/downloader/manager.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index b6c052c66..74aa8a8c3 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -240,11 +240,26 @@ func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) return res.Resolve(req, repoNames) } +// chartPathLocks serializes downloadAll calls per ChartPath, since concurrent +// calls targeting the same path can race on the shared "charts/" directory. +var chartPathLocks sync.Map // map[string]*sync.Mutex + +func lockForChartPath(chartPath string) *sync.Mutex { + lock, _ := chartPathLocks.LoadOrStore(chartPath, &sync.Mutex{}) + return lock.(*sync.Mutex) +} + // downloadAll takes a list of dependencies and downloads them into charts/ // // It will delete versions of the chart that exist on disk and might cause // a conflict. func (m *Manager) downloadAll(deps []*chart.Dependency) error { + // safeMoveDeps below isn't safe against other calls targeting the same + // ChartPath, so serialize per-path here. + lock := lockForChartPath(m.ChartPath) + lock.Lock() + defer lock.Unlock() + repos, err := m.loadChartRepositories() if err != nil { return err