diff --git a/common/pkg/config/connections.go b/common/pkg/config/connections.go index b554efa207..d7c2c7d8a5 100644 --- a/common/pkg/config/connections.go +++ b/common/pkg/config/connections.go @@ -9,6 +9,7 @@ import ( "path/filepath" "github.com/containers/storage/pkg/ioutils" + "github.com/containers/storage/pkg/lockfile" ) const connectionsFile = "podman-connections.json" @@ -113,6 +114,15 @@ func EditConnectionConfig(callback func(cfg *ConnectionsFile) error) error { if err != nil { return err } + + lockPath := path + ".lock" + lock, err := lockfile.GetLockFile(lockPath) + if err != nil { + return fmt.Errorf("obtain lock file: %w", err) + } + lock.Lock() + defer lock.Unlock() + conf, err := readConnectionConf(path) if err != nil { return fmt.Errorf("read connections file: %w", err) diff --git a/common/pkg/config/connections_test.go b/common/pkg/config/connections_test.go index de027f2360..14176da455 100644 --- a/common/pkg/config/connections_test.go +++ b/common/pkg/config/connections_test.go @@ -3,6 +3,8 @@ package config import ( "os" "path/filepath" + "strconv" + "sync" . "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" @@ -73,6 +75,43 @@ var _ = Describe("Connections conf", func() { gomega.Expect(conf).To(gomega.Equal(orgConf)) }) + It("parallel EditConnectionConfig", func() { + // race test for EditConnectionConfig + // Basic idea spawn a bunch of goroutines and call EditConnectionConfig at the same time. + // We read a int from one field and then +1 one it each time so at the end we must have + // the number in the filed for how many times we called EditConnectionConfig. If it is + // less than it is racy. + count := 50 + wg := sync.WaitGroup{} + wg.Add(count) + for i := 0; i < count; i++ { + go func() { + defer wg.Done() + err := EditConnectionConfig(func(cfg *ConnectionsFile) error { + if cfg.Connection.Default == "" { + cfg.Connection.Default = "1" + return nil + } + // basic idea just add 1 + i, err := strconv.Atoi(cfg.Connection.Default) + if err != nil { + return err + } + i++ + cfg.Connection.Default = strconv.Itoa(i) + return nil + }) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + }() + } + wg.Wait() + path, err := connectionsConfigFile() + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + conf, err := readConnectionConf(path) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + gomega.Expect(conf.Connection.Default).To(gomega.Equal("50")) + }) + Context("GetConnection/Farm", func() { const defConnectionsConf = `{"Connection":{"Default":"test","Connections":{"test":{"URI":"ssh://podman.io"},"QA":{"URI":"ssh://test","Identity":".ssh/id","IsMachine":true}}},"farm":{"Default":"farm1","List":{"farm1":["test"]}}}` const defContainersConf = `