Skip to content

Conversation

@camilamacedo86
Copy link
Contributor

@camilamacedo86 camilamacedo86 commented Nov 9, 2025

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 ConfigSchemaProvider interface 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:

What Your Operator Supports Do You Need watchNamespace?
AllNamespaces only No - you can omit it or set it to null
OwnNamespace only Yes - must be the same namespace where the operator is installed
SingleNamespace only Yes - must be a different namespace than where the operator is installed
AllNamespaces + OwnNamespace No - optional, defaults to AllNamespaces if you don't specify
OwnNamespace + SingleNamespace Yes - you must pick a namespace

Design Diagram

┌─────────────────────────────────────────────────────────────┐
│                    ConfigSchemaProvider                     │
│                         (Interface)                         │
└────────────┬────────────────────────────────┬───────────────┘
             │                                │
             ▼                                ▼
    ┌────────────────┐              ┌─────────────────┐
    │  RegistryV1    │              │  Future: Helm,  │
    │                │              │  Registry+v2    │
    │ GetConfigSchema│              │                 │
    │ (generates     │              │ GetConfigSchema │
    │  from install  │              │ (reads from     │
    │  modes)        │              │  chart/bundle)  │
    └────────┬───────┘              └────────┬────────┘
             │                               │
             └───────────┬───────────────────┘
                         ▼
              ┌──────────────────────┐
              │  UnmarshalConfig()   │
              │                      │
              │  1. Get schema       │
              │  2. Validate         │──→ Custom Validators:
              │  3. Parse            │    • ownNamespaceInstallMode
              └──────────┬───────────┘    • singleNamespaceInstallMode
                         │
                         ▼
                 ┌──────────────┐
                 │ bundle.Config│
                 │              │
                 │ - Opaque data│
                 │ - Accessors  │
                 └──────────────┘

TL"DR:

Examples - Use cases

No configuration (AllNamespaces mode)

spec:
  packageName: my-operator

Watch specific namespace (SingleNamespace mode)

metadata:
  namespace: operator-install-ns
spec:
  packageName: my-operator
  config:
    watchNamespace: target-namespace  # Different from install namespace

Watch install namespace (OwnNamespace mode)

metadata:
  namespace: my-namespace
spec:
  packageName: my-operator
  config:
    watchNamespace: my-namespace  # Same as install namespace

Error Messages

All errors start with invalid ClusterExtension configuration: invalid configuration: followed by the specific problem.

Typo in Field Name

spec:
  config:
    watchNamespce: my-namespace  # Typo

Error:

invalid ClusterExtension configuration: invalid configuration: unknown field "watchNamespce"

Missing Required Field

spec:
  packageName: my-operator
  # No config - but operator requires watchNamespace

Error:

invalid ClusterExtension configuration: invalid configuration: required field "watchNamespace" is missing

Wrong Type

spec:
  config:
    watchNamespace: 123  # Should be a string

Error:

invalid ClusterExtension configuration: invalid configuration: invalid value type for field "watchNamespace": expected "string" but got "number"

OwnNamespace Mode - Wrong Namespace

metadata:
  namespace: correct-namespace
spec:
  config:
    watchNamespace: wrong-namespace  # Must match install namespace

Error:

invalid ClusterExtension configuration: invalid configuration: configuration validation failed: 
- at '/watchNamespace': 'wrong-namespace' is not valid ownNamespaceInstallMode: 
  invalid value "wrong-namespace": watchNamespace must be "correct-namespace" 
  (the namespace where the operator is installed) because this operator only 
  supports OwnNamespace install mode

SingleNamespace Mode - Can't Use Install Namespace

metadata:
  namespace: install-ns
spec:
  config:
    watchNamespace: install-ns  # Must be different from install namespace

Error:

invalid ClusterExtension configuration: invalid configuration: configuration validation failed: 
- at '/watchNamespace': 'install-ns' is not valid singleNamespaceInstallMode: 
  invalid value "install-ns": watchNamespace must be different from "install-ns" 
  (the install namespace) because this operator uses SingleNamespace install mode 
  to watch a different namespace

Invalid JSON/YAML

spec:
  config: {"incomplete

Error:

invalid ClusterExtension configuration: invalid configuration: found unexpected end of stream
Why we are formatting the messages:

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!

@camilamacedo86 camilamacedo86 requested a review from a team as a code owner November 9, 2025 09:32
@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 9, 2025
@netlify
Copy link

netlify bot commented Nov 9, 2025

Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit b10fc49
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/691b35abafd61600089d8180
😎 Deploy Preview https://deploy-preview-2316--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link

codecov bot commented Nov 9, 2025

Codecov Report

❌ Patch coverage is 81.40704% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.31%. Comparing base (c06f27f) to head (b10fc49).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
internal/operator-controller/config/config.go 75.53% 22 Missing and 12 partials ⚠️
internal/operator-controller/applier/provider.go 75.00% 1 Missing and 1 partial ⚠️
...al/operator-controller/rukpak/bundle/registryv1.go 98.07% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2316      +/-   ##
==========================================
+ Coverage   74.30%   74.31%   +0.01%     
==========================================
  Files          91       92       +1     
  Lines        7083     7226     +143     
==========================================
+ Hits         5263     5370     +107     
- Misses       1405     1427      +22     
- Partials      415      429      +14     
Flag Coverage Δ
e2e 44.69% <0.00%> (-0.95%) ⬇️
experimental-e2e 48.68% <59.79%> (+0.28%) ⬆️
unit 58.23% <55.77%> (-0.38%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@camilamacedo86 camilamacedo86 force-pushed the add-checks-config branch 3 times, most recently from a753a5c to f15ea9f Compare November 9, 2025 10:35
@camilamacedo86 camilamacedo86 changed the title WIP: ✨ (chore): Add structured bundle config validation helpers and tests. ✨ (chore): Add structured bundle config validation helpers and tests. Nov 9, 2025
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 9, 2025
@camilamacedo86 camilamacedo86 changed the title ✨ (chore): Add structured bundle config validation helpers and tests. ✨ (chore): Bundle configuration is now validated - errors caught early with clear messages. Nov 9, 2025
@camilamacedo86 camilamacedo86 changed the title ✨ (chore): Bundle configuration is now validated - errors caught early with clear messages. ✨ Bundle configuration is now validated - errors caught early with clear messages. Nov 9, 2025
Copilot AI review requested due to automatic review settings November 10, 2025 10:13
@camilamacedo86 camilamacedo86 force-pushed the add-checks-config branch 2 times, most recently from 1f3451a to 65bd4ef Compare November 10, 2025 10:17

This comment was marked as outdated.

@camilamacedo86 camilamacedo86 force-pushed the add-checks-config branch 2 times, most recently from d78bf4a to 920f78e Compare November 10, 2025 11:08

This comment was marked as outdated.

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings November 17, 2025 13:19
Copilot finished reviewing on behalf of camilamacedo86 November 17, 2025 13:21
Copy link

Copilot AI left a 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 10 out of 10 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.

@camilamacedo86
Copy link
Contributor Author

Hi @pedjak,

Regarding the function name:

I would suggest choosing a better name for this function, because we are not formatting the error — we convert it.

I am not the best to pick names but IMHO, formatSchemaError is still the right name. The purpose of the function is to
format the error message into something user-friendly. The conversion step is just
the internal mechanism behind that formatting ("how" and not "what"). Naming should reflect the goal,
not the implementation details.

When someone sees formatSchemaError, they immediately understand: “this takes a
schema error and formats it for users.” That’s exactly what it does.

Would keeping this name be a blocker for moving forward?

type SchemaProvider interface {
// Get returns a JSON Schema describing what configuration is valid.
// Returns nil if this package format type doesn't need configuration validation.
Get() (map[string]any, error)
Copy link
Contributor

Choose a reason for hiding this comment

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

I get that at this level it becomes redundant, but maybe we should say what we're getting? something like?

Suggested change
Get() (map[string]any, error)
GetConfigSchema() (map[string]any, error)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. Done.

Copilot AI review requested due to automatic review settings November 17, 2025 14:32
Copilot finished reviewing on behalf of camilamacedo86 November 17, 2025 14:35
Copy link

Copilot AI left a 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 10 out of 10 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 228 to 229
// Note: Using %s not %w since ve.Error() is already a formatted string
return fmt.Errorf("%s", ve.Error())
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

[nitpick] Using fmt.Errorf("%s", ve.Error()) creates an error wrapping an error string, which is unnecessarily verbose. Consider using errors.New(ve.Error()) instead for cleaner error messages, or if you need to wrap the original error for debugging, use fmt.Errorf("invalid configuration: %w", ve) to preserve the error chain.

Suggested change
// Note: Using %s not %w since ve.Error() is already a formatted string
return fmt.Errorf("%s", ve.Error())
// Note: Using errors.New since ve.Error() is already a formatted string
return errors.New(ve.Error())

Copilot uses AI. Check for mistakes.
}

if len(errorMessages) == 0 {
return fmt.Errorf("invalid configuration: %s", ve.Error())
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

[nitpick] Using fmt.Errorf("invalid configuration: %s", ve.Error()) creates an error wrapping an error string. Consider using fmt.Errorf("invalid configuration: %w", ve) instead to preserve the error chain, or errors.New("invalid configuration: " + ve.Error()) if you don't need error wrapping.

Suggested change
return fmt.Errorf("invalid configuration: %s", ve.Error())
return fmt.Errorf("invalid configuration: %w", ve)

Copilot uses AI. Check for mistakes.
@perdasilva
Copy link
Contributor

/approve 🚀

@openshift-ci openshift-ci bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Nov 17, 2025
Copy link
Contributor

@pedjak pedjak left a comment

Choose a reason for hiding this comment

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

/lgtm

@openshift-ci openshift-ci bot added the lgtm Indicates that a PR is ready to be merged. label Nov 17, 2025
@openshift-ci
Copy link

openshift-ci bot commented Nov 17, 2025

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: pedjak, perdasilva

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

ClusterExtension 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
@openshift-ci openshift-ci bot removed the lgtm Indicates that a PR is ready to be merged. label Nov 17, 2025
@pedjak
Copy link
Contributor

pedjak commented Nov 17, 2025

/lgtm

@openshift-ci openshift-ci bot added the lgtm Indicates that a PR is ready to be merged. label Nov 17, 2025
@openshift-merge-bot openshift-merge-bot bot merged commit 15b4904 into operator-framework:main Nov 17, 2025
25 checks passed
@camilamacedo86 camilamacedo86 deleted the add-checks-config branch November 17, 2025 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants