Skip to content

Commit 5130feb

Browse files
fix(auth-callout): restore nv-config permissions watcher (#26)
Signed-off-by: Frank Spitulski <fspitulski@nvidia.com>
1 parent c708899 commit 5130feb

34 files changed

Lines changed: 7757 additions & 56 deletions

auth-callout/go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ require (
2121
)
2222

2323
require (
24-
github.com/fsnotify/fsnotify v1.9.0
2524
github.com/knadh/koanf/maps v0.1.2
2625
github.com/knadh/koanf/parsers/yaml v1.1.0
2726
github.com/knadh/koanf/providers/env v1.1.0
2827
github.com/knadh/koanf/providers/file v1.2.0
2928
github.com/knadh/koanf/providers/rawbytes v1.0.0
3029
github.com/knadh/koanf/v2 v2.3.0
3130
github.com/prometheus/client_golang v1.23.2
31+
gitlab-master.nvidia.com/ncp/vmaas/libs/golang/nv-config v0.1.7
3232
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
3333
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
3434
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
@@ -44,6 +44,7 @@ require (
4444
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4545
github.com/davecgh/go-spew v1.1.1 // indirect
4646
github.com/felixge/httpsnoop v1.0.4 // indirect
47+
github.com/fsnotify/fsnotify v1.9.0 // indirect
4748
github.com/go-logr/logr v1.4.3 // indirect
4849
github.com/go-logr/stdr v1.2.2 // indirect
4950
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
@@ -53,6 +54,7 @@ require (
5354
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
5455
github.com/inconshreveable/mousetrap v1.1.0 // indirect
5556
github.com/klauspost/compress v1.18.0 // indirect
57+
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
5658
github.com/minio/highwayhash v1.0.3 // indirect
5759
github.com/mitchellh/copystructure v1.2.0 // indirect
5860
github.com/mitchellh/reflectwalk v1.0.2 // indirect

auth-callout/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpb
5454
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
5555
github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4=
5656
github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg=
57+
github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
58+
github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
5759
github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc=
5860
github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY=
5961
github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
@@ -120,6 +122,8 @@ github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W
120122
github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw=
121123
github.com/uptrace/opentelemetry-go-extra/otelzap v0.2.4 h1:/4mU8NB88+6u9JVKlkdD6HjrhRM1V1KRTsJaU8FSr8I=
122124
github.com/uptrace/opentelemetry-go-extra/otelzap v0.2.4/go.mod h1:JoL6Kg6zYo9WtK5Y715GWItSUNpWprRYj5wgO01h00g=
125+
gitlab-master.nvidia.com/ncp/vmaas/libs/golang/nv-config v0.1.7 h1:GU2CMTh5HCdPX0bwZFig9oOGtuCn0m+5zWQuICqCmrU=
126+
gitlab-master.nvidia.com/ncp/vmaas/libs/golang/nv-config v0.1.7/go.mod h1:Q0VFXFw0iM1/YM2DHyG7wROkjhCgTHU1wE8kWv/MupI=
123127
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
124128
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
125129
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=

auth-callout/src/internal/config/permissions.go

Lines changed: 23 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ import (
77
"encoding/json"
88
"fmt"
99
"os"
10-
"path/filepath"
1110
"sync/atomic"
11+
"time"
1212

13-
"github.com/fsnotify/fsnotify"
1413
"github.com/nats-io/jwt/v2"
1514
"github.com/uptrace/opentelemetry-go-extra/otelzap"
1615
"go.uber.org/zap"
16+
17+
nvconfig "gitlab-master.nvidia.com/ncp/vmaas/libs/golang/nv-config"
1718
)
1819

20+
const permissionsReloadPollingInterval = 5 * time.Second
21+
1922
// UserProfile contains the account and permissions for a user
2023
type UserProfile struct {
2124
Name string `json:"-"` // Friendly name from config (not serialized)
@@ -66,7 +69,7 @@ type PermissionsManager struct {
6669
mtlsLookup atomic.Pointer[map[string]UserProfile] // identity -> profile
6770
nkeyLookup atomic.Pointer[map[string]UserProfile] // public_key -> profile
6871
noauthProfile atomic.Pointer[UserProfile] // optional no-auth profile
69-
fileWatcher *fsnotify.Watcher
72+
fileWatcher nvconfig.FileWatcher
7073
logger *otelzap.Logger
7174
}
7275

@@ -92,7 +95,22 @@ func NewPermissionsManager(filePath string, logger *otelzap.Logger) (*Permission
9295
return nil, fmt.Errorf("failed to load initial config: %w", err)
9396
}
9497

95-
if err := pm.startWatcher(filePath); err != nil {
98+
fw, err := nvconfig.NewFileWatcher(nvconfig.WithPollingInterval(permissionsReloadPollingInterval))
99+
if err != nil {
100+
return nil, fmt.Errorf("failed to create file watcher: %w", err)
101+
}
102+
pm.fileWatcher = fw
103+
104+
if err := fw.WatchFile(filePath, func(_ string) error {
105+
pm.logger.Info("Config file changed, reloading...")
106+
if err := pm.load(filePath); err != nil {
107+
pm.logger.Error("Error reloading config", zap.Error(err))
108+
return err
109+
}
110+
pm.logger.Info("Config reloaded successfully")
111+
return nil
112+
}); err != nil {
113+
_ = fw.Stop()
96114
return nil, fmt.Errorf("failed to watch config file: %w", err)
97115
}
98116

@@ -198,53 +216,6 @@ func (pm *PermissionsManager) load(filePath string) error {
198216
return nil
199217
}
200218

201-
func (pm *PermissionsManager) startWatcher(filePath string) error {
202-
watcher, err := fsnotify.NewWatcher()
203-
if err != nil {
204-
return fmt.Errorf("failed to create file watcher: %w", err)
205-
}
206-
pm.fileWatcher = watcher
207-
208-
if err := watcher.Add(filepath.Dir(filePath)); err != nil {
209-
_ = watcher.Close()
210-
return fmt.Errorf("failed to watch config directory: %w", err)
211-
}
212-
213-
go func() {
214-
for {
215-
select {
216-
case event, ok := <-watcher.Events:
217-
if !ok {
218-
return
219-
}
220-
if !shouldReload(event, filePath) {
221-
continue
222-
}
223-
pm.logger.Info("Config file changed, reloading...")
224-
if err := pm.load(filePath); err != nil {
225-
pm.logger.Error("Error reloading config", zap.Error(err))
226-
continue
227-
}
228-
pm.logger.Info("Config reloaded successfully")
229-
case err, ok := <-watcher.Errors:
230-
if !ok {
231-
return
232-
}
233-
pm.logger.Error("Config watcher error", zap.Error(err))
234-
}
235-
}
236-
}()
237-
238-
return nil
239-
}
240-
241-
func shouldReload(event fsnotify.Event, filePath string) bool {
242-
if filepath.Clean(event.Name) != filepath.Clean(filePath) {
243-
return false
244-
}
245-
return event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Rename)
246-
}
247-
248219
// GetOAuth2Profile returns the user profile and required scope for OAuth2 claims (subject and azp)
249220
func (pm *PermissionsManager) GetOAuth2Profile(subject, azp string) (UserProfile, string, bool) {
250221
lookup := pm.oauth2Lookup.Load()
@@ -293,8 +264,5 @@ func (pm *PermissionsManager) GetNoAuthProfile() (UserProfile, bool) {
293264

294265
// Close stops the file watcher
295266
func (pm *PermissionsManager) Close() error {
296-
if pm.fileWatcher == nil {
297-
return nil
298-
}
299-
return pm.fileWatcher.Close()
267+
return pm.fileWatcher.Stop()
300268
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package config
5+
6+
import (
7+
"encoding/json"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
"time"
12+
13+
"github.com/stretchr/testify/require"
14+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
15+
"go.uber.org/zap"
16+
)
17+
18+
func TestPermissionsManagerReloadsKubernetesConfigMapVolume(t *testing.T) {
19+
volumeDir := t.TempDir()
20+
permissionsFile := filepath.Join(volumeDir, "permissions.json")
21+
22+
writeKubernetesConfigMapVersion(t, volumeDir, "2026_05_27_00_00_00.000000001", "APP1")
23+
24+
pm, err := NewPermissionsManager(permissionsFile, testLogger())
25+
require.NoError(t, err)
26+
t.Cleanup(func() {
27+
require.NoError(t, pm.Close())
28+
})
29+
30+
requireNoAuthAccount(t, pm, "APP1")
31+
32+
writeKubernetesConfigMapVersion(t, volumeDir, "2026_05_27_00_00_01.000000002", "APP2")
33+
34+
require.Eventually(t, func() bool {
35+
profile, ok := pm.GetNoAuthProfile()
36+
return ok && profile.Account == "APP2"
37+
}, 10*time.Second, 100*time.Millisecond)
38+
}
39+
40+
func testLogger() *otelzap.Logger {
41+
return otelzap.New(zap.NewNop())
42+
}
43+
44+
func writeKubernetesConfigMapVersion(t *testing.T, volumeDir, version, account string) {
45+
t.Helper()
46+
47+
versionName := ".." + version
48+
versionDir := filepath.Join(volumeDir, versionName)
49+
require.NoError(t, os.Mkdir(versionDir, 0o755))
50+
writeNoAuthPermissions(t, filepath.Join(versionDir, "permissions.json"), account)
51+
52+
tmpLink := filepath.Join(volumeDir, "..data_tmp")
53+
require.NoError(t, os.RemoveAll(tmpLink))
54+
require.NoError(t, os.Symlink(versionName, tmpLink))
55+
require.NoError(t, os.Rename(tmpLink, filepath.Join(volumeDir, "..data")))
56+
57+
permissionsLink := filepath.Join(volumeDir, "permissions.json")
58+
if _, err := os.Lstat(permissionsLink); os.IsNotExist(err) {
59+
require.NoError(t, os.Symlink(filepath.Join("..data", "permissions.json"), permissionsLink))
60+
} else {
61+
require.NoError(t, err)
62+
}
63+
}
64+
65+
func writeNoAuthPermissions(t *testing.T, path, account string) {
66+
t.Helper()
67+
68+
data, err := json.Marshal(PermissionsConfig{
69+
NoAuth: &NoAuthEntry{
70+
Account: account,
71+
},
72+
})
73+
require.NoError(t, err)
74+
require.NoError(t, os.WriteFile(path, data, 0o644))
75+
}
76+
77+
func requireNoAuthAccount(t *testing.T, pm *PermissionsManager, account string) {
78+
t.Helper()
79+
80+
profile, ok := pm.GetNoAuthProfile()
81+
require.True(t, ok)
82+
require.Equal(t, account, profile.Account)
83+
}

auth-callout/vendor/github.com/knadh/koanf/providers/confmap/LICENSE

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

auth-callout/vendor/github.com/knadh/koanf/providers/confmap/confmap.go

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

auth-callout/vendor/gitlab-master.nvidia.com/ncp/vmaas/libs/golang/nv-config/.gitignore

Lines changed: 70 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)