diff --git a/pkg/repo/chartrepo.go b/pkg/repo/chartrepo.go index e41226fa4..bbcab2ae8 100644 --- a/pkg/repo/chartrepo.go +++ b/pkg/repo/chartrepo.go @@ -17,6 +17,7 @@ limitations under the License. package repo // import "helm.sh/helm/v4/pkg/repo" import ( + "context" "crypto/rand" "encoding/base64" "encoding/json" @@ -27,6 +28,9 @@ import ( "os" "path/filepath" "strings" + "time" + + "github.com/gofrs/flock" "helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/helmpath" @@ -106,13 +110,41 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) { for name := range indexFile.Entries { fmt.Fprintln(&charts, name) } + chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) os.MkdirAll(filepath.Dir(chartsFile), 0755) - os.WriteFile(chartsFile, []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) + + // context for lock files + lockCtx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + // lock the charts file + chLock := flock.New(filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)+".lock")) + chLocked, err := chLock.TryLockContext(lockCtx, 500*time.Millisecond) + if err == nil && chLocked { + defer chLock.Unlock() + } + if err != nil { + return "", err + } + + // write charts files after lock + os.WriteFile(chartsFile, []byte(charts.String()), 0644) + + // lock the index file + idxLock := flock.New(filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)+".lock")) + idxLocked, err := idxLock.TryLockContext(lockCtx, 500*time.Millisecond) + if err == nil && idxLocked { + defer idxLock.Unlock() + } + if err != nil { + return "", err + } + + // Create the index file in the cache directory return fname, os.WriteFile(fname, index, 0644) } diff --git a/pkg/repo/index.go b/pkg/repo/index.go index c26d7581c..b93816d71 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -18,6 +18,7 @@ package repo import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -30,6 +31,7 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/gofrs/flock" "sigs.k8s.io/yaml" "helm.sh/helm/v4/internal/fileutil" @@ -103,6 +105,19 @@ func NewIndexFile() *IndexFile { // LoadIndexFile takes a file at the given path and returns an IndexFile object func LoadIndexFile(path string) (*IndexFile, error) { + lockCtx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + idxLock := flock.New(filepath.Join(path + ".lock")) + + idxLocked, err := idxLock.TryRLockContext(lockCtx, 500*time.Millisecond) + if err == nil && idxLocked { + defer idxLock.Unlock() + } + if err != nil { + return nil, err + } + b, err := os.ReadFile(path) if err != nil { return nil, err @@ -225,6 +240,19 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { // // The mode on the file is set to 'mode'. func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { + lockCtx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + idxLock := flock.New(dest + ".lock") + idxLocked, err := idxLock.TryLockContext(lockCtx, 500*time.Millisecond) + + if err == nil && idxLocked { + defer idxLock.Unlock() + } + if err != nil { + return err + } + b, err := yaml.Marshal(i) if err != nil { return err @@ -237,6 +265,19 @@ func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { // // The mode on the file is set to 'mode'. func (i IndexFile) WriteJSONFile(dest string, mode os.FileMode) error { + lockCtx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + idxLock := flock.New(dest + ".lock") + idxLocked, err := idxLock.TryLockContext(lockCtx, 500*time.Millisecond) + + if err == nil && idxLocked { + defer idxLock.Unlock() + } + if err != nil { + return err + } + b, err := json.MarshalIndent(i, "", " ") if err != nil { return err