-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
enable direct error handling for bundle plugin trigger method #7143
base: main
Are you sure you want to change the base?
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 |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package bundle | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/open-policy-agent/opa/download" | ||
) | ||
|
||
// Errors represents a list of errors that occurred during a bundle load enriched by the bundle name. | ||
type Errors []Error | ||
|
||
func (e Errors) Unwrap() []error { | ||
output := make([]error, len(e)) | ||
for i := range e { | ||
output[i] = e[i] | ||
} | ||
return output | ||
} | ||
func (e Errors) Error() string { | ||
err := errors.Join(e.Unwrap()...) | ||
return err.Error() | ||
} | ||
|
||
type Error struct { | ||
BundleName string | ||
Code string | ||
HTTPCode int | ||
Message string | ||
Err error | ||
} | ||
|
||
func NewBundleError(bundleName string, cause error) Error { | ||
var ( | ||
httpError download.HTTPError | ||
) | ||
switch { | ||
case cause == nil: | ||
return Error{BundleName: bundleName, Code: "", HTTPCode: -1, Message: "", Err: nil} | ||
case errors.As(cause, &httpError): | ||
return Error{BundleName: bundleName, Code: errCode, HTTPCode: httpError.StatusCode, Message: httpError.Error(), Err: cause} | ||
default: | ||
return Error{BundleName: bundleName, Code: errCode, HTTPCode: -1, Message: cause.Error(), Err: cause} | ||
} | ||
} | ||
|
||
func (e Error) Error() string { | ||
return fmt.Sprintf("Bundle name: %s, Code: %s, HTTPCode: %d, Message: %s", e.BundleName, errCode, e.HTTPCode, e.Message) | ||
} | ||
|
||
func (e Error) Unwrap() error { | ||
return e.Err | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package bundle | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/open-policy-agent/opa/ast" | ||
"github.com/open-policy-agent/opa/download" | ||
) | ||
|
||
func TestErrors(t *testing.T) { | ||
errs := Errors{ | ||
NewBundleError("foo", fmt.Errorf("foo error")), | ||
NewBundleError("bar", fmt.Errorf("bar error")), | ||
} | ||
|
||
expected := "Bundle name: foo, Code: bundle_error, HTTPCode: -1, Message: foo error\nBundle name: bar, Code: bundle_error, HTTPCode: -1, Message: bar error" | ||
result := errs.Error() | ||
|
||
if result != expected { | ||
t.Errorf("Expected: %v \nbut got: %v", expected, result) | ||
} | ||
} | ||
|
||
func TestUnwrapSlice(t *testing.T) { | ||
fooErr := NewBundleError("foo", fmt.Errorf("foo error")) | ||
barErr := NewBundleError("bar", fmt.Errorf("bar error")) | ||
|
||
errs := Errors{fooErr, barErr} | ||
|
||
result := errs.Unwrap() | ||
|
||
if result[0].Error() != fooErr.Error() { | ||
t.Fatalf("expected %v \nbut got: %v", fooErr, result[0]) | ||
} | ||
if result[1].Error() != barErr.Error() { | ||
t.Fatalf("expected %v \nbut got: %v", barErr, result[1]) | ||
} | ||
} | ||
|
||
func TestUnwrap(t *testing.T) { | ||
serverHTTPError := NewBundleError("server", download.HTTPError{StatusCode: 500}) | ||
clientHTTPError := NewBundleError("client", download.HTTPError{StatusCode: 400}) | ||
astErrors := ast.Errors{ast.NewError(ast.ParseErr, ast.NewLocation(nil, "foo.rego", 100, 2), "blarg")} | ||
|
||
errs := Errors{serverHTTPError, clientHTTPError, NewBundleError("ast", astErrors)} | ||
|
||
// unwrap first bundle.Error | ||
var bundleError Error | ||
if !errors.As(errs, &bundleError) { | ||
t.Fatal("failed to unwrap Error") | ||
} | ||
if bundleError.Error() != serverHTTPError.Error() { | ||
t.Fatalf("expected: %v \ngot: %v", serverHTTPError, bundleError) | ||
} | ||
|
||
// unwrap first HTTPError | ||
var httpError download.HTTPError | ||
if !errors.As(errs, &httpError) { | ||
t.Fatal("failed to unwrap Error") | ||
} | ||
if httpError.Error() != serverHTTPError.Err.Error() { | ||
t.Fatalf("expected: %v \ngot: %v", serverHTTPError.Err, httpError) | ||
} | ||
|
||
// unwrap HTTPError from bundle.Error | ||
if !errors.As(bundleError, &httpError) { | ||
t.Fatal("failed to unwrap HTTPError") | ||
} | ||
if httpError.Error() != serverHTTPError.Err.Error() { | ||
t.Fatalf("expected: %v \nbgot: %v", serverHTTPError.Err, httpError) | ||
} | ||
|
||
var unwrappedAstErrors ast.Errors | ||
if !errors.As(errs, &unwrappedAstErrors) { | ||
t.Fatal("failed to unwrap ast.Errors") | ||
} | ||
if unwrappedAstErrors.Error() != astErrors.Error() { | ||
t.Fatalf("expected: %v \ngot: %v", astErrors, unwrappedAstErrors) | ||
} | ||
} | ||
|
||
func TestHTTPErrorWrapping(t *testing.T) { | ||
err := download.HTTPError{StatusCode: 500} | ||
bundleErr := NewBundleError("foo", err) | ||
|
||
if bundleErr.BundleName != "foo" { | ||
t.Fatalf("BundleName: expected: %v \ngot: %v", "foo", bundleErr.BundleName) | ||
} | ||
if bundleErr.HTTPCode != err.StatusCode { | ||
t.Fatalf("HTTPCode: expected: %v \ngot: %v", err.StatusCode, bundleErr.HTTPCode) | ||
} | ||
if bundleErr.Message != err.Error() { | ||
t.Fatalf("Message: expected: %v \ngot: %v", err.Error(), bundleErr.Message) | ||
} | ||
if bundleErr.Code != errCode { | ||
t.Fatalf("Code: expected: %v \ngot: %v", errCode, bundleErr.Code) | ||
} | ||
if bundleErr.Err != err { | ||
t.Fatalf("Err: expected: %v \ngot: %v", err, bundleErr.Err) | ||
} | ||
} | ||
|
||
func TestASTErrorsWrapping(t *testing.T) { | ||
err := ast.Errors{ast.NewError(ast.ParseErr, ast.NewLocation(nil, "foo.rego", 100, 2), "blarg")} | ||
bundleErr := NewBundleError("foo", err) | ||
|
||
if bundleErr.BundleName != "foo" { | ||
t.Fatalf("BundleName: expected: %v \ngot: %v", "foo", bundleErr.BundleName) | ||
} | ||
if bundleErr.HTTPCode != -1 { | ||
t.Fatalf("HTTPCode: expected: %v \ngot: %v", -1, bundleErr.HTTPCode) | ||
} | ||
if bundleErr.Message != err.Error() { | ||
t.Fatalf("Message: expected: %v \ngot: %v", err.Error(), bundleErr.Message) | ||
} | ||
if bundleErr.Code != errCode { | ||
t.Fatalf("Code: expected: %v \ngot: %v", errCode, bundleErr.Code) | ||
} | ||
if bundleErr.Err.Error() != err.Error() { | ||
t.Fatalf("Err: expected: %v \ngot: %v", err.Error(), bundleErr.Err.Error()) | ||
} | ||
} | ||
|
||
func TestGenericErrorWrapping(t *testing.T) { | ||
err := fmt.Errorf("foo error") | ||
bundleErr := NewBundleError("foo", err) | ||
|
||
if bundleErr.BundleName != "foo" { | ||
t.Fatalf("BundleName: expected: %v \ngot: %v", "foo", bundleErr.BundleName) | ||
} | ||
if bundleErr.HTTPCode != -1 { | ||
t.Fatalf("HTTPCode: expected: %v \ngot: %v", -1, bundleErr.HTTPCode) | ||
} | ||
if bundleErr.Message != err.Error() { | ||
t.Fatalf("Message: expected: %v \ngot: %v", err.Error(), bundleErr.Message) | ||
} | ||
if bundleErr.Code != errCode { | ||
t.Fatalf("Code: expected: %v \ngot: %v", errCode, bundleErr.Code) | ||
} | ||
if bundleErr.Err.Error() != err.Error() { | ||
t.Fatalf("Err: expected: %v \ngot: %v", err.Error(), bundleErr.Err.Error()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -257,16 +257,25 @@ func (p *Plugin) Loaders() map[string]Loader { | |
|
||
// Trigger triggers a bundle download on all configured bundles. | ||
func (p *Plugin) Trigger(ctx context.Context) error { | ||
var errs Errors | ||
|
||
p.mtx.Lock() | ||
downloaders := map[string]Loader{} | ||
for name, dl := range p.downloaders { | ||
downloaders[name] = dl | ||
} | ||
p.mtx.Unlock() | ||
|
||
for _, d := range downloaders { | ||
// plugin callback will log the trigger error and include it in the bundle status | ||
_ = d.Trigger(ctx) | ||
for name, d := range downloaders { | ||
// plugin callback will also log the trigger error and include it in the bundle status | ||
err := d.Trigger(ctx) | ||
// only return errors for TriggerMode manual as periodic bundles will be retried | ||
if err != nil && *p.config.Bundles[name].Trigger == plugins.TriggerManual { | ||
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. Use the 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. @ashutosh-narkar do you think it makes sense to only return the errors for bundles in 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. Manual trigger mode can currently only be configured via a library integration. So to keep parity with the discovery plugin, returning errors in the manual mode makes sense. |
||
errs = append(errs, NewBundleError(name, err)) | ||
} | ||
} | ||
if len(errs) > 0 { | ||
return errs | ||
} | ||
return nil | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6522,6 +6522,68 @@ func TestPluginManualTriggerWithTimeout(t *testing.T) { | |
} | ||
} | ||
|
||
func TestPluginManualTriggerWithServerError(t *testing.T) { | ||
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. This test triggers the race condition.
This would also solve the question if Trigger should return errors for bundles with "periodic" trigger mode. 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. @ashutosh-narkar 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 think having only a manual trigger for the test should be fine. |
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | ||
defer cancel() | ||
|
||
s := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { | ||
resp.WriteHeader(500) | ||
})) | ||
|
||
// setup plugin pointing at fake server | ||
manager := getTestManagerWithOpts([]byte(fmt.Sprintf(`{ | ||
"services": { | ||
"default": { | ||
"url": %q | ||
} | ||
} | ||
}`, s.URL))) | ||
|
||
var manual plugins.TriggerMode = "manual" | ||
var periodic plugins.TriggerMode = "periodic" | ||
var delay int64 = 10 | ||
polling := download.PollingConfig{MinDelaySeconds: &delay, MaxDelaySeconds: &delay} | ||
|
||
plugin := New(&Config{ | ||
Bundles: map[string]*Source{ | ||
"manual": { | ||
Service: "default", | ||
SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), | ||
Config: download.Config{Trigger: &manual}, | ||
}, | ||
"periodic": { | ||
Service: "default", | ||
SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), | ||
Config: download.Config{Trigger: &periodic, Polling: polling}, | ||
}, | ||
}, | ||
}, manager) | ||
|
||
err := plugin.Start(ctx) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
// manually trigger bundle download | ||
err = plugin.Trigger(ctx) | ||
|
||
plugin.Stop(ctx) | ||
|
||
var bundleErrors Errors | ||
if errors.As(err, &bundleErrors) { | ||
if len(bundleErrors) != 1 { | ||
t.Fatalf("expected exactly one error, got %d", len(bundleErrors)) | ||
} | ||
for _, e := range bundleErrors { | ||
if e.BundleName != "manual" { | ||
t.Fatalf("expect error only for bundle with manual trigger mode") | ||
} | ||
} | ||
} else { | ||
t.Fatalf("expected type of error to be %s but got %s", reflect.TypeOf(bundleErrors), reflect.TypeOf(err)) | ||
} | ||
} | ||
|
||
func TestGetNormalizedBundleName(t *testing.T) { | ||
cases := []struct { | ||
input string | ||
|
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.
Nit: Is there a reason to define this method?
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.
it will allow using
errors.As
for retrieving the wrapped error. Something like this:There is a test in
errors_test.go
(TestUnwrap) that will start to fail if we remove the function.