-
Notifications
You must be signed in to change notification settings - Fork 68
✨ Add Bundle-Agnostic Configuration Validation Using JSON Schema #2316
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
base: main
Are you sure you want to change the base?
✨ Add Bundle-Agnostic Configuration Validation Using JSON Schema #2316
Conversation
✅ Deploy Preview for olmv1 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
c547c6d to
05d349b
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2316 +/- ##
==========================================
+ Coverage 74.30% 74.31% +0.01%
==========================================
Files 91 92 +1
Lines 7083 7227 +144
==========================================
+ Hits 5263 5371 +108
- Misses 1405 1429 +24
- Partials 415 427 +12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
a753a5c to
f15ea9f
Compare
f15ea9f to
e9352a6
Compare
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
e9352a6 to
c2e887e
Compare
1f3451a to
65bd4ef
Compare
65bd4ef to
195d51d
Compare
d78bf4a to
920f78e
Compare
920f78e to
aaa71d0
Compare
8ac9e61 to
df17573
Compare
df17573 to
33b1369
Compare
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.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
33b1369 to
5dfd324
Compare
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.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // UnmarshalConfig, which does the validation. | ||
| type Config struct { | ||
| WatchNamespace *string `json:"watchNamespace"` | ||
| data map[string]any |
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.
@perdasilva we are removing to make more Generic
So, I think is important we move with it , if we want change it, before we promote Config API ( consequentely single/own ns )
| return nil | ||
| } | ||
| // Extract string value | ||
| if str, ok := val.(string); ok { |
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.
you can drop now lines 100-102, because it is covered by line 103.
| // | ||
| // If the user doesn't provide any configuration but the bundle requires some fields | ||
| // (like watchNamespace), validation will fail with a helpful error. | ||
| func UnmarshalConfig(bytes []byte, schema map[string]any, installNamespace string) (*Config, error) { |
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 function unmarshal + validatates optionally the config. I would split these into two functions at least so that we can achieve something like:
c := NewConfigBuilder.FromBytes(bytes).WithSchema(schema).WithInstallNamespace(installNamespace).Build()
err := v.Validate()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 like the Builder approach, but I think we should add it only IF/when we start to have many args.
I asked Claude AI to evaluate the current approach vs Builder Pattern. Here's the summary:
Claude AI Analysis Summary
Current Approach
config, err := bundle.UnmarshalConfig(bytes, schema, installNamespace)Design principle: Validation happens at construction. If you have a Config object, it's already valid.
Builder Pattern Option 1: Validation in Build()
config, err := bundle.NewConfigBuilder().
FromBytes(bytes).
WithSchema(schema).
WithInstallNamespace(ns).
Build() // validation happens hereResult: Functionally identical to current approach, just more verbose (1 function call → 5 method calls).
Builder Pattern Option 2: Separate Validate()
config := builder.Build() // unvalidated
err := config.Validate() // validate separatelyProblems:
- Allows invalid Config objects to exist
- Developers might forget to call Validate()
- Would need to run validation twice (inefficient)
- Breaks safety guarantee
When Builder Pattern Makes Sense
✅ Good for:
- 7-10+ parameters
- Many optional parameters
- Parameters can be set independently
- Multiple valid construction paths
❌ Our situation:
- 3 required parameters
- All interdependent
- One construction path
Maintainability Comparison
Current: 1 function to maintain
- Add parameter → update 1 function + call sites
- Bug → debug 1 function
- Test → 1 test function
Builder: 5+ methods to maintain
- Add parameter → update struct + new method + Build() + call sites
- Bug → debug 5 methods to find which one
- Test → 6+ test functions
Comparison Table
| Aspect | Current | Builder Option 1 | Builder Option 2 |
|---|---|---|---|
| Lines of code | 1 | 5 | 6 |
| API methods | 1 | 4 | 5 |
| Validation timing | At construction | At construction | After construction |
| Can have invalid Config? | ❌ No | ❌ No | ✅ Yes (bad!) |
| Double validation risk | ❌ No | ❌ No | ✅ Yes |
| Maintainability | ✅ High | ❌ Low | |
| Safety guarantees | ✅ Strong | ✅ Strong | ❌ Weak |
Problems with Each Option
Builder Option 1 Problems:
- More code to write and maintain (5 methods vs 1 function)
- When adding new parameter: must update struct + new method + Build() + all callers
- Bug fixing: must search through 5 methods instead of 1
- Testing: need 6+ test functions instead of 1
- More opportunities for breaking changes (5 methods can break vs 1)
Builder Option 2 Problems:
- Critical: Allows invalid Config objects to exist
- Developers can forget to call Validate()
- Must track validation state internally
- Double validation problem: If Build() validates → Validate() runs same checks again (waste). If Build() doesn't validate → invalid Config objects exist until Validate() is called (unsafe). Either way, it's problematic.
- Errors can occur in multiple places (Build() and Validate())
- Violates "safe by construction" principle
Conclusion from Analysis (Recommendation)
Keep current approach. Builder pattern would add complexity without benefit for 3 required interdependent parameters. Refactor to builder only if we grow to 7+ parameters.
The current design already follows best practices:
- ✅ Validation at construction time
- ✅ Immutable validated objects
- ✅ Simple, clear API
- ✅ Strong safety guarantees
Why wait?
- YAGNI principle - Don't add complexity for hypothetical future needs
- Current code is clean - Simple, safe, well-tested
- Easy to refactor later - If we add more parameters, we can migrate then
- No breaking change risk - Adding builder later doesn't break existing code
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 Test_ErrorFormatting_* tests make sure this parsing works. If we upgrade the | ||
| // JSON schema library and those tests start failing, we'll know we need to update | ||
| // this parsing logic. | ||
| func formatSchemaError(err error) error { |
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.
formatSchemaError is unprecise because it converts ValueError into another error instance. Also, why a user could not understand the original ValueError message?
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 minimal option here would be like
// formatSchemaError removes technical schema file paths from validation errors
// while preserving the library's clear error messages.
func formatSchemaError(err error) error {
msg := err.Error()
// Strip the "jsonschema validation failed with 'file:///.../schema.json#'" prefix
// Keep everything after the newline which has the actual error details
if idx := strings.Index(msg, "\n"); idx != -1 {
return fmt.Errorf("invalid configuration:%s", msg[idx:])
}
return fmt.Errorf("invalid configuration: %s", msg)
}However, I followed your suggestion to use DetailedOutput() / BasicOutput(), and I think we've achieved a better result with improved error messages that are neither overly fragile nor difficult to maintain.
Output Comparison (Minimal vs Current Approach)
| Scenario | Example Input | Minimal Approach | Current Approach | Winner |
|---|---|---|---|---|
| Missing required field | {} when watchNamespace required |
invalid configuration:- at '': missing property 'watchNamespace' |
required field "watchNamespace" is missing |
Current - Clearer terminology |
| Null instead of required | {"watchNamespace": null} |
invalid configuration:- at '/watchNamespace': got null, want string |
required field "watchNamespace" is missing |
Current - Recognizes intent |
| Unknown field | {"unknownField": "value"} |
invalid configuration:- at '': additional properties 'unknownField' not allowed |
unknown field "unknownField" |
Current - Concise |
| Type mismatch | {"watchNamespace": 123} |
invalid configuration:- at '/watchNamespace': got number, want string |
invalid type for field "watchNamespace": got number, want string |
Current - Adds field context |
| Nested field error | {"resources": {"memory": 512}} |
invalid configuration:- at '/resources/memory': got number, want string |
invalid type for field "resources.memory": got number, want string |
Current - Dot notation for paths |
| Root type error | true (not an object) |
invalid configuration:- at '': got boolean, want object |
invalid type: got boolean, want object |
Similar |
| Constraint violation | {"replicaCount": 0} with min=1 |
invalid configuration:- at '/replicaCount': value should be >= 1 |
value should be >= 1 |
Similar - Pass through |
| Enum violation | {"type": "Invalid"} |
invalid configuration:- at '/type': value should be one of [...] |
value should be one of [...] |
Similar - Pass through |
| Custom format validator | {"watchNamespace": "wrong-ns"} |
invalid configuration:- at '/watchNamespace': 'wrong-ns' is not valid ownNamespaceInstallMode: ... |
invalid value "wrong-ns": watchNamespace must be "install-ns" (the namespace where the operator is installed) ... |
Current - No bullet prefix |
| Multiple errors | {} requires 2 fields |
invalid configuration:- at '': missing property 'replicaCount'(stops at first error) |
multiple errors found: - required field "replicaCount" is missing - required field "image" is missing |
Current - Shows ALL errors! |
I think we should keep the custom as it is now.
WDYT?
5720563 to
c16f9e0
Compare
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.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
c16f9e0 to
e97183c
Compare
e97183c to
daa37bd
Compare
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.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Bundle configuration is now validated using JSONSchema. Configuration errors (typos, missing required fields, wrong types) are caught immediately with clear error messages instead of failing during installation. Assisted-by: Cursor
daa37bd to
6392c11
Compare
When we install an operator using ClusterExtension, we can now configure things like which namespace it should watch. Previously, if you made a typo or configured it incorrectly, the error would only show up later during deployment. Now, configuration is validated immediately using JSON Schema, and we get clear error messages right away.
Why We Did This
Issue: https://issues.redhat.com/browse/OPRUN-4112
Users were getting confusing errors when they misconfigured their operators. By validating configuration upfront, we can give much better error messages that tell you exactly what's wrong and how to fix it.
How It Works
We introduced a
ConfigSchemaProviderinterface that lets different bundle types describe their own configuration rules and all packages format types (registry/v1 or helm) use the same validation process - only the source of the rules changes.Do I Need to Configure
watchNamespace?It depends on what install modes your operator supports:
Examples That Work
No configuration (AllNamespaces mode)
Watch specific namespace (SingleNamespace mode)
Watch install namespace (OwnNamespace mode)
Error Messages
All errors start with
invalid ClusterExtension configuration: invalid configuration:followed by the specific problem.Typo in Field Name
Error:
Missing Required Field
Error:
Wrong Type
Error:
OwnNamespace Mode - Wrong Namespace
Error:
SingleNamespace Mode - Can't Use Install Namespace
Error:
Invalid JSON/YAML
Error:
Technical Details
Design Diagram