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

Examples That Work

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

Technical Details

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  │
                 └──────────────┘

@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 daa37bd
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/6916263ef0ffdc00087c0d42
😎 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.72043% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.29%. Comparing base (c95fc24) to head (e97183c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ternal/operator-controller/rukpak/bundle/config.go 74.60% 21 Missing and 11 partials ⚠️
internal/operator-controller/applier/provider.go 75.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2316      +/-   ##
==========================================
- Coverage   74.30%   74.29%   -0.01%     
==========================================
  Files          91       92       +1     
  Lines        7083     7217     +134     
==========================================
+ Hits         5263     5362      +99     
- Misses       1405     1428      +23     
- Partials      415      427      +12     
Flag Coverage Δ
e2e 44.71% <0.00%> (-0.94%) ⬇️
experimental-e2e 48.69% <62.36%> (+0.33%) ⬆️
unit 58.91% <81.72%> (+0.31%) ⬆️

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
@openshift-ci
Copy link

openshift-ci bot commented Nov 9, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign joelanford for approval. For more information see the Code Review Process.

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 /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@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.

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings November 13, 2025 12:16
Copilot finished reviewing on behalf of camilamacedo86 November 13, 2025 12:18
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 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.

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 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.

Comment on lines 271 to 276
name: "handles empty installNamespace gracefully when OwnNamespace only",
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace},
rawConfig: []byte(`{"watchNamespace": "valid-ns"}`),
installNamespace: "",
expectedWatchNamespace: ptr.To("valid-ns"),
},
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The test name says "handles empty installNamespace gracefully when OwnNamespace only", but this scenario represents a configuration issue rather than expected behavior. When an operator only supports OwnNamespace install mode (meaning watchNamespace must equal installNamespace), having an empty installNamespace makes validation meaningless.

The current implementation (lines 169-170 in config.go) skips validation when installNamespace == "", allowing any value. While this prevents crashes, it creates a confusing edge case where invalid configurations are silently accepted.

Consider either:

  1. Renaming the test to clarify this is testing fallback behavior (e.g., "skips validation when installNamespace is empty")
  2. Or changing the validation logic to require a non-empty installNamespace when format validators are active

Copilot uses AI. Check for mistakes.
// UnmarshalConfig, which does the validation.
type Config struct {
WatchNamespace *string `json:"watchNamespace"`
data map[string]any
Copy link
Contributor Author

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 {
Copy link
Contributor

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) {
Copy link
Contributor

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()

// 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 {
Copy link
Contributor

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?

Copy link
Contributor Author

@camilamacedo86 camilamacedo86 Nov 13, 2025

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!

Give a look and let me wdyt?

Copilot AI review requested due to automatic review settings November 13, 2025 17:55
Copilot finished reviewing on behalf of camilamacedo86 November 13, 2025 17:56
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 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.

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
Copilot AI review requested due to automatic review settings November 13, 2025 18:40
Copilot finished reviewing on behalf of camilamacedo86 November 13, 2025 18:44
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 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.

Comment on lines +225 to +226
// Not a ValidationError, return as-is wrapped
return fmt.Errorf("invalid configuration: %w", err)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The error wrapping at this line could result in duplicate "invalid configuration:" prefixes in error messages. When this error is returned to UnmarshalConfig (line 134), it will be wrapped again with "invalid configuration: %w", resulting in "invalid configuration: invalid configuration: ..." for non-ValidationError cases.

Consider returning the error without the "invalid configuration:" prefix here, since the caller already adds it:

return err
Suggested change
// Not a ValidationError, return as-is wrapped
return fmt.Errorf("invalid configuration: %w", err)
// Not a ValidationError, return as-is
return err

Copilot uses AI. Check for mistakes.
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.

3 participants