build: ref-count policy FS lifecycle

Make memoized policy FS ref-counted so repeated get()/close() pairs don't
prematurely close shared handles.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-02-26 18:48:38 -08:00
parent f35e45c307
commit 9ef416b258
3 changed files with 118 additions and 11 deletions
+28 -8
View File
@@ -228,28 +228,48 @@ func normalizeLocalPolicyPath(name, contextDir string) string {
type memoizedPolicyFS struct { type memoizedPolicyFS struct {
init func() (fs.StatFS, func() error, error) init func() (fs.StatFS, func() error, error)
once sync.Once mu sync.Mutex
loaded bool
refs int
fs fs.StatFS fs fs.StatFS
closeFn func() error closeFn func() error
err error err error
} }
func (m *memoizedPolicyFS) get() (fs.StatFS, error) { func (m *memoizedPolicyFS) get() (fs.StatFS, error) {
m.once.Do(func() { m.mu.Lock()
if m.init == nil { defer m.mu.Unlock()
return if !m.loaded {
m.loaded = true
if m.init != nil {
m.fs, m.closeFn, m.err = m.init()
} }
m.fs, m.closeFn, m.err = m.init() }
})
if m.err != nil { if m.err != nil {
return nil, m.err return nil, m.err
} }
m.refs++
return m.fs, nil return m.fs, nil
} }
func (m *memoizedPolicyFS) close() error { func (m *memoizedPolicyFS) close() error {
if m.closeFn != nil { m.mu.Lock()
return m.closeFn() if m.refs > 0 {
m.refs--
}
if m.refs > 0 {
m.mu.Unlock()
return nil
}
closeFn := m.closeFn
m.fs = nil
m.closeFn = nil
m.err = nil
m.loaded = false
m.mu.Unlock()
if closeFn != nil {
return closeFn()
} }
return nil return nil
} }
+85
View File
@@ -0,0 +1,85 @@
package build
import (
"io/fs"
"testing"
"testing/fstest"
"github.com/stretchr/testify/require"
)
func TestMemoizedPolicyFSRefCountedClose(t *testing.T) {
var initCalls int
var closeCalls int
m := &memoizedPolicyFS{
init: func() (fs.StatFS, func() error, error) {
initCalls++
root := fstest.MapFS{
"policy.rego": &fstest.MapFile{Data: []byte("package docker\n")},
}
return root, func() error {
closeCalls++
return nil
}, nil
},
}
first, err := m.get()
require.NoError(t, err)
require.NotNil(t, first)
require.Equal(t, 1, initCalls)
second, err := m.get()
require.NoError(t, err)
require.NotNil(t, second)
require.Equal(t, 1, initCalls)
require.NoError(t, m.close())
require.Equal(t, 0, closeCalls)
third, err := m.get()
require.NoError(t, err)
require.NotNil(t, third)
require.Equal(t, 1, initCalls)
require.NoError(t, m.close())
require.Equal(t, 0, closeCalls)
require.NoError(t, m.close())
require.Equal(t, 1, closeCalls)
}
func TestMemoizedPolicyFSReinitializesAfterAllRefsClosed(t *testing.T) {
var initCalls int
var closeCalls int
m := &memoizedPolicyFS{
init: func() (fs.StatFS, func() error, error) {
initCalls++
root := fstest.MapFS{
"policy.rego": &fstest.MapFile{Data: []byte("package docker\n")},
}
return root, func() error {
closeCalls++
return nil
}, nil
},
}
first, err := m.get()
require.NoError(t, err)
require.NotNil(t, first)
require.Equal(t, 1, initCalls)
require.NoError(t, m.close())
require.Equal(t, 1, closeCalls)
second, err := m.get()
require.NoError(t, err)
require.NotNil(t, second)
require.Equal(t, 2, initCalls)
require.NoError(t, m.close())
require.Equal(t, 2, closeCalls)
}
+5 -3
View File
@@ -486,13 +486,15 @@ func (p *Policy) readFile(path string, limit int64) ([]byte, error) {
if p.opt.FS == nil { if p.opt.FS == nil {
return nil, errors.Errorf("no policy FS defined for reading context files") return nil, errors.Errorf("no policy FS defined for reading context files")
} }
fs, cf, err := p.opt.FS() root, closeFS, err := p.opt.FS()
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to get policy FS for reading context files") return nil, errors.Wrapf(err, "failed to get policy FS for reading context files")
} }
defer cf() if closeFS != nil {
defer closeFS()
}
f, err := fs.Open(path) f, err := root.Open(path)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed opening file %q", path) return nil, errors.Wrapf(err, "failed opening file %q", path)
} }