diff --git a/pkg/repo/v1/chartrepo.go b/pkg/repo/v1/chartrepo.go index 95c04ccef..2c442c985 100644 --- a/pkg/repo/v1/chartrepo.go +++ b/pkg/repo/v1/chartrepo.go @@ -17,6 +17,7 @@ limitations under the License. package repo // import "helm.sh/helm/v4/pkg/repo/v1" import ( + "bytes" "crypto/rand" "encoding/base64" "encoding/json" @@ -28,6 +29,7 @@ import ( "path/filepath" "strings" + "helm.sh/helm/v4/internal/fileutil" "helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/helmpath" ) @@ -108,12 +110,13 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) { } chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) os.MkdirAll(filepath.Dir(chartsFile), 0755) - os.WriteFile(chartsFile, []byte(charts.String()), 0644) + + fileutil.AtomicWriteFile(chartsFile, bytes.NewReader([]byte(charts.String())), 0644) // Create the index file in the cache directory fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)) os.MkdirAll(filepath.Dir(fname), 0755) - return fname, os.WriteFile(fname, index, 0644) + return fname, fileutil.AtomicWriteFile(fname, bytes.NewReader(index), 0644) } type findChartInRepoURLOptions struct { diff --git a/pkg/repo/v1/chartrepo_test.go b/pkg/repo/v1/chartrepo_test.go index 05e034dd8..e82d72279 100644 --- a/pkg/repo/v1/chartrepo_test.go +++ b/pkg/repo/v1/chartrepo_test.go @@ -22,8 +22,10 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "runtime" "strings" + "sync" "testing" "time" @@ -31,6 +33,7 @@ import ( "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/getter" + "helm.sh/helm/v4/pkg/helmpath" ) type CustomGetter struct { @@ -91,6 +94,60 @@ func TestIndexCustomSchemeDownload(t *testing.T) { } } +func TestConcurrenyDownloadIndex(t *testing.T) { + srv, err := startLocalServerForTests(nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + repo, err := NewChartRepository(&Entry{ + Name: "nginx", + URL: srv.URL, + }, getter.All(&cli.EnvSettings{})) + + if err != nil { + t.Fatalf("Problem loading chart repository from %s: %v", srv.URL, err) + } + repo.CachePath = t.TempDir() + + // initial download index + idx, err := repo.DownloadIndexFile() + if err != nil { + t.Fatalf("Failed to download index file to %s: %v", idx, err) + } + + indexFName := filepath.Join(repo.CachePath, helmpath.CacheIndexFile(repo.Config.Name)) + + var wg sync.WaitGroup + + // Simultaneously start multiple goroutines that: + // 1) download index.yaml via DownloadIndexFile (write operation), + // 2) read index.yaml via LoadIndexFile (read operation). + // This checks for race conditions and ensures correct behavior under concurrent read/write access. + for range 150 { + wg.Add(1) + + go func() { + defer wg.Done() + idx, err := repo.DownloadIndexFile() + if err != nil { + t.Errorf("Failed to download index file to %s: %v", idx, err) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + _, err := LoadIndexFile(indexFName) + if err != nil { + t.Errorf("Failed to load index file: %v", err) + } + }() + } + wg.Wait() +} + // startLocalServerForTests Start the local helm server func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { if handler == nil {