Skip to content
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

Export reconciler Validate method #580

Merged
merged 1 commit into from
Jan 30, 2025

Conversation

scothis
Copy link
Member

@scothis scothis commented Jan 28, 2025

Export the Validate() method for Reconcilers and SubReconcilers. Test cases detect this method and call it before the test is run to fail fast with a meaningful error for ambiguous reconciler definitions.

Since the reconciler validation tests can now live in a _test package, they moved to live next to the primary tests for their respective reconciler.

Tests previously using invalid reconcilers have been corrected or removed.

Resolves #578

Export the Validate() method for Reconcilers and SubReconcilers. Test
cases detect this method and call it before the test is run to fail fast
with a meaningful error for ambiguous reconciler definitions.

Since the reconciler validation tests can now live in a _test package,
they moved to live next to the primary tests for their respective
reconciler.

Tests previously using invalid reconcilers have been corrected or
removed.

Signed-off-by: Scott Andrews <[email protected]>
@scothis scothis requested a review from mamachanko January 28, 2025 22:46
Copy link

codecov bot commented Jan 28, 2025

Codecov Report

Attention: Patch coverage is 39.47368% with 23 lines in your changes missing coverage. Please review.

Project coverage is 58.26%. Comparing base (1386bbb) to head (e10baa2).
Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
reconcilers/flow.go 50.00% 5 Missing ⚠️
testing/reconciler.go 0.00% 4 Missing ⚠️
testing/subreconciler.go 0.00% 4 Missing ⚠️
reconcilers/advice.go 50.00% 1 Missing ⚠️
reconcilers/aggregate.go 50.00% 1 Missing ⚠️
reconcilers/cast.go 50.00% 1 Missing ⚠️
reconcilers/child.go 50.00% 1 Missing ⚠️
reconcilers/childset.go 50.00% 1 Missing ⚠️
reconcilers/config.go 50.00% 1 Missing ⚠️
reconcilers/finalizer.go 50.00% 1 Missing ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #580      +/-   ##
==========================================
- Coverage   58.33%   58.26%   -0.08%     
==========================================
  Files          33       33              
  Lines        3795     3800       +5     
==========================================
  Hits         2214     2214              
- Misses       1487     1492       +5     
  Partials       94       94              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment on lines +35 to +38
type Validator interface {
Validate(ctx context.Context) error
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[thought]:

I had tested your these changes with my kit, dropping the field ChildObjectManager from one of the reconcilers and expecting an error message. However, it still produced the panic described by #578.

head scratching...

Then I realised that I am working with a higher-order reconciler, i.e. Advice, which delegates to a ChildReconciler. Right now higher-order reconcilers only validate themselves, and not their inner reconcilers. That means the inner validation errors are masked. In fact, we should not even see them at runtime, but panics instead.

Therefore, I think we should export the Validator interface from the reconcilers package instead. That would allow higher-order reconcilers to sniff their delegate reconcilers for Validate and fail fast in case of invalidity.

For example like this with Advice only:

diff --git a/reconcilers/advice.go b/reconcilers/advice.go
index fb083f6..12d223d 100644
--- a/reconcilers/advice.go
+++ b/reconcilers/advice.go
@@ -126,7 +126,11 @@ func (r *Advice[T]) Validate(ctx context.Context) error {
 	if r.Before == nil && r.Around == nil && r.After == nil {
 		return fmt.Errorf("Advice %q must implement at least one of Before, Around or After", r.Name)
 	}
-
+	if v, ok := r.Reconciler.(Validator); ok {
+		if err := v.Validate(ctx); err != nil {
+			return fmt.Errorf("Advice %q has an invalid reconciler: %s", r.Name, err)
+		}
+	}
 	return nil
 }
 
diff --git a/reconcilers/reconcilers.go b/reconcilers/reconcilers.go
index c3fba65..59b38ae 100644
--- a/reconcilers/reconcilers.go
+++ b/reconcilers/reconcilers.go
@@ -39,6 +39,10 @@ type SubReconciler[Type client.Object] interface {
 	Reconcile(ctx context.Context, resource Type) (Result, error)
 }
 
+type Validator interface {
+	Validate(ctx context.Context) error
+}
+
 var (
 	// ErrQuiet are not logged or recorded as events within reconciler.io/runtime.
 	// They are propagated as errors unless otherwise defined.
diff --git a/testing/reconciler.go b/testing/reconciler.go
index e51ad77..0132b85 100644
--- a/testing/reconciler.go
+++ b/testing/reconciler.go
@@ -32,10 +32,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
-type Validator interface {
-	Validate(ctx context.Context) error
-}
-
 // ReconcilerTestCase holds a single test case of a reconciler test suite.
 type ReconcilerTestCase struct {
 	// Name is a descriptive name for this test suitable as a first argument to t.Run()
@@ -212,7 +208,7 @@ func (tc *ReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, factory
 	ctx = reconcilers.StashAdditionalConfigs(ctx, configs)
 
 	r := factory(t, tc, expectConfig.Config())
-	if v, ok := r.(Validator); ok {
+	if v, ok := r.(reconcilers.Validator); ok {
 		if err := v.Validate(ctx); err != nil {
 			t.Fatalf("reconciler validation failed: %s", err)
 		}
diff --git a/testing/subreconciler.go b/testing/subreconciler.go
index ecd1009..abc3dcb 100644
--- a/testing/subreconciler.go
+++ b/testing/subreconciler.go
@@ -228,7 +228,7 @@ func (tc *SubReconcilerTestCase[T]) Run(t *testing.T, scheme *runtime.Scheme, fa
 	c := expectConfig.Config()
 
 	r := factory(t, tc, c)
-	if v, ok := r.(Validator); ok {
+	if v, ok := r.(reconcilers.Validator); ok {
 		if err := v.Validate(ctx); err != nil {
 			t.Fatalf("subreconciler validation failed: %s", err)
 		}

We would have to extend the same to all higher-order reconcilers

❯ rg "\tReconciler SubReconciler\[T" -l
reconcilers/webhook.go
reconcilers/config.go
reconcilers/advice.go
reconcilers/finalizer.go
reconcilers/aggregate.go
reconcilers/flow.go
reconcilers/resource.go

I'd be happy to follow-up with a PR, if you prefer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch regarding nested validation. I'd like to avoid calling into nested validators directory within the validate method because the reconciler name may not be set, which will make it harder to identify where the validation error is actually coming from. For the non-test scenario it would cause the validation to be called n+1 times, where n is depth of the sub reconciler.

Perhaps we can set a value in the context to signal the validation should be recursive and turn it on only for tests. If we want to get fancy with the context, we could also pass a validation error handler via the context and use it to t.Fatal for tests and panic for deployed reconcilers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! I like the idea about controlling behaviour via the context and possibly propagating a validation error handler within it. I have recorded #583 and will work on it.

Copy link
Contributor

@mamachanko mamachanko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥳

Comment on lines +35 to +38
type Validator interface {
Validate(ctx context.Context) error
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! I like the idea about controlling behaviour via the context and possibly propagating a validation error handler within it. I have recorded #583 and will work on it.

@scothis scothis merged commit 8872d52 into reconcilerio:main Jan 30, 2025
2 of 4 checks passed
@scothis scothis deleted the reconciler-validation branch January 30, 2025 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Validate reconcilers at test-time w/ helpful error messages
2 participants