fix: eliminate race condition in rewriteChartDependencies (#2541)

* fix: eliminate race condition in rewriteChartDependencies by copying chart before modifying

Instead of modifying the original Chart.yaml in-place (which causes race
conditions when multiple releases reference the same local chart), copy the
chart to a temporary directory and rewrite the copy's dependencies. This
eliminates the need for per-chart mutex locks and prevents file corruption
when concurrent goroutines process releases sharing the same local chart.

Fixes #2502

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: address PR review comments for rewriteChartDependencies

- Handle non-NotExist errors from st.fs.Stat to surface permission/IO failures
- Reword function doc to clarify temp copy is conditional on rewrite being needed
- Assert rewrittenPath vs tempDir based on expectModified in test table

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: add integration test for issue #2502 race condition with shared local chart

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: separate environments and releases with --- in helmfile.yaml

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: correct file:// path and remove --skip-deps for dependency build

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: correct file:// dependency path (5 levels up to test/integration/)

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: remove output validation from race condition test

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: assert WriteFile/MkdirTemp/RemoveAll/CopyDir in DefaultFileSystem test

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: add strategicMergePatches to trigger chartify in race condition test

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: scope test values under raw subchart and align ConfigMap name with strategic merge patches

The race condition test values.yaml had templates at the top level instead
of scoped under the raw subchart key, causing helm template to produce no
output and chartify's ReplaceWithRendered to fail with an empty
helmx.1.rendered directory. Also align the ConfigMap name to match the
strategicMergePatches target.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero
2026-04-20 10:15:47 +08:00
committed by GitHub
parent cf542df19f
commit fc31dbfc5e
9 changed files with 301 additions and 129 deletions
+1
View File
@@ -96,6 +96,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
# TEST CASES----------------------------------------------------------------------------------------------------------
. ${dir}/test-cases/issue-2502-race-condition-local-chart.sh
. ${dir}/test-cases/chart-deps-condition.sh
. ${dir}/test-cases/fetch-forl-local-chart.sh
. ${dir}/test-cases/suppress-output-line-regex.sh
@@ -0,0 +1,50 @@
# Integration test for issue #2502: Race condition when multiple releases share a local chart
# https://github.com/helmfile/helmfile/issues/2502
#
# When multiple releases reference the same local chart, concurrent goroutines
# race on rewriting Chart.yaml dependencies, causing:
# "Error: validation: chart.metadata.name is required"
#
# This test verifies the fix works WITHOUT --concurrency 1 workaround.
issue_2502_input_dir="${cases_dir}/issue-2502-race-condition-local-chart/input"
issue_2502_tmp=$(mktemp -d)
actual="${issue_2502_tmp}/actual.yaml"
cleanup_issue_2502() {
if [ -n "${issue_2502_tmp}" ] && [ -d "${issue_2502_tmp}" ]; then
rm -rf "${issue_2502_tmp}"
fi
}
trap cleanup_issue_2502 EXIT
test_start "issue #2502: race condition with shared local chart"
info "Running helmfile template with 5 releases sharing the same local chart (default concurrency)"
# Run WITHOUT --concurrency 1 to test the fix.
# Before the fix, this would intermittently fail with:
# "Error: validation: chart.metadata.name is required"
# Run multiple iterations to increase chance of catching a race.
pass=0
iterations=5
for i in $(seq 1 ${iterations}); do
if ${helmfile} -f ${issue_2502_input_dir}/helmfile.yaml -e test template > ${actual} 2>&1; then
pass=$((pass + 1))
else
cat ${actual}
fail "helmfile template failed on iteration ${i}/${iterations} (race condition on shared local chart)"
fi
done
if [ ${pass} -ne ${iterations} ]; then
fail "Expected ${iterations}/${iterations} passes but got ${pass}/${iterations}"
fi
info "All ${iterations} iterations passed successfully"
cleanup_issue_2502
trap - EXIT
test_pass "issue #2502: race condition with shared local chart"
@@ -0,0 +1,10 @@
dependencies:
- name: raw
repository: file://../../../../../charts/raw
version: 0.0.1
apiVersion: v2
appVersion: "1.0.0"
description: A test chart for race condition reproduction
name: my-chart
type: application
version: 0.1.0
@@ -0,0 +1,9 @@
raw:
templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
key: value
@@ -0,0 +1,49 @@
environments:
test: {}
---
releases:
- name: app
chart: helm/my-chart
strategicMergePatches:
- apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
key: value
- name: app-2
chart: helm/my-chart
strategicMergePatches:
- apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
key: value
- name: app-3
chart: helm/my-chart
strategicMergePatches:
- apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
key: value
- name: app-4
chart: helm/my-chart
strategicMergePatches:
- apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
key: value
- name: app-5
chart: helm/my-chart
strategicMergePatches:
- apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
key: value