Skip to content

Recover Full Site Before Reconfiguring Rotuer#2519

Open
c-kruse wants to merge 1 commit into
skupperproject:mainfrom
c-kruse:fix-site-recovery-stability
Open

Recover Full Site Before Reconfiguring Rotuer#2519
c-kruse wants to merge 1 commit into
skupperproject:mainfrom
c-kruse:fix-site-recovery-stability

Conversation

@c-kruse

@c-kruse c-kruse commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Adds a new recovery mode to supress partial router ConfigMap writes with missing bridge configuration. Adds additional Controler init processes for recovering all resources that can affect site bindings prior to finalization, including selector connector pods and network status for exposeByPods listeners.

Closes #2504

Summary by CodeRabbit

  • New Features

    • Enhanced startup recovery to restore attached connector resources, connector pod watchers, and network status more reliably after restart.
    • Added explicit sync capabilities for connector pod state during recovery.
  • Bug Fixes

    • Preserves router configuration during recovery, including bridge and connector TCP listeners/connectors.
    • Prevents router updates from being applied mid-recovery; reapplies recovered link/router settings when recovery finishes.
    • Improves status update robustness by retrying on Kubernetes update conflicts.

Adds a new recovery mode to supress partial router ConfigMap writes with
missing bridge configuration. Adds additional Controler init processes
for recovering all resources that can affect site bindings prior to
finalization, including selector connector pods and network status for
exposeByPods listeners.

Signed-off-by: Christian Kruse <christian@c-kruse.com>
@c-kruse c-kruse requested a review from AryanP123 July 2, 2026 17:59
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds pod watcher sync support, recovery-mode tracking, conflict-aware status retries, and controller startup recovery changes so recovered sites finish with stable router config.

Changes

Recovery flow and router config stability

Layer / File(s) Summary
TargetSelection Sync interface and implementations
internal/kube/site/bindings.go, internal/kube/site/extended_bindings.go, internal/kube/site/bindings_test.go
Adds Sync(stopCh) bool support to TargetSelection, delegates it through TargetSelectionImpl and PodWatcher, adds ExtendedBindings.SyncConnectorPods and SyncAttachedConnectorPods, and updates the mock/test coverage for sync behavior.
Site recovery state and status retry
internal/kube/site/site.go, internal/kube/site/binding_status.go
Adds Site.inRecovery, suppresses router config updates during recovery, reapplies recovered router config after recovery, exposes site-level sync delegates, and wraps binding/network status updates in conflict-aware retry logic.
Controller watcher fields and startup recovery sequencing
internal/kube/controller/controller.go
Stores additional watcher handles on the controller, reorders recovery initialization, tracks sites that need recovery completion, syncs connector pod watchers with a timeout, and switches recovered sites to FinishRecovery.
Recovery test coverage
internal/kube/controller/controller_test.go
Adds controller recovery coverage that exercises router config preservation through fake Kubernetes client updates and final router ConfigMap verification.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding recovery mode to avoid router reconfiguration churn during startup.
Linked Issues check ✅ Passed The changes address #2504 by suppressing router config writes during recovery and reapplying full config after recovery completes.
Out of Scope Changes check ✅ Passed The additional controller recovery and conflict-retry changes support the recovery flow and do not appear unrelated to the PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

@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: 3

🧹 Nitpick comments (1)
internal/kube/site/extended_bindings.go (1)

397-413: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Early-return skips syncing remaining independent watchers.

Both methods stop at the first failing selector/connector, so all subsequent (independent) pod watchers are never given a chance to sync. Since the caller only logs and continues on failure (best-effort during recovery), continuing the loop and aggregating the overall result would maximize how much state is fresh before updateRecoveredRouterConfig builds the final config.

♻️ Proposed fix: continue syncing all watchers, aggregate result
 func (b *ExtendedBindings) SyncConnectorPods(stopCh <-chan struct{}) bool {
+	ok := true
 	for _, selector := range b.selectors {
-		if selector != nil && !selector.Sync(stopCh) {
-			return false
-		}
+		if selector != nil && !selector.Sync(stopCh) {
+			ok = false
+		}
 	}
-	return true
+	return ok
 }

 func (b *ExtendedBindings) SyncAttachedConnectorPods(stopCh <-chan struct{}) bool {
+	ok := true
 	for _, connector := range b.connectors {
-		if connector.watcher != nil && !connector.watcher.Sync(stopCh) {
-			return false
-		}
+		if connector.watcher != nil && !connector.watcher.Sync(stopCh) {
+			ok = false
+		}
 	}
-	return true
+	return ok
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 493f76bd-0b66-45e3-a727-a0fdcd1e8bbc

📥 Commits

Reviewing files that changed from the base of the PR and between 143bc97 and c9223bb.

📒 Files selected for processing (6)
  • internal/kube/controller/controller.go
  • internal/kube/controller/controller_test.go
  • internal/kube/site/bindings.go
  • internal/kube/site/bindings_test.go
  • internal/kube/site/extended_bindings.go
  • internal/kube/site/site.go

Comment on lines +350 to +353
for _, connector := range c.attachedConnectorWatcher.List() {
if !c.namespaces.isControlled(connector.Namespace) {
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Filter attached connector recovery by target site namespace.

checkAttachedConnector applies the object to connector.Spec.SiteNamespace, so this recovery filter can skip externally-namespaced attached connectors that target a controlled site. Use the target site namespace for the control check.

🐛 Proposed fix
 	for _, connector := range c.attachedConnectorWatcher.List() {
-		if !c.namespaces.isControlled(connector.Namespace) {
+		if !c.namespaces.isControlled(connector.Spec.SiteNamespace) {
 			continue
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, connector := range c.attachedConnectorWatcher.List() {
if !c.namespaces.isControlled(connector.Namespace) {
continue
}
for _, connector := range c.attachedConnectorWatcher.List() {
if !c.namespaces.isControlled(connector.Spec.SiteNamespace) {
continue
}

Comment thread internal/kube/controller/controller.go
Comment on lines +142 to 153
func (s *Site) FinishRecovery(siteDef *skupperv2alpha1.Site) error {
if !s.inRecovery {
return s.Reconcile(siteDef)
}

err := s.Reconcile(siteDef)
s.inRecovery = false
if err != nil {
return err
}
return s.updateRecoveredRouterConfig()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clearing inRecovery before confirming the deferred update succeeded.

s.inRecovery = false runs immediately after s.Reconcile(siteDef), before its error is checked. If Reconcile fails, updateRecoveredRouterConfig() is never invoked for this attempt, yet inRecovery is already cleared — so a subsequent FinishRecovery retry (if any) will take the !s.inRecovery fast path and call plain Reconcile instead of re-running the aggregated recovery update. This happens to still converge correctly today because a fully-initialised site's normal update path pushes full state (not incremental listener-by-listener state), but it's fragile: any future change to that normal path (or an interleaved event calling updateRouterConfigForGroup on a single component in between) could reintroduce the very partial-config churn this PR is fixing.

Consider keeping inRecovery set until updateRecoveredRouterConfig() actually succeeds:

🐛 Proposed fix: keep inRecovery set until the recovered update succeeds
 	err := s.Reconcile(siteDef)
-	s.inRecovery = false
 	if err != nil {
+		s.inRecovery = false
 		return err
 	}
-	return s.updateRecoveredRouterConfig()
+	if err := s.updateRecoveredRouterConfig(); err != nil {
+		return err
+	}
+	s.inRecovery = false
+	return nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *Site) FinishRecovery(siteDef *skupperv2alpha1.Site) error {
if !s.inRecovery {
return s.Reconcile(siteDef)
}
err := s.Reconcile(siteDef)
s.inRecovery = false
if err != nil {
return err
}
return s.updateRecoveredRouterConfig()
}
func (s *Site) FinishRecovery(siteDef *skupperv2alpha1.Site) error {
if !s.inRecovery {
return s.Reconcile(siteDef)
}
err := s.Reconcile(siteDef)
if err != nil {
s.inRecovery = false
return err
}
if err := s.updateRecoveredRouterConfig(); err != nil {
return err
}
s.inRecovery = false
return nil
}

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/kube/site/site.go (1)

1535-1542: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid mutating s.site before persisting network status. updateSiteNetworkStatus updates the in-memory object first; if UpdateSiteStatus or the conflict retry fails, the next NetworkStatusUpdated call can hit the reflect.DeepEqual fast path and skip retrying the same network update.

Suggested change
 func (s *Site) updateSiteNetworkStatus(network []skupperv2alpha1.SiteRecord) (*skupperv2alpha1.Site, error) {
-	s.site.Status.Network = network
-	s.site.Status.SitesInNetwork = len(network)
-	updated, err := s.UpdateSiteStatus(s.site)
+	desired := s.site.DeepCopy()
+	desired.Status.Network = network
+	desired.Status.SitesInNetwork = len(network)
+	updated, err := s.UpdateSiteStatus(desired)
 	if !errors.IsConflict(err) {
 		return updated, err
 	}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03368e85-0b2a-4a32-a189-275c72090bb5

📥 Commits

Reviewing files that changed from the base of the PR and between c9223bb and 86a4949.

📒 Files selected for processing (2)
  • internal/kube/site/binding_status.go
  • internal/kube/site/site.go

Comment thread internal/kube/site/binding_status.go Outdated
@c-kruse c-kruse force-pushed the fix-site-recovery-stability branch from 86a4949 to c9223bb Compare July 2, 2026 20:03
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.

Controller Startup Churns Router tcpListener Configuration

1 participant