-
Notifications
You must be signed in to change notification settings - Fork 263
fix: init NNC from reconciler instead of directly #4199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request refactors the CNS (Container Networking Service) initialization to use the NNC (NodeNetworkConfig) reconciler flow instead of a separate out-of-band initialization path. This consolidates NNC reads to a single location, preventing bugs from ingesting stale NNCs and ensuring caching/filtering optimizations are consistently applied.
Changes:
- Removed direct NNC client initialization and replaced it with an initializer callback passed to the reconciler
- Modified the reconciler to invoke the initializer function on the first reconcile
- Updated function signatures to accept the initializer as a parameter
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| cns/service/main.go | Removed direct NNC client creation and retry logic; replaced with initialization closure passed to reconciler |
| cns/kubecontroller/nodenetworkconfig/reconciler.go | Added initializer callback field and invocation logic in Reconcile method |
| cns/kubecontroller/nodenetworkconfig/reconciler_test.go | Updated test calls to include no-op initializer function parameter |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cns/service/main.go
Outdated
| if err := reconcileInitialCNSState(nnc, httpRestServiceImplementation, podInfoByIPProvider, cnsconfig.EnableSwiftV2); err != nil { | ||
| return err | ||
| } | ||
| hasNNCInitialized.Set(2) |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The metric value changed from 1 to 2, but there's no explanation for this change. The metric name and documentation suggest it's a boolean indicator (0 or 1), so setting it to 2 is unclear. Please clarify the intent or revert to the original value of 1.
| hasNNCInitialized.Set(2) | |
| hasNNCInitialized.Set(1) |
| logger.Printf("reconciled initial CNS state after %d attempts", attempt) | ||
| hasNNCInitialized.Set(1) | ||
| logger.Printf("Reconciling initial CNS state") | ||
| if err := reconcileInitialCNSState(nnc, httpRestServiceImplementation, podInfoByIPProvider, cnsconfig.EnableSwiftV2); err != nil { |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nncInitFailure metric counter is no longer incremented when initialization fails. In the previous code, this counter was incremented in the retry loop. Consider incrementing this counter when reconcileInitialCNSState returns an error to maintain observability of initialization failures.
| if err := reconcileInitialCNSState(nnc, httpRestServiceImplementation, podInfoByIPProvider, cnsconfig.EnableSwiftV2); err != nil { | |
| if err := reconcileInitialCNSState(nnc, httpRestServiceImplementation, podInfoByIPProvider, cnsconfig.EnableSwiftV2); err != nil { | |
| metrics.NNCInitFailure.Inc() |
| // Provided nncListeners are passed the NNC after the Reconcile preprocesses it. Note: order matters! The | ||
| // passed Listeners are notified in the order provided. | ||
| func NewReconciler(cnscli cnsClient, ipampoolmonitorcli nodeNetworkConfigListener, nodeIP string, isSwiftV2 bool) *Reconciler { | ||
| func NewReconciler(cnscli cnsClient, initializer nodenetworkconfigSink, ipampoolmonitorcli nodeNetworkConfigListener, nodeIP string, isSwiftV2 bool) *Reconciler { |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra space between parameters in the function signature.
| func NewReconciler(cnscli cnsClient, initializer nodenetworkconfigSink, ipampoolmonitorcli nodeNetworkConfigListener, nodeIP string, isSwiftV2 bool) *Reconciler { | |
| func NewReconciler(cnscli cnsClient, initializer nodenetworkconfigSink, ipampoolmonitorcli nodeNetworkConfigListener, nodeIP string, isSwiftV2 bool) *Reconciler { |
cns/service/main.go
Outdated
| if err != nil { | ||
| logger.Errorf("failed to reconcile initial CNS state, attempt: %d err: %v", attempt, err) | ||
| nncInitFailure.Inc() | ||
|
|
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing whitespace on empty line.
| return nil | ||
| } | ||
|
|
||
|
|
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing whitespace on empty line.
| // call initFunc on first reconcile | ||
| if err := r.initializer(nnc); err != nil { | ||
| logger.Errorf("[cns-rc] initializer failed during reconcile: %v", err) | ||
| return reconcile.Result{}, errors.Wrap(err, "initializer failed during reconcile") | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new initialization logic using the initializer callback is not covered by tests. The existing tests pass a no-op function (func(*v1alpha.NodeNetworkConfig) error { return nil }). Consider adding tests that verify:
- The initializer is called exactly once on the first reconcile
- The initializer is not called on subsequent reconciles
- Error handling when the initializer returns an error
cns/service/main.go
Outdated
| hasInitialized := false | ||
| initFunc := func(nnc *v1alpha.NodeNetworkConfig) error { | ||
| if hasInitialized { | ||
| return nil | ||
| } | ||
| return errors.Wrap(err, "failed to initialize CNS state") | ||
| }, retry.Context(ctx), retry.Delay(initCNSInitalDelay), retry.MaxDelay(time.Minute), retry.UntilSucceeded()) | ||
| logger.Printf("reconciled initial CNS state after %d attempts", attempt) | ||
| hasNNCInitialized.Set(1) | ||
| logger.Printf("Reconciling initial CNS state") | ||
| if err := reconcileInitialCNSState(nnc, httpRestServiceImplementation, podInfoByIPProvider, cnsconfig.EnableSwiftV2); err != nil { | ||
| return err | ||
| } | ||
| hasNNCInitialized.Set(2) | ||
| hasInitialized = true | ||
| return nil |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hasInitialized flag is not protected by a mutex, which could lead to a race condition if the reconciler's Reconcile method is called concurrently. Controller-runtime can invoke reconcile handlers concurrently for different requests. Consider using a sync.Once pattern similar to how the reconciler's started field is managed, or protect the flag with a mutex.
|
/azp run Azure Container Networking PR |
|
Azure Pipelines successfully started running 1 pipeline(s). |
cfab669 to
14b9b41
Compare
|
/azp run Azure Container Networking PR |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
|
||
| type nodeNetworkConfigListener interface { | ||
| Update(*v1alpha.NodeNetworkConfig) error | ||
| Update(*v1alpha.NodeNetworkConfig) error // phasing this out in favor of the sink |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a // Deprecated: use nodenetworkconfigSink?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point
| err = reconcileInitialCNSState(ctx, directscopedcli, httpRestServiceImplementation, podInfoByIPProvider, cnsconfig.EnableSwiftV2) | ||
| if err != nil { | ||
| logger.Errorf("failed to reconcile initial CNS state, attempt: %d err: %v", attempt, err) | ||
| nncInitFailure.Inc() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still want to keep this counter around?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be implicitly counted in failed nnc reconciles
| logger.Errorf("[cns-rc] initializer failed during reconcile: %v", err) | ||
| return reconcile.Result{}, errors.Wrap(err, "initializer failed during reconcile") | ||
| } | ||
| r.initializer = nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's the same behavior for this to be wedged until initialization succeeds, but do we want to eventually circuit break? It seems like that was the original intention of the existing logic as I read the comment above it... whether that was true in practice may be dubious though because of swallowing the retry error and the UntilSucceeded().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You read the prior intent correctly - we wanted to retry for a while and eventually fail out as a signal to the user that something was wrong. However, the retry was long enough that CNS wouldn't exit frequently enough to end up in CrashLoopBackoff and instead the Pod would just clock 5-6 restarts per hour.
Eventually someone might notice, but in practice this effectively was a retry-forever.
By tying this in to the Reconcile loop, we will be able to lean on the ctrlruntime machinery - it automatically does retry backoff and exposes metrics for failed reconciles which we can collect and use as a signal.
nddq
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor comments, but the changes make sense
| // call initFunc on first reconcile and never again | ||
| if r.initializer != nil { | ||
| if err := r.initializer(nnc); err != nil { | ||
| logger.Errorf("[cns-rc] initializer failed during reconcile: %v", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cns-rc? that's new 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops
| // Reconcile is called on CRD status changes | ||
| func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { | ||
| listenersToNotify := []nodeNetworkConfigListener{} | ||
| listenersToNotify := []nodenetworkconfigSink{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
were there going to be any other listener other than the ipam pool monitor?
Reason for Change:
There's no clear reason that CNS initializes with an NNC that it gets out of band from the typical NNC reconciler flow.
This separate path for fetching the NNC leads to bugs such as ingesting an NNC for a previous Node of the same name and state corruption once the reconciler (which is smart enough to wait for the actual NNC for its Node) starts.
Additionally, any caching, filtering, or other selection done to NNCs for efficiency is not applied or must be duplicated.
Instead, initialization can be run from the first NNC that the reconciler is pushed. This consolidates all CNS NNC reads to a single location where list-watch optimizations and filtering can be centralized.