fix: resolve issues #2295, #2296, and #2297#2298
Conversation
2bbcc3a to
2762123
Compare
|
@aditmeno split 3 PRs are better next time. thanks so much. |
|
@yxxhero PR is ready for review |
34125ac to
36290c3
Compare
161444c to
e1e0416
Compare
|
@champtar This concern is addressed in the merged changes from PR #2301. The new implementation does not force How it works: The When
This means:
The 60-second TTL is chosen to cover a typical helmfile run duration while still allowing fresh pulls on subsequent runs. This should handle changing tags like |
e1e0416 to
751cda6
Compare
|
@aditmeno thanks for working on this Skipping refresh when the chart is recent is a separate issue, this is a good improvement, but the time should not be hardcoded |
Address PR review feedback from @champtar about the OCI chart caching mechanism. The previous implementation used a 60-second timeout which was arbitrary and caused race conditions when helm deployments took longer (e.g., deployments triggering scaling up/down). Changes: - Replace 60s refresh marker timeout with proper reader-writer locks - Use shared locks (RLock) when using cached charts (allows concurrent reads) - Use exclusive locks (Lock) when refreshing/downloading charts - Hold locks during entire helm operation lifecycle (not just during download) - Add getNamedRWMutex() for in-process RW coordination - Update PrepareCharts() to return locks map for lifecycle management - Add chartLockReleaser in run.go to release locks after helm callback - Remove unused mutexMap and getNamedMutex (replaced by RW versions) - Add comprehensive tests for shared/exclusive lock behavior This eliminates the race condition where one process could delete a cached chart while another process's helm command was still using it. Fixes review comment on PR helmfile#2298 Signed-off-by: Aditya Menon <[email protected]>
|
@champtar Thank you for the feedback! You're absolutely right about the 60s timeout being problematic. I've addressed your concerns. Changes made:
Why release locks immediately? Holding locks for the entire helm operation caused deadlocks when multiple releases shared the same OCI chart:
Test coverage:
|
8198420 to
0077ab9
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes four related bugs affecting chart preparation, caching, and OCI registry authentication in Helmfile. The changes implement filesystem-level locking to prevent race conditions during parallel chart downloads (issue #2295), ensure helmDefaults settings are properly respected for repo synchronization (issue #2296), fix path normalization for local charts with transformers (issue #2297), and correct OCI registry login URL handling.
Key changes:
- Added cross-process reader-writer locking using
github.com/gofrs/flockfor safe parallel OCI chart downloads - Extended repo skip logic to check both CLI options and
helmDefaults.skipDeps/skipRefresh - Normalized local chart paths to absolute before chartification to prevent "repo not found" errors
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/state/state.go | Core locking implementation with acquireChartLock, acquireSharedLock, acquireExclusiveLock functions; added extractRegistryHost for OCI registry login; fixed local chart path normalization before chartification; exported Logger() method |
| pkg/app/run.go | Updated withPreparedCharts and Deps to check helmDefaults.skipDeps/skipRefresh when deciding to skip repo sync; added chartLockReleaser type and logic (currently no-op since locks released immediately) |
| pkg/state/oci_chart_lock_test.go | Comprehensive unit tests for file locking behavior including shared/exclusive lock interactions, double-check locking pattern, and timeout handling |
| pkg/state/issue_2296_test.go | Unit tests verifying helmDefaults settings are respected in skip logic |
| pkg/state/issue_2297_test.go | Unit tests for local chart path normalization with transformers |
| pkg/state/state_test.go | Unit tests for extractRegistryHost and OCI registry login behavior |
| pkg/exectest/helm.go | Added RegistryLoginHost field to capture host passed to RegistryLogin for testing |
| test/integration/test-cases/oci-parallel-pull.sh | Integration test verifying parallel OCI chart pulls don't cause race conditions |
| test/integration/test-cases/issue-2297-local-chart-transformers.sh | Integration test for local chart with transformers scenario |
| test/integration/test-cases/oci-parallel-pull/input/helmfile.yaml | Test input file defining OCI chart for parallel pull testing |
| test/integration/test-cases/issue-2297-local-chart-transformers/input/* | Test input files with helmfile.d structure and local chart for issue #2297 |
| test/integration/run.sh | Added new integration tests to test suite execution |
| go.mod | Promoted github.com/gofrs/flock from indirect to direct dependency |
This commit addresses the automated review comments from GitHub Copilot: 1. pkg/state/state.go: Add nil check for logger in Release() method to prevent potential nil pointer dereference when logger is nil. 2. pkg/state/state.go: Fix misleading comment about "external callers" to accurately reflect that Logger() is used by the app package. 3. pkg/state/issue_2296_test.go: Add comment noting that boolPtr helper is already defined in skip_test.go (shared across test files). 4. test/integration/test-cases/oci-parallel-pull.sh: Replace hardcoded /tmp paths with a dedicated temp directory for test outputs. Add cleanup for the output directory in the cleanup function. 5. test/integration/test-cases/issue-2297-local-chart-transformers.sh: Add cleanup trap to remove temp directory on exit, preventing leftover files from accumulating. Signed-off-by: Aditya Menon <[email protected]>
When multiple workers concurrently process releases using the same chart, they need to coordinate to avoid redundant downloads. The previous fix set SkipRefresh=true for OCI charts, which prevented legitimate refresh scenarios (e.g., floating tags). This commit implements a better solution using a callback mechanism: 1. acquireChartLock() now accepts an optional skipRefreshCheck callback 2. Before deleting a cached chart for refresh, the callback is invoked 3. If the callback returns true (in-memory cache has the chart), skip refresh 4. This allows deduplication within a process while respecting cross-run refresh The flow is now: - Worker 1 downloads chart, adds to in-memory cache, releases lock - Worker 2 acquires lock, sees needsRefresh=true, but callback sees in-memory cache is populated → uses cached instead of deleting This correctly handles: - Within-process deduplication: only one download per chart - Cross-run refresh: respects --skip-refresh flag for floating tags - Immutable versions: cached and reused as expected Changes: - Add skipRefreshCheck callback parameter to acquireChartLock() - Update getOCIChart() to pass in-memory cache check callback - Update forcedDownloadChart() to pass in-memory cache check callback - Remove SkipRefresh=true workaround for OCI charts Signed-off-by: Aditya Menon <[email protected]>
This commit addresses the automated review comments from GitHub Copilot: 1. pkg/state/state.go: Add nil check for logger in Release() method to prevent potential nil pointer dereference when logger is nil. 2. pkg/state/state.go: Fix misleading comment about "external callers" to accurately reflect that Logger() is used by the app package. 3. pkg/state/issue_2296_test.go: Add comment noting that boolPtr helper is already defined in skip_test.go (shared across test files). 4. test/integration/test-cases/oci-parallel-pull.sh: Replace hardcoded /tmp paths with a dedicated temp directory for test outputs. Add cleanup for the output directory in the cleanup function. 5. test/integration/test-cases/issue-2297-local-chart-transformers.sh: Add cleanup trap to remove temp directory on exit, preventing leftover files from accumulating. 6. Remove dead code: The chartLocks map in PrepareCharts was always empty since locks are released immediately after download. Removed the unused return value and corresponding handling in run.go to improve code clarity and maintainability. Signed-off-by: Aditya Menon <[email protected]>
The integration test was intermittently failing in CI due to Docker Hub rate limiting or network issues. These failures are not helmfile bugs. Changes: - Add is_registry_error() function to detect external registry issues (rate limits, network timeouts, connection refused, etc.) - Check for the race condition bug (issue helmfile#2295) first and fail fast - If other failures occur, check if they're registry-related - Skip test gracefully when registry issues are detected instead of failing CI on external infrastructure problems This ensures the test still catches the actual race condition bug while not causing false failures due to Docker Hub rate limits in CI. Signed-off-by: Aditya Menon <[email protected]>
The integration test was failing in CI for two reasons: 1. Docker Hub rate limiting or network issues causing helmfile to fail 2. The test script exits early due to `set -e` when `wait` returns non-zero Changes: - Use `wait $pid || exit=$?` pattern to capture exit codes without triggering set -e. When wait returns non-zero, the || branch captures the exit code into the variable, preventing script termination. - Add is_registry_error() function to detect external registry issues (rate limits, network timeouts, connection refused, etc.) - Check for the race condition bug (issue helmfile#2295) first and fail fast - Skip test gracefully when registry issues are detected instead of failing CI on external infrastructure problems This ensures the test still catches the actual race condition bug while not causing false failures due to Docker Hub rate limits in CI. Signed-off-by: Aditya Menon <[email protected]>
…lease Address Copilot review comments: 1. pkg/state/state.go: Reinitialize fileLock after releasing shared lock When upgrading from shared to exclusive lock, the fileLock needs to be reinitialized with flock.New() after calling Release(). This ensures a fresh flock object is used for the exclusive lock acquisition. 2. test/integration/test-cases/oci-parallel-pull.sh: Add lock file verification warning if no lock files are found, to ensure the locking mechanism is actually being tested. Signed-off-by: Aditya Menon <[email protected]>
31c7817 to
4053b99
Compare
Address 8 Copilot review comments: 1. pkg/state/state.go: Release in-process mutex during retry backoff to avoid blocking other goroutines for up to 90 seconds. 2. pkg/state/state.go: Include chartPath in shared lock error message for better debugging. 3. pkg/state/state.go: Document that extractRegistryHost does not handle URLs with query parameters or fragments (uncommon for OCI registries). 4. pkg/state/state.go: Document that skipRefreshCheck callback should be fast and non-blocking since it runs while holding exclusive lock. 5. oci-parallel-pull.sh: Use case-insensitive grep (-i flag) to catch error variations like "I/O timeout". 6. helmfile.yaml: Expand comment explaining why library charts can't be used for this test (they can't be templated by Helm). Skipped (with justification): - PrepareChartKey helper: Only 2 usages with different source structs - Context reuse in retry: Per-attempt contexts provide clearer semantics Signed-off-by: Aditya Menon <[email protected]>
1. Make race condition detection grep more robust (oci-parallel-pull.sh) - Use case-insensitive extended regex (-iqE) - Add multiple pattern variations to catch different tar/helm versions 2. Remove unused Logger() method from HelmState (state.go) - Method was never called; all lock releases use st.logger directly 3. Add clarifying comments for lock retry behavior (state.go) - Document why file system errors are retried but timeouts are not - Explain flock returns (false, nil) on context deadline exceeded Signed-off-by: Aditya Menon <[email protected]>
Lock files are ephemeral and may be cleaned up immediately after helmfile processes complete. Update comments and warning message to make clear their absence doesn't indicate locking wasn't used. Signed-off-by: Aditya Menon <[email protected]>
The helm-git plugin requires HELM_BIN environment variable to be set. Without it, the plugin fails with "HELM_BIN: parameter not set". Add HELM_BIN=/usr/local/bin/helm to all Dockerfile variants. Fixes helmfile#2303 Signed-off-by: Aditya Menon <[email protected]>
Summary
This PR fixes four related bugs that affect chart preparation, caching, and OCI registry authentication:
Issue #2295: OCI chart cache conflicts with parallel helmfile processes
Problem: When running multiple helmfile processes in parallel using the same OCI chart, race conditions cause "file already exists" errors due to concurrent cache writes.
Solution:
flockfor cross-process synchronizationRLock) for reading cached charts, exclusive locks (Lock) for download/refreshtempDircleanup is deferred until after helm operations complete inwithPreparedCharts(), so charts won't be deleted mid-useFiles changed:
pkg/state/state.go(acquireChartLock, acquireSharedLock, acquireExclusiveLock, getOCIChart, forcedDownloadChart functions)Issue #2296:
helmDefaults.skipDepsandhelmDefaults.skipRefreshare ignoredProblem: CLI flags
--skip-depsand--skip-refreshwork correctly, but setting these inhelmfile.yamlunderhelmDefaultshas no effect - repos are still synced.Root cause:
SkipReposwas only set from CLI options inChartPrepareOptions, not fromhelmDefaults.Solution: Check both CLI options AND
r.state.HelmDefaults.SkipDeps/SkipRefreshwhen deciding whether to skip repo sync.Files changed:
pkg/app/run.go(withPreparedCharts and Deps functions)Issue #2297: Local chart + transformers causes panic
Problem: Regression from 1.1.9 to 1.2.1. When using a local chart with a relative path (
chart: ../chart) combined with transformers inhelmfile.d/, helmfile fails with:Root cause: The relative chart path was not normalized to an absolute path before being passed to chartify. Chartify then tried to
helm pull ../charttreating it as a remote chart reference.Solution: Normalize local chart paths to absolute before calling
processChartification().Files changed:
pkg/state/state.go(prepareChartForRelease function)OCI Registry Login URL Fix (from PR #2301)
Problem: When using OCI registries with chart paths (e.g., ECR with
/chartspath),helm registry loginwould fail because the full URL was passed instead of just the registry host.Solution:
extractRegistryHost()function to extract just the registry host from OCI URLs123456789012.dkr.ecr.us-east-1.amazonaws.com/charts→123456789012.dkr.ecr.us-east-1.amazonaws.comSyncReposto use the extracted host for OCI registry loginFiles changed:
pkg/state/state.go(extractRegistryHost, SyncRepos functions)Test Plan
pkg/state/issue_2296_test.go)pkg/state/issue_2297_test.go)pkg/state/oci_chart_lock_test.go)Test_extractRegistryHost,TestHelmState_SyncRepos_OCI)test/integration/test-cases/issue-2297-local-chart-transformers.sh)test/integration/test-cases/oci-parallel-pull.sh)Breaking Changes
None. All changes are backwards compatible bug fixes.
Fixes #2295
Fixes #2296
Fixes #2297