-
Notifications
You must be signed in to change notification settings - Fork 203
extproc: load default config if file does not exist #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,45 +65,47 @@ func (cw *configWatcher) watch(ctx context.Context, tick time.Duration) { | |
| // loadConfig loads a new config from the given path and updates the Receiver by | ||
| // calling the [Receiver.Load]. | ||
| func (cw *configWatcher) loadConfig(ctx context.Context) error { | ||
| var ( | ||
| cfg *filterapi.Config | ||
| raw []byte | ||
| ) | ||
|
|
||
| stat, err := os.Stat(cw.path) | ||
| if err != nil { | ||
| switch { | ||
| case err != nil && os.IsNotExist(err): | ||
| // If the file does not exist, do not fail (which could lead to the extproc process to terminate) | ||
| // Instead, load the default configuration and keep running unconfigured | ||
| cfg, raw = filterapi.MustLoadDefaultConfig() | ||
| case err != nil: | ||
| return err | ||
| } | ||
| if stat.ModTime().Sub(cw.lastMod) <= 0 { | ||
| return nil | ||
|
|
||
| if cfg != nil { | ||
| cw.l.Info("config file does not exist; loading default config", slog.String("path", cw.path)) | ||
| cw.lastMod = time.Now() | ||
| } else { | ||
| cw.l.Info("loading a new config", slog.String("path", cw.path)) | ||
| if stat.ModTime().Sub(cw.lastMod) <= 0 { | ||
| return nil | ||
| } | ||
| cw.lastMod = stat.ModTime() | ||
| cfg, raw, err = filterapi.UnmarshalConfigYaml(cw.path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| cw.lastMod = stat.ModTime() | ||
| cw.l.Info("loading a new config", slog.String("path", cw.path)) | ||
|
|
||
| // Print the diff between the old and new config. | ||
| if cw.l.Enabled(ctx, slog.LevelDebug) { | ||
| // Re-hydrate the current config file for later diffing. | ||
| previous := cw.current | ||
| cw.current, err = cw.getConfigString() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to read the config file: %w", err) | ||
| } | ||
|
|
||
| cw.current = string(raw) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed the unmarshal method to return the raw contents as well so we don't have to read the file again when logging the diff. |
||
| cw.diff(previous, cw.current) | ||
| } | ||
|
|
||
| cfg, err := filterapi.UnmarshalConfigYaml(cw.path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return cw.rcv.LoadConfig(ctx, cfg) | ||
| } | ||
|
|
||
| // getConfigString gets a string representation of the current config | ||
| // read from the path. This is only used for debug log path for diff prints. | ||
| func (cw *configWatcher) getConfigString() (string, error) { | ||
| currentByte, err := os.ReadFile(cw.path) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return string(currentByte), nil | ||
| } | ||
|
|
||
| func (cw *configWatcher) diff(oldConfig, newConfig string) { | ||
| if oldConfig == "" { | ||
| return | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,13 +8,15 @@ package extproc | |
| import ( | ||
| "bytes" | ||
| "context" | ||
| "io" | ||
| "log/slog" | ||
| "os" | ||
| "strings" | ||
| "sync" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/envoyproxy/ai-gateway/filterapi" | ||
|
|
@@ -40,9 +42,30 @@ func (m *mockReceiver) getConfig() *filterapi.Config { | |
| return m.cfg | ||
| } | ||
|
|
||
| var _ io.Writer = (*syncBuffer)(nil) | ||
|
|
||
| // syncBuffer is a bytes.Buffer that is safe for concurrent read/write access. | ||
| // used just in the tests to safely read the logs in assertions without data races. | ||
| type syncBuffer struct { | ||
| mu sync.RWMutex | ||
| b *bytes.Buffer | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why this didn't fail before, but when running the unit tests I got data races in the buffer, between the message logs and the call to |
||
|
|
||
| func (s *syncBuffer) Write(p []byte) (n int, err error) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| return s.b.Write(p) | ||
| } | ||
|
|
||
| func (s *syncBuffer) String() string { | ||
| s.mu.RLock() | ||
| defer s.mu.RUnlock() | ||
| return s.b.String() | ||
| } | ||
|
|
||
| // newTestLoggerWithBuffer creates a new logger with a buffer for testing and asserting the output. | ||
| func newTestLoggerWithBuffer() (*slog.Logger, *bytes.Buffer) { | ||
| buf := &bytes.Buffer{} | ||
| func newTestLoggerWithBuffer() (*slog.Logger, *syncBuffer) { | ||
| buf := &syncBuffer{b: &bytes.Buffer{}} | ||
| logger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{ | ||
| Level: slog.LevelDebug, | ||
| })) | ||
|
|
@@ -54,7 +77,22 @@ func TestStartConfigWatcher(t *testing.T) { | |
| path := tmpdir + "/config.yaml" | ||
| rcv := &mockReceiver{} | ||
|
|
||
| require.NoError(t, os.WriteFile(path, []byte{}, 0o600)) | ||
| logger, buf := newTestLoggerWithBuffer() | ||
| err := StartConfigWatcher(t.Context(), path, rcv, logger, time.Millisecond*100) | ||
| require.NoError(t, err) | ||
|
|
||
| defaultCfg, _ := filterapi.MustLoadDefaultConfig() | ||
| require.NoError(t, err) | ||
|
|
||
| // Verify the default config has been loaded. | ||
| require.EventuallyWithT(t, func(c *assert.CollectT) { | ||
| assert.Equal(c, defaultCfg, rcv.getConfig()) | ||
| }, 1*time.Second, 100*time.Millisecond) | ||
|
|
||
| // Verify the buffer contains the default config loading. | ||
| require.Eventually(t, func() bool { | ||
| return strings.Contains(buf.String(), "config file does not exist; loading default config") | ||
| }, 1*time.Second, 100*time.Millisecond, buf.String()) | ||
|
|
||
| // Create the initial config file. | ||
| cfg := ` | ||
|
|
@@ -84,15 +122,10 @@ rules: | |
| value: gpt4.4444 | ||
| ` | ||
| require.NoError(t, os.WriteFile(path, []byte(cfg), 0o600)) | ||
| ctx, cancel := context.WithCancel(t.Context()) | ||
| defer cancel() | ||
| logger, buf := newTestLoggerWithBuffer() | ||
| err := StartConfigWatcher(ctx, path, rcv, logger, time.Millisecond*100) | ||
| require.NoError(t, err) | ||
|
|
||
| // Initial loading should have happened. | ||
| require.Eventually(t, func() bool { | ||
| return rcv.getConfig() != nil | ||
| return rcv.getConfig() != defaultCfg | ||
| }, 1*time.Second, 100*time.Millisecond) | ||
| firstCfg := rcv.getConfig() | ||
| require.NotNil(t, firstCfg) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to clear
err = nilnow? not exist will end up captured in if err ! = nil belowThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes. when this method returned an error it made sense; not anymore. fixed