authz: add onPolicyUpdate callback to authz file watcher#9142
Conversation
|
|
bc68034 to
183e653
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #9142 +/- ##
==========================================
- Coverage 83.27% 83.23% -0.04%
==========================================
Files 420 420
Lines 34022 34026 +4
==========================================
- Hits 28331 28323 -8
- Misses 4262 4271 +9
- Partials 1429 1432 +3
🚀 New features to boost your workflow:
|
4381232 to
056e67a
Compare
| close(updates) | ||
| if len(updates) != 0 { | ||
| t.Fatalf("expected exactly 2 updates in channel") | ||
| } |
There was a problem hiding this comment.
I don't quite understand what this block of code is for.
There was a problem hiding this comment.
The intent was to catch if the code calls the callback >1x per file modification.
There was a problem hiding this comment.
Please replace this block of code with the following
If we make updates to be a channel of size 1 as I recommended above, you could instead do the following here:
sCtx, sCancel := context.WithTimeout(ctx, 100 * time.Millisecond)
defer sCancel()
select {
case <-updates:
t.Fatal("OnPolicyUpdate callback invoked more times than expected")
case <-sCtx.Done():
}The above approach has the added benefit of waiting for a short amount of time to ensure that no additional callback invocations happen.
dce75de to
a7534e0
Compare
|
@hnefatl : Please don't mark review comment threads as resolved. It is the responsibility of the reviewer to do that. If the author marks them as "resolved", the reviewer would have to "unresolve" each of those to verify if the comments were addressed satisfactorily. Thanks for understanding. |
easwars
left a comment
There was a problem hiding this comment.
LGTM, modulo minor nits.
| // options. | ||
| func NewFileWatcherWithOptions(options FileWatcherOptions) (*FileWatcherInterceptor, error) { | ||
| if options.PolicyFile == "" { | ||
| return nil, fmt.Errorf("authorization policy file path is empty") |
There was a problem hiding this comment.
Nit: While you are here, could you please add an "authz: " prefix to all error returned by this package from its exported functions? I can only see three of them now.
There was a problem hiding this comment.
Done+updated tests in a separate commit (iiuc they'll be merged into a single commit on main? but helps keep things distinct during review).
I kept the existing "exact error matching" rather than switching to e.g. regex matching errors because it was less disruptive, but lmk if you'd prefer switching to a fuzzier match.
|
|
||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatalf("timeout waiting for policy update") |
There was a problem hiding this comment.
Nit: s/timeout/Timeout
The non-capitalization of error messages does not apply to test error strings and logs. See: https://google.github.io/styleguide/go/decisions#error-strings
Here and elsewhere in this test. Thanks.
| ctx, cancel := context.WithTimeout(t.Context(), defaultTestTimeout) | ||
| defer cancel() | ||
|
|
||
| updates := make(chan string, 10) |
There was a problem hiding this comment.
Please make this a channel of size 1.
| close(updates) | ||
| if len(updates) != 0 { | ||
| t.Fatalf("expected exactly 2 updates in channel") | ||
| } |
There was a problem hiding this comment.
Please replace this block of code with the following
If we make updates to be a channel of size 1 as I recommended above, you could instead do the following here:
sCtx, sCancel := context.WithTimeout(ctx, 100 * time.Millisecond)
defer sCancel()
select {
case <-updates:
t.Fatal("OnPolicyUpdate callback invoked more times than expected")
case <-sCtx.Done():
}The above approach has the added benefit of waiting for a short amount of time to ensure that no additional callback invocations happen.
4d43a3a to
191ecae
Compare
easwars
left a comment
There was a problem hiding this comment.
Apologies for the delay in the review.
LGTM, modulo minor nits
| } | ||
|
|
||
| // FileWatcherOptions contains configuration options for the | ||
| // FileWatcherInterceptor. |
There was a problem hiding this comment.
Can we please make this API experimental. We do this by adding the following block of docstring here:
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.| // NewFileWatcherWithOptions returns a new FileWatcherInterceptor from a set of | ||
| // options. | ||
| func NewFileWatcherWithOptions(options FileWatcherOptions) (*FileWatcherInterceptor, error) { |
There was a problem hiding this comment.
Same with this one. Please make it experimental.
| file := createTmpPolicyFile(t, "onpolicyupdate", []byte(content)) | ||
| i, err := authz.NewFileWatcherWithOptions(authz.FileWatcherOptions{PolicyFile: file, RefreshDuration: time.Millisecond, OnPolicyUpdate: onPolicyUpdate}) | ||
| if err != nil { | ||
| t.Fatalf("NewFileWatcherWithCallback() returned err: %v", err) |
There was a problem hiding this comment.
Nit: s/NewFileWatcherWithCallback/NewFileWatcherWithOptions
|
|
||
| content := `{"name": "foo1", "allow_rules":[{"name":"bar"}]}` | ||
| file := createTmpPolicyFile(t, "onpolicyupdate", []byte(content)) | ||
| i, err := authz.NewFileWatcherWithOptions(authz.FileWatcherOptions{PolicyFile: file, RefreshDuration: time.Millisecond, OnPolicyUpdate: onPolicyUpdate}) |
There was a problem hiding this comment.
Nit: Please make a local var opts to hold the options and pass it to authz.NewFileWatcherWithOptions. That way, the options can be printed in the following t.Fatalf. See: https://google.github.io/styleguide/go/decisions#identify-the-input
|
|
||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatalf("timeout waiting for policy update") |
|
/gemini review |
|
Moving to @mbissa for second set of eyes. I had some minor nits in my last pass, but looks mostly good to me. |
There was a problem hiding this comment.
Code Review
This pull request prefixes authorization error messages with "authz: " and introduces FileWatcherOptions to support an OnPolicyUpdate callback when the authorization policy is updated. Feedback on these changes includes replacing t.Context() with context.Background() in tests to maintain compatibility with older Go versions, documenting that the OnPolicyUpdate callback runs synchronously to prevent blocking the background refresh goroutine, and increasing the test's refresh duration from 1ms to 20ms to avoid high CPU usage and test flakiness.
| } | ||
|
|
||
| func (s) TestOnPolicyUpdate(t *testing.T) { | ||
| ctx, cancel := context.WithTimeout(t.Context(), defaultTestTimeout) |
There was a problem hiding this comment.
Using t.Context() requires Go 1.24 or later. Since grpc-go maintains compatibility with older Go versions (such as Go 1.22 and 1.23), using t.Context() will break compilation on those versions.
Please use context.Background() instead to ensure backward compatibility.
| ctx, cancel := context.WithTimeout(t.Context(), defaultTestTimeout) | |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| // OnPolicyUpdate is a callback to be invoked when a policy is | ||
| // loaded/updated. The loaded policy string is passed as an argument. | ||
| OnPolicyUpdate func(string) |
There was a problem hiding this comment.
Since this callback is executed synchronously within the file watcher's background goroutine, any blocking or slow operations inside OnPolicyUpdate will delay subsequent policy refreshes.
It is highly recommended to document this behavior so users know they should keep the callback fast and non-blocking (or offload heavy work to a separate goroutine).
// OnPolicyUpdate is a callback to be invoked when a policy is
// loaded/updated. The loaded policy string is passed as an argument.
//
// Note: This callback is executed synchronously on the file watcher's
// background goroutine. To avoid blocking or delaying subsequent policy
// refreshes, the callback should be non-blocking and fast. If any
// slow or blocking operations are needed, they should be run in a
// separate goroutine.
OnPolicyUpdate func(string)|
|
||
| content := `{"name": "foo1", "allow_rules":[{"name":"bar"}]}` | ||
| file := createTmpPolicyFile(t, "onpolicyupdate", []byte(content)) | ||
| i, err := authz.NewFileWatcherWithOptions(authz.FileWatcherOptions{PolicyFile: file, RefreshDuration: time.Millisecond, OnPolicyUpdate: onPolicyUpdate}) |
There was a problem hiding this comment.
Using a RefreshDuration of time.Millisecond (1ms) is extremely aggressive and causes the background goroutine to poll the file 1000 times per second. This can lead to high CPU usage and test flakiness, especially on resource-constrained CI environments.
Consider increasing this to a more reasonable value like 20 * time.Millisecond.
| i, err := authz.NewFileWatcherWithOptions(authz.FileWatcherOptions{PolicyFile: file, RefreshDuration: time.Millisecond, OnPolicyUpdate: onPolicyUpdate}) | |
| i, err := authz.NewFileWatcherWithOptions(authz.FileWatcherOptions{PolicyFile: file, RefreshDuration: 20 * time.Millisecond, OnPolicyUpdate: onPolicyUpdate}) |
Signed-off-by: Keith Collister <kcollister@google.com>
|
Addressed comments from Easwar+Gemini. |
Small additional capability, so user code can tell when a new policy has been loaded.
Our current usecase is updating an opentelemetry metric to reflect the policy version (~= file mtime). We could just run a goroutine polling the file separately, but then there's no guarantee that the authz policy has actually been loaded - it could have failed parsing, or the process could be CPU-starved, etc.
RELEASE NOTES: