@@ -7,12 +7,32 @@ import (
77 "path/filepath"
88 "strings"
99 "sync"
10+ "syscall"
1011
1112 "github.com/utmstack/utmstack/backend/modules/integrations/connectors"
1213 "github.com/utmstack/utmstack/backend/modules/integrations/domain"
1314 "gopkg.in/yaml.v3"
1415)
1516
17+ // withFileLock acquires an exclusive advisory lock on <path>.lock via flock,
18+ // runs fn, releases the lock on close. The kernel drops the lock on fd close
19+ // (including process death), so a crashed writer never leaves a stuck lock.
20+ // Linux/BSD only; UTMStack runs in Linux containers.
21+ func withFileLock (path string , fn func () error ) error {
22+ if err := os .MkdirAll (filepath .Dir (path ), 0o755 ); err != nil {
23+ return err
24+ }
25+ lf , err := os .OpenFile (path + ".lock" , os .O_CREATE | os .O_RDWR , 0o600 )
26+ if err != nil {
27+ return err
28+ }
29+ defer lf .Close ()
30+ if err := syscall .Flock (int (lf .Fd ()), syscall .LOCK_EX ); err != nil {
31+ return err
32+ }
33+ return fn ()
34+ }
35+
1636var _ connectors.TenantRepository = (* TenantStore )(nil )
1737
1838type TenantStore struct {
@@ -37,29 +57,44 @@ var pluginNames = map[string]string{
3757 "SOPHOS" : "sophos" ,
3858}
3959
40- func tenantFileName (module string ) string {
60+ func pluginKey (module string ) string {
4161 name , ok := pluginNames [strings .ToUpper (module )]
4262 if ! ok {
4363 name = strings .ToLower (module )
4464 }
45- return "system_plugins_" + name + ".yaml"
65+ return name
66+ }
67+
68+ func tenantFileName (module string ) string {
69+ return "system_plugins_" + pluginKey (module ) + ".yaml"
4670}
4771
4872func (s * TenantStore ) path (module string ) string {
4973 return filepath .Join (s .dir , tenantFileName (module ))
5074}
5175
76+ // pluginsFile is the on-disk wrapper: plugins.<name>.tenants: [...].
77+ type pluginsFile struct {
78+ Plugins map [string ]pluginEntry `yaml:"plugins"`
79+ }
80+
81+ type pluginEntry struct {
82+ Tenants []domain.Tenant `yaml:"tenants"`
83+ }
84+
5285func (s * TenantStore ) SetActiveByModule (_ context.Context , module string , active bool ) error {
5386 if active {
5487 return nil
5588 }
5689 s .mu .Lock ()
5790 defer s .mu .Unlock ()
5891
59- if err := os .Remove (s .path (module )); err != nil && ! errors .Is (err , os .ErrNotExist ) {
60- return err
61- }
62- return nil
92+ return withFileLock (s .path (module ), func () error {
93+ if err := os .Remove (s .path (module )); err != nil && ! errors .Is (err , os .ErrNotExist ) {
94+ return err
95+ }
96+ return nil
97+ })
6398}
6499
65100func (s * TenantStore ) Load (module string ) ([]domain.Tenant , error ) {
@@ -70,18 +105,28 @@ func (s *TenantStore) Load(module string) ([]domain.Tenant, error) {
70105 if err != nil {
71106 return nil , err
72107 }
73- var tenants []domain. Tenant
74- if err := yaml .Unmarshal (data , & tenants ); err != nil {
108+ var pf pluginsFile
109+ if err := yaml .Unmarshal (data , & pf ); err != nil {
75110 return nil , err
76111 }
77- return tenants , nil
112+ entry , ok := pf .Plugins [pluginKey (module )]
113+ if ! ok {
114+ return []domain.Tenant {}, nil
115+ }
116+ return entry .Tenants , nil
78117}
79118
80119func (s * TenantStore ) Save (module string , tenants []domain.Tenant ) error {
81- if err := os .MkdirAll (s .dir , 0o755 ); err != nil {
82- return err
83- }
84- data , err := yaml .Marshal (tenants )
120+ return withFileLock (s .path (module ), func () error {
121+ return s .saveLocked (module , tenants )
122+ })
123+ }
124+
125+ // saveLocked writes the file assuming the caller already holds the file lock.
126+ func (s * TenantStore ) saveLocked (module string , tenants []domain.Tenant ) error {
127+ key := pluginKey (module )
128+ pf := pluginsFile {Plugins : map [string ]pluginEntry {key : {Tenants : tenants }}}
129+ data , err := yaml .Marshal (pf )
85130 if err != nil {
86131 return err
87132 }
@@ -97,38 +142,42 @@ func (s *TenantStore) Upsert(module string, t domain.Tenant) error {
97142 s .mu .Lock ()
98143 defer s .mu .Unlock ()
99144
100- tenants , err := s .Load (module )
101- if err != nil {
102- return err
103- }
104- for i := range tenants {
105- if tenants [i ].Name == t .Name {
106- tenants [i ] = t
107- return s .Save (module , tenants )
145+ return withFileLock (s .path (module ), func () error {
146+ tenants , err := s .Load (module )
147+ if err != nil {
148+ return err
108149 }
109- }
110- return s .Save (module , append (tenants , t ))
150+ for i := range tenants {
151+ if tenants [i ].Name == t .Name {
152+ tenants [i ] = t
153+ return s .saveLocked (module , tenants )
154+ }
155+ }
156+ return s .saveLocked (module , append (tenants , t ))
157+ })
111158}
112159
113160func (s * TenantStore ) Delete (module , name string ) error {
114161 s .mu .Lock ()
115162 defer s .mu .Unlock ()
116163
117- tenants , err := s .Load (module )
118- if err != nil {
119- return err
120- }
121- out := make ([]domain.Tenant , 0 , len (tenants ))
122- found := false
123- for _ , t := range tenants {
124- if t .Name == name {
125- found = true
126- continue
164+ return withFileLock (s .path (module ), func () error {
165+ tenants , err := s .Load (module )
166+ if err != nil {
167+ return err
127168 }
128- out = append (out , t )
129- }
130- if ! found {
131- return domain .ErrTenantNotFound
132- }
133- return s .Save (module , out )
169+ out := make ([]domain.Tenant , 0 , len (tenants ))
170+ found := false
171+ for _ , t := range tenants {
172+ if t .Name == name {
173+ found = true
174+ continue
175+ }
176+ out = append (out , t )
177+ }
178+ if ! found {
179+ return domain .ErrTenantNotFound
180+ }
181+ return s .saveLocked (module , out )
182+ })
134183}
0 commit comments