-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
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 { | ||
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. Nit: Is there a reason to define this method? 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. it will allow using
There is a test in |
||
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 |
---|---|---|
|
@@ -6609,6 +6609,61 @@ 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. |
||
t.Parallel() | ||
|
||
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" | ||
|
||
plugin := New(&Config{ | ||
Bundles: map[string]*Source{ | ||
"test": { | ||
Service: "default", | ||
SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), | ||
Config: download.Config{Trigger: &manual}, | ||
}, | ||
}, | ||
}, 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 != "test" { | ||
t.Fatalf("expected error for bundle 'test' but got '%s'", e.BundleName) | ||
} | ||
} | ||
} else { | ||
t.Fatalf("expected type of error to be %s but got %s", reflect.TypeOf(bundleErrors), reflect.TypeOf(err)) | ||
} | ||
} | ||
|
||
// Warning: This test modifies package variables, and as | ||
// a result, cannot be run in parallel with other tests. | ||
func TestGetNormalizedBundleName(t *testing.T) { | ||
|
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.
I'm trying to understand what we're testing here?
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.
the existing test is verifying that the errors are present in the status,
my goal was to also verify that these errors are also returned directly from the trigger method
handing over the expected error as a parameter became necessary due to a detected race condition as the goroutine will otherwise work on
tc.expErrs
which is changed concurrently in the outer loop (l. 294)