Skip to content

OLS-3450: Add credentialHotReload flag to skip app-server restart on LLM secret rotation#1779

Open
lalan7 wants to merge 1 commit into
openshift:mainfrom
lalan7:feat/credential-hot-reload
Open

OLS-3450: Add credentialHotReload flag to skip app-server restart on LLM secret rotation#1779
lalan7 wants to merge 1 commit into
openshift:mainfrom
lalan7:feat/credential-hot-reload

Conversation

@lalan7

@lalan7 lalan7 commented Jul 2, 2026

Copy link
Copy Markdown

Summary

  • Adds opt-in credentialHotReload boolean to OLSSpec CRD (default: false)
  • When enabled, the operator's secret watcher skips rolling restart for LLM credential secrets — the service re-reads credentials from disk on each request (RFE-9380)
  • Preserves existing behavior for all clusters when disabled

Test plan

  • Unit tests: 3 new tests covering skip/no-skip/non-LLM paths (23/23 pass)
  • E2e test: rotate LLM secret with flag enabled, verify no restart via Consistently
  • go vet ./... clean
  • go build ./... clean
  • CRD regenerated via make manifests

Related

…LLM secret rotation

Adds an opt-in credentialHotReload boolean to OLSSpec CRD. When enabled,
the operator's secret watcher skips rolling restart for LLM credential
secrets, allowing the service's in-process hot-reload to handle rotated
tokens without downtime (RFE-9380).

Default: false (preserves existing behavior).

- New CRD field: spec.olsConfig.credentialHotReload (default: false)
- Watcher: skip restart for LLM secrets when flag is true
- Info log emitted during reconciliation when enabled
- Unit tests (3 new) + e2e test for hot-reload skip
- Documentation: docs/credential-hot-reload.md

Companion to lightspeed-service PR #2955.
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jul 2, 2026
@openshift-ci-robot

openshift-ci-robot commented Jul 2, 2026

Copy link
Copy Markdown

@lalan7: This pull request references OLS-3450 which is a valid jira issue.

Details

In response to this:

Summary

  • Adds opt-in credentialHotReload boolean to OLSSpec CRD (default: false)
  • When enabled, the operator's secret watcher skips rolling restart for LLM credential secrets — the service re-reads credentials from disk on each request (RFE-9380)
  • Preserves existing behavior for all clusters when disabled

Test plan

  • Unit tests: 3 new tests covering skip/no-skip/non-LLM paths (23/23 pass)
  • E2e test: rotate LLM secret with flag enabled, verify no restart via Consistently
  • go vet ./... clean
  • go build ./... clean
  • CRD regenerated via make manifests

Related

Made with Cursor

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a CredentialHotReload boolean flag to OLSConfig that, when enabled, allows the operator to skip app-server restarts on LLM credential secret data changes. Includes reconciler wiring, watcher config population, secret-watcher skip logic, unit tests, an e2e test, and documentation.

Changes

Credential Hot-Reload

Layer / File(s) Summary
API field and watcher config schema
api/v1alpha1/olsconfig_types.go, internal/controller/utils/types.go
Adds optional CredentialHotReload *bool to OLSSpec and extends WatcherConfig with CredentialHotReload and LLMSecretNames fields plus concurrency documentation.
Reconciler wiring and logging
internal/controller/olsconfig_helpers.go, internal/controller/olsconfig_controller.go
annotateExternalResources resets and populates LLMSecretNames from LLM providers and sets CredentialHotReload from the CR spec; Reconcile logs an informational message when hot-reload is enabled.
Secret watcher restart-skip logic and tests
internal/controller/watchers/watchers.go, internal/controller/watchers/watchers_test.go
SecretWatcherFilter skips deployment restart when hot-reload is enabled and the secret is a tracked LLM secret; tests cover enabled/disabled and matched/unmatched secret scenarios.
E2E validation and documentation
test/e2e/reconciliation_test.go, docs/credential-hot-reload.md
E2E test rotates an LLM secret with hot-reload enabled and asserts the Deployment generation is unchanged; documentation explains the flag, behavior matrix, and prerequisites.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Reconciler as OLSConfigReconciler
  participant Watcher as WatcherConfig
  participant Filter as SecretWatcherFilter
  participant Deployment as AppServerDeployment

  Reconciler->>Watcher: set CredentialHotReload, populate LLMSecretNames
  Note over Filter: On secret change event
  Filter->>Watcher: check CredentialHotReload and LLMSecretNames
  alt hot-reload enabled and secret tracked
    Filter-->>Filter: log skip, no restart
  else
    Filter->>Deployment: annotate for restart
  end
Loading

Poem

A rabbit hops through secrets deep,
No restart needed while credentials sleep,
Watchers watch, but hold their paw,
Hot-reload spares the app from law,
Docs and tests confirm the claim—
🐰 rotation now, without the flame.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: adding credentialHotReload to avoid app-server restarts on LLM secret rotation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@openshift-ci openshift-ci Bot requested review from bparees and xrajesh July 2, 2026 20:34
@openshift-ci

openshift-ci Bot commented Jul 2, 2026

Copy link
Copy Markdown

[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 blublinsky for approval. For more information see the Code Review Process.

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

Details 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

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/controller/watchers/watchers_test.go (1)

363-471: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated Deployment/reconciler setup into a helper.

The three new tests duplicate identical dep/createTestReconciler boilerplate. A small local helper (e.g., newTestDeploymentAndReconciler(cr)) would reduce repetition within this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/watchers/watchers_test.go` around lines 363 - 471, The
new Credential hot-reload tests duplicate the same Deployment and reconciler
setup in each case. Extract that repeated boilerplate into a small local helper
in watchers_test.go, such as a helper that builds the standard Deployment and
calls createTestReconciler, then reuse it in the three Describe/It blocks while
keeping the test-specific watcher config and Secret setup inline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/controller/utils/types.go`:
- Around line 60-73: `WatcherConfig` is shared mutable state and is accessed
concurrently by `SecretWatcherFilter()` while `annotateExternalResources()`
updates `CredentialHotReload` and `LLMSecretNames`; add synchronization or
switch to an immutable snapshot approach. Update the `WatcherConfig` type in
`types.go` to guard these fields with a mutex (or refactor to store/retrieve a
copied config atomically), and make both `SecretWatcherFilter()` and
`annotateExternalResources()` use the same protection when reading or writing
`CredentialHotReload` and `LLMSecretNames`.

In `@test/e2e/reconciliation_test.go`:
- Around line 400-416: The reconciliation test uses a fixed sleep after updating
OLSConfig.CredentialHotReload, which makes the hot-reload setup timing-dependent
and flaky. Replace the blind time.Sleep in the credentialHotReload setup with an
Eventually-based wait on an observable signal from the reconciler, such as a
status condition, annotation, or other field updated when the latest
spec.generation has been processed. Use the existing client.Update, client.Get,
and deployment.Generation flow to locate the test, and keep the later
Consistently assertion only after the reconciler-ready signal is observed.

---

Nitpick comments:
In `@internal/controller/watchers/watchers_test.go`:
- Around line 363-471: The new Credential hot-reload tests duplicate the same
Deployment and reconciler setup in each case. Extract that repeated boilerplate
into a small local helper in watchers_test.go, such as a helper that builds the
standard Deployment and calls createTestReconciler, then reuse it in the three
Describe/It blocks while keeping the test-specific watcher config and Secret
setup inline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b0d1bbf1-2bfc-49b1-a705-c12229c81079

📥 Commits

Reviewing files that changed from the base of the PR and between 7ed4504 and b59c74c.

⛔ Files ignored due to path filters (1)
  • config/crd/bases/ols.openshift.io_olsconfigs.yaml is excluded by !config/crd/bases/**
📒 Files selected for processing (8)
  • api/v1alpha1/olsconfig_types.go
  • docs/credential-hot-reload.md
  • internal/controller/olsconfig_controller.go
  • internal/controller/olsconfig_helpers.go
  • internal/controller/utils/types.go
  • internal/controller/watchers/watchers.go
  • internal/controller/watchers/watchers_test.go
  • test/e2e/reconciliation_test.go

Comment on lines +60 to 73
// WatcherConfig contains all watcher configuration.
// NOTE: This struct is written by the reconciler and read by watcher event handlers
// (including predicate filters such as SecretWatcherFilter). This is safe because
// controller-runtime serializes reconcile calls and predicate evaluations on the same
// controller work queue. If MaxConcurrentReconciles is ever increased above 1,
// a sync.RWMutex must be added here.
type WatcherConfig struct {
Secrets SecretWatcherConfig
ConfigMaps ConfigMapWatcherConfig
AnnotatedSecretMapping map[string][]string
AnnotatedConfigMapMapping map[string][]string
CredentialHotReload bool
LLMSecretNames map[string]bool
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does controller-runtime call Predicate.Update/Create in the same goroutine as Reconcile, or in the informer event handler goroutine?

💡 Result:

In controller-runtime, Predicate functions (Create, Update, Delete, Generic) are executed in the informer event handler goroutine, not in the goroutine that runs the Reconcile function [1][2]. Predicates operate within the informer event path, where they serve as filters that determine whether an event should be enqueued for reconciliation [1][3]. Because they execute before a request enters the workqueue, they must be efficient, non-blocking, and should not perform expensive computations or API calls [1]. In contrast, the Reconcile function runs in a separate pool of worker goroutines dedicated to processing the workqueue [1][4]. The number of concurrent Reconcile goroutines is controlled by the MaxConcurrentReconciles configuration [1]. The separation ensures that the critical path of informer event processing remains fast, preventing bottlenecks from propagating back to the informer's event stream [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and find all uses of WatcherConfig / LLMSecretNames / CredentialHotReload.
ast-grep outline internal/controller/utils/types.go --view expanded || true
printf '\n--- uses ---\n'
rg -n "WatcherConfig|LLMSecretNames|CredentialHotReload|SecretWatcherFilter|annotateExternalResources" internal/controller -S

printf '\n--- types.go excerpt ---\n'
sed -n '1,160p' internal/controller/utils/types.go

printf '\n--- relevant controller excerpts ---\n'
for f in $(rg -l "WatcherConfig|LLMSecretNames|CredentialHotReload|SecretWatcherFilter|annotateExternalResources" internal/controller -S); do
  echo "### $f"
  sed -n '1,260p' "$f"
done

Repository: openshift/lightspeed-operator

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- watcher filter ---\n'
sed -n '220,340p' internal/controller/watchers/watchers.go

printf '\n--- annotateExternalResources ---\n'
sed -n '360,420p' internal/controller/olsconfig_helpers.go

printf '\n--- any synchronization around WatcherConfig ---\n'
rg -n "RWMutex|Mutex|Lock\\(|RLock\\(|WatcherConfig" internal/controller -S

printf '\n--- setup/watch registration ---\n'
sed -n '520,700p' internal/controller/olsconfig_controller.go

Repository: openshift/lightspeed-operator

Length of output: 258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- watcher filter ---'
sed -n '220,340p' internal/controller/watchers/watchers.go

echo
echo '--- annotateExternalResources ---'
sed -n '360,420p' internal/controller/olsconfig_helpers.go

echo
echo '--- synchronization search ---'
rg -n "RWMutex|Mutex|Lock\\(|RLock\\(|WatcherConfig" internal/controller -S

echo
echo '--- watch registration ---'
sed -n '520,700p' internal/controller/olsconfig_controller.go

Repository: openshift/lightspeed-operator

Length of output: 21680


Protect WatcherConfig from concurrent access. SecretWatcherFilter() reads CredentialHotReload and LLMSecretNames while annotateExternalResources() rewrites them during reconcile on a different goroutine. This shared state needs a mutex or immutable snapshot; otherwise a secret event can race with reconcile and hit concurrent map access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/utils/types.go` around lines 60 - 73, `WatcherConfig` is
shared mutable state and is accessed concurrently by `SecretWatcherFilter()`
while `annotateExternalResources()` updates `CredentialHotReload` and
`LLMSecretNames`; add synchronization or switch to an immutable snapshot
approach. Update the `WatcherConfig` type in `types.go` to guard these fields
with a mutex (or refactor to store/retrieve a copied config atomically), and
make both `SecretWatcherFilter()` and `annotateExternalResources()` use the same
protection when reading or writing `CredentialHotReload` and `LLMSecretNames`.

Comment on lines +400 to +416
By("enable credentialHotReload on the CR")
err = client.Update(cr, func(obj ctrlclient.Object) error {
config := obj.(*olsv1alpha1.OLSConfig)
hotReload := true
config.Spec.OLSConfig.CredentialHotReload = &hotReload
return nil
})
Expect(err).NotTo(HaveOccurred())

// credentialHotReload only affects in-memory WatcherConfig, not the
// Deployment spec, so there is no Generation bump to wait for.
// Give the reconciler time to process the CR update.
time.Sleep(5 * time.Second)

err = client.Get(deployment)
Expect(err).NotTo(HaveOccurred())
generation := deployment.Generation

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Hard-coded sleep for reconciliation is a flakiness risk.

time.Sleep(5 * time.Second) assumes the reconciler will have processed the credentialHotReload CR update (and repopulated WatcherConfig) within 5 seconds. Under CI load or slower reconcile loops, the LLM secret rotation could race ahead of the watcher-config update, causing the subsequent Consistently check to intermittently fail (the deployment would restart because hot-reload wasn't yet active in-memory).

Since there's no Deployment/ConfigMap side-effect to poll on (per the comment), consider polling on an observable signal instead of a blind sleep — e.g., increasing the sleep with a retry/backoff, or exposing a lightweight status condition/annotation that reflects the reconciler having processed the latest spec.generation, and using Eventually against that.

💡 Possible mitigation without a new status signal
-		// credentialHotReload only affects in-memory WatcherConfig, not the
-		// Deployment spec, so there is no Generation bump to wait for.
-		// Give the reconciler time to process the CR update.
-		time.Sleep(5 * time.Second)
+		// credentialHotReload only affects in-memory WatcherConfig, not the
+		// Deployment spec, so there is no Generation bump to wait for.
+		// Poll to reduce flakiness risk vs. a fixed sleep.
+		Eventually(func() bool {
+			var current olsv1alpha1.OLSConfig
+			current.Name = cr.Name
+			if err := client.Get(&current); err != nil {
+				return false
+			}
+			return current.Spec.OLSConfig.CredentialHotReload != nil && *current.Spec.OLSConfig.CredentialHotReload
+		}, 15*time.Second, 1*time.Second).Should(BeTrue())

As per path instructions, "Flag flaky timing, missing teardown, or assumptions about optional components not deployed in bundle/operator CI jobs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/reconciliation_test.go` around lines 400 - 416, The reconciliation
test uses a fixed sleep after updating OLSConfig.CredentialHotReload, which
makes the hot-reload setup timing-dependent and flaky. Replace the blind
time.Sleep in the credentialHotReload setup with an Eventually-based wait on an
observable signal from the reconciler, such as a status condition, annotation,
or other field updated when the latest spec.generation has been processed. Use
the existing client.Update, client.Get, and deployment.Generation flow to locate
the test, and keep the later Consistently assertion only after the
reconciler-ready signal is observed.

Source: Path instructions

@openshift-ci

openshift-ci Bot commented Jul 2, 2026

Copy link
Copy Markdown

@lalan7: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/bundle-e2e-4-21 b59c74c link true /test bundle-e2e-4-21

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@lalan7

lalan7 commented Jul 7, 2026

Copy link
Copy Markdown
Author

Hi @blublinsky — this is the operator companion to service PR openshift/lightspeed-service#2955 (RFE-9380).

Adds an opt-in credentialHotReload boolean to the OLSConfig CRD. When enabled, the operator skips rolling restarts for LLM credential secret rotations — the service handles it in-process. Default is false, so no behavior change for existing clusters.

All CI checks pass. The bundle-e2e-4-21 failure is a pre-existing main issue (unknown fields agenticConsole/alertsAdapter in test fixtures from PRs #1758/#1759), not related to this change.

Would appreciate a review when you have a chance.

@blublinsky

Copy link
Copy Markdown
Contributor

Suggestion: drive hot-reload from ForEachExternalSecret instead of SecretWatcherFilter

The flag is currently wired in two places: LLMSecretNames + CredentialHotReload are populated before ForEachExternalSecret, and SecretWatcherFilter has a runtime skip branch. That duplicates the provider iteration and adds special-case logic on the event path.

A simpler approach is to handle this inside the ForEachExternalSecret callback, the same way TLS secrets get their AnnotatedSecretMapping today: when credentialHotReload is enabled and source is llm-provider-*, do not annotate that secret (skip annotateSecretIfNeeded). Without the watcher annotation, shouldWatchSecret never admits it, so no restart watcher is created for that secret — no .data events on the hot-reload path, no SecretWatcherFilter changes, no LLMSecretNames map, no extra fields on WatcherConfig, no skip logic at event time.

When the flag is off, behavior stays as today: foreach annotates LLM credential secrets and rotations trigger a rolling restart as they do now.

That keeps “which secrets does the operator react to?” entirely in the existing foreach/mapping configuration model, instead of splitting it between reconcile-time config and filter-time exceptions.

One thing to nail when toggling the flag on for clusters that already have annotated LLM secrets: reconcile may need to remove the watcher annotation when hot-reload is enabled, not only skip adding it for new ones.


I’d hold off on deeper review until this is reworked — the foreach-based approach removes most of the PR’s watcher-layer changes and is the pattern we already use for other secret types.

@blublinsky

Copy link
Copy Markdown
Contributor

Follow-up: credentialHotReload must reach the service via OLS config for end-to-end behavior

Today this flag lives only on the CR and in operator WatcherConfig (skip restart). It is not propagated into the app-server olsconfig ConfigMap (buildOLSConfig / AppSrvConfigFile) and the service PR has no corresponding config field either.

That leaves two independent behaviors with no runtime coupling:

Layer What happens
Operator (credentialHotReload: true) Skips pod restart on LLM secret rotation
Service (lightspeed-service #2955) Re-reads credentials_path on every request — but unconditionally, not driven by this flag

For this to work end-to-end, the flag should flow:

OLSConfig CR (spec.ols.credentialHotReload)
  → operator builds olsconfig ConfigMap
  → service reads config and enables hot-reload credential reads when true

Without that, enabling the CR flag on a cluster with an older service image (or before #2955 is deployed) silently disables restarts while credentials stay stale at startup — the CR comment warns about this, but nothing in config enforces it.

Suggest:

  1. Add the field to the generated OLS config (utils.OLSConfig / buildOLSConfig).
  2. Service side: gate get_credentials() disk re-read on that config flag (default false for backward compatibility).
  3. Operator watcher skip (ideally via ForEachExternalSecret / no annotation) should follow the same CR flag.

That gives one user-facing switch that coordinates both “don’t restart” and “do re-read from disk,” instead of two repos assuming the other half is always correct.

@blublinsky

Copy link
Copy Markdown
Contributor

Scoping: operator-only PR + bundle follow-up

This PR is correctly scoped to the operator only — service changes stay in openshift/lightspeed-service#2955. Keep it that way.

Once the operator changes here are in good shape, suggest landing this PR without blocking on the OLM bundle, then a follow-up PR that runs make manifests && make bundle (or hack/update_bundle.sh) so bundle/manifests/ols.openshift.io_olsconfigs.yaml picks up credentialHotReload. Right now the field is only in config/crd/bases/; OLM installs use the bundle CRD, so the feature isn’t usable from the shipped bundle until that follow-up.

Order I’d expect:

  1. Operator PR (this repo) — CRD + reconcile/watcher wiring + config pass-through to olsconfig once agreed
  2. Service PR — gated get_credentials() re-read on config flag
  3. Bundle follow-up — regenerate committed bundle/ manifests

That keeps review focused and matches how we usually ship CRD schema vs bundle image updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants