Skip to content

fix: return errors instead of panicking in config file substitution - #324

Open
mvanhorn wants to merge 2 commits into
redhat-data-and-ai:mainfrom
mvanhorn:fix/276-config-loader-panic-on-file-read
Open

fix: return errors instead of panicking in config file substitution#324
mvanhorn wants to merge 2 commits into
redhat-data-and-ai:mainfrom
mvanhorn:fix/276-config-loader-panic-on-file-read

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Changes

📝 Description

What changed?

substituteString in pkg/config/loader.go previously called panic() when os.ReadFile failed for a file| substitution path. It now returns a wrapped error (failed to read config file <path>: <cause>). SubstituteConfigValues returns error and propagates the first failure up through (*Config).Load, whose signature already returns error. The env| substitution and plain-string handling are unchanged.

Why is this change needed?

If a Kubernetes secret volume mount is missing or a configured file path is wrong, the operator crashes with no recovery path. Returning an error lets the caller handle the failure instead of taking down the process. Fixes #276.

Dependencies

  • N/A

🧪 Testing

Test Coverage

Added pkg/config/loader_test.go covering: reading a valid file| path, an env| value, a plain string, a missing file| path returning an error without panicking, and Load surfacing the file error. Updated the existing TestFileSubstitution to the new SubstituteConfigValues return signature.

Ran go build ./... (passes) and go test -count=1 ./pkg/config/... (11 tests pass).

Performance Impact

  • N/A

🚀 Deployment

Deploy Steps

  1. N/A

Prerequisites

  • N/A

Post-Deployment Monitoring

  • N/A

Rollback Plan

  • N/A

⚠️ Breaking Changes

  • This PR contains breaking changes
  • Migration guide provided (if applicable)

Details:

The exported SubstituteConfigValues signature changes from func(reflect.Value) to func(reflect.Value) error. All in-tree callers are updated. External callers, if any, need to handle the returned error.


⚙️ Configuration Changes

  • N/A

✅ Developer Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added positive and negative tests that prove my fix is effective or that my feature works
  • Relevant documentation (README, tech specs, etc.) has been added or updated
  • All CI/CD checks are passing

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the robustness of the configuration loader by replacing runtime panics with graceful error handling. When a file-based configuration substitution fails, the system now propagates the error up the call stack, allowing the application to handle missing files or incorrect paths without crashing. This change introduces a breaking API modification to the substitution functions, which has been addressed across the codebase.

Highlights

  • Error Handling Improvement: Replaced panic() calls with explicit error returns in the configuration file substitution logic to prevent operator crashes when file paths are invalid.
  • API Signature Change: Updated the signature of SubstituteConfigValues to return an error, requiring updates to all internal callers.
  • Testing: Added a new test suite in pkg/config/loader_test.go to verify error propagation and file substitution behavior.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


The code would panic, stop, and die, / When config files were not nearby. / Now errors flow with grace and care, / And leave the system standing there.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the configuration loader to return errors instead of panicking during file substitution, updating the associated tests to handle these errors. The review feedback suggests several optimizations and improvements: handling reflect.Interface types during reflection traversal, and reducing reflection overhead by skipping recursive processing for primitive types inside maps, slices, and arrays.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/config/loader.go
Comment on lines 117 to 123
// If it's a pointer, resolve it
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return
return nil
}
SubstituteConfigValues(v.Elem())
return
return SubstituteConfigValues(v.Elem())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Currently, SubstituteConfigValues only unwraps pointers (reflect.Ptr) at the top level. If a struct contains a field of type interface{} (e.g., interface{} holding a nested struct or map), its kind will be reflect.Interface. Since this is not handled at the top level, the function will return nil immediately, and any string values inside the interface will not be substituted.

We should handle reflect.Interface similarly to reflect.Ptr by unwrapping it using v.Elem().

Suggested change
// If it's a pointer, resolve it
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return
return nil
}
SubstituteConfigValues(v.Elem())
return
return SubstituteConfigValues(v.Elem())
}
// If it's a pointer or interface, resolve it
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
if v.IsNil() {
return nil
}
return SubstituteConfigValues(v.Elem())
}

Comment thread pkg/config/loader.go
Comment on lines 145 to 161
// Only settable if map value is addressable, so we replace by setting
if val.Kind() == reflect.String {
newVal := reflect.ValueOf(substituteString(val.String()))
substituted, err := substituteString(val.String())
if err != nil {
return err
}
newVal := reflect.ValueOf(substituted)
v.SetMapIndex(key, newVal)
} else {
// Recursively process nested maps/structs
copyVal := reflect.New(val.Type()).Elem()
copyVal.Set(val)
SubstituteConfigValues(copyVal)
if err := SubstituteConfigValues(copyVal); err != nil {
return err
}
v.SetMapIndex(key, copyVal)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In the else block of the map processing logic, any non-string value (including primitive types like int, bool, float64, etc.) is recursively processed. This causes unnecessary allocations via reflect.New and reflection overhead for every primitive value in a map.

We can optimize this by only recursively processing types that can actually contain nested structures or strings (i.e., pointers, structs, maps, slices, and arrays).

Suggested change
// Only settable if map value is addressable, so we replace by setting
if val.Kind() == reflect.String {
newVal := reflect.ValueOf(substituteString(val.String()))
substituted, err := substituteString(val.String())
if err != nil {
return err
}
newVal := reflect.ValueOf(substituted)
v.SetMapIndex(key, newVal)
} else {
// Recursively process nested maps/structs
copyVal := reflect.New(val.Type()).Elem()
copyVal.Set(val)
SubstituteConfigValues(copyVal)
if err := SubstituteConfigValues(copyVal); err != nil {
return err
}
v.SetMapIndex(key, copyVal)
}
// Only settable if map value is addressable, so we replace by setting
if val.Kind() == reflect.String {
substituted, err := substituteString(val.String())
if err != nil {
return err
}
newVal := reflect.ValueOf(substituted)
v.SetMapIndex(key, newVal)
} else if k := val.Kind(); k == reflect.Ptr || k == reflect.Struct || k == reflect.Map || k == reflect.Slice || k == reflect.Array {
// Recursively process nested maps/structs/slices/pointers
copyVal := reflect.New(val.Type()).Elem()
copyVal.Set(val)
if err := SubstituteConfigValues(copyVal); err != nil {
return err
}
v.SetMapIndex(key, copyVal)
}

Comment thread pkg/config/loader.go
Comment on lines 165 to 173
// If it's a slice or array, process its elements
if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
for i := 0; i < v.Len(); i++ {
SubstituteConfigValues(v.Index(i))
if err := SubstituteConfigValues(v.Index(i)); err != nil {
return err
}
}
return
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When processing slices or arrays, the function recursively calls SubstituteConfigValues on every single element. For large slices of primitive types (e.g., []int, []bool), this results in significant unnecessary overhead.

We can optimize this by checking the element type of the slice/array first, and skipping the loop entirely if the elements cannot contain strings or nested structures.

	// If it's a slice or array, process its elements
	if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
		elemKind := v.Type().Elem().Kind()
		if elemKind == reflect.String || elemKind == reflect.Ptr || elemKind == reflect.Interface ||
			elemKind == reflect.Struct || elemKind == reflect.Map || elemKind == reflect.Slice || elemKind == reflect.Array {
			for i := 0; i < v.Len(); i++ {
				if err := SubstituteConfigValues(v.Index(i)); err != nil {
					return err
				}
			}
		}
		return nil
	}

The dev.yaml setup line in TestLoad exceeded the 120-char limit (lll).
Split the os.WriteFile arguments across lines; no behavior change.
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.

[H5] panic() in config loader crashes operator on missing secret file

1 participant