-
Notifications
You must be signed in to change notification settings - Fork 1
proxy: scope datapath rules to the node hosting the backend #18
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: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,83 @@ type ServicesController struct { | |
| Clientset *kubernetes.Clientset | ||
| Services *ServiceMap | ||
| Proxy nat.ProxyProcessor | ||
|
|
||
| // NodeName is the node this instance runs on. Datapath rules are only | ||
| // programmed for backend pods hosted here. Empty disables the check and | ||
| // restores the previous cluster-wide behavior. | ||
| NodeName string | ||
| } | ||
|
|
||
| // endpointNode returns the node hosting the endpoint's first address. | ||
| func endpointNode(ep *v1.Endpoints) (string, bool) { | ||
| if !hasValidEndpointIP(ep) { | ||
| return "", false | ||
| } | ||
| node := ep.Subsets[0].Addresses[0].NodeName | ||
| if node == nil || *node == "" { | ||
| return "", false | ||
| } | ||
| return *node, true | ||
| } | ||
|
|
||
| // servesEndpoint reports whether this node must program datapath rules for ep. | ||
| // | ||
| // The rules are node-local: only the node hosting the backend pod may rewrite | ||
| // the service IP. Programming them cluster-wide makes a non-owning node | ||
| // translate the destination before the packet even leaves it, so the owning | ||
| // node records a conntrack tuple the SNATed reply can no longer match, and | ||
| // port_filter drops that reply. | ||
| // | ||
| // When the node name is unknown (NODE_NAME not injected, or an endpoint | ||
| // carrying no NodeName) the rules are programmed anyway, so an older chart | ||
| // keeps the previous behavior instead of silently losing the datapath. | ||
| func (c *ServicesController) servesEndpoint(ep *v1.Endpoints) bool { | ||
| if c.NodeName == "" { | ||
| return true | ||
| } | ||
| node, ok := endpointNode(ep) | ||
| if !ok { | ||
| return true | ||
| } | ||
| return node == c.NodeName | ||
| } | ||
|
|
||
| // applyRules programs the datapath for a (service, endpoint) pair when this | ||
| // node hosts the backend pod, and withdraws it otherwise. Both objects must | ||
| // already have been checked with hasValidServiceIP/hasValidEndpointIP. | ||
| func (c *ServicesController) applyRules(svc *v1.Service, ep *v1.Endpoints, ctx string) { | ||
| svcIP := svc.Status.LoadBalancer.Ingress[0].IP | ||
| podIP := ep.Subsets[0].Addresses[0].IP | ||
|
|
||
| if !c.servesEndpoint(ep) { | ||
| c.withdrawRules(svcIP, podIP, ctx+" (backend not on this node)") | ||
| return | ||
| } | ||
|
|
||
| c.Proxy.EnsureRules(svcIP, podIP) | ||
| c.reconcilePortFilter(svc, svcIP, podIP, ctx) | ||
| } | ||
|
|
||
| // withdrawRules removes every datapath entry for the pair. Absent entries are | ||
| // not an error. | ||
| func (c *ServicesController) withdrawRules(svcIP, podIP, ctx string) { | ||
| c.clearPortFilter(svcIP, podIP, ctx) | ||
| c.Proxy.DeleteRules(svcIP, podIP) | ||
|
Comment on lines
+152
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Handle failed proxy reconciliation. Lines 152 and 160 discard errors from 🧰 Tools🪛 golangci-lint (2.12.2)[error] 152-152: Error return value of (errcheck) [error] 160-160: Error return value of (errcheck) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| } | ||
|
|
||
| // withdrawStaleEndpoint drops the rules of a previous endpoint whose pod IP no | ||
| // longer matches the current one, which is what happens when a VM is migrated | ||
| // to another node. Without this the old node keeps a mapping for a pod it no | ||
| // longer hosts. | ||
| func (c *ServicesController) withdrawStaleEndpoint(svc *v1.Service, prev *v1.Endpoints, podIP, ctx string) { | ||
| if !hasValidServiceIP(svc) || !hasValidEndpointIP(prev) { | ||
| return | ||
| } | ||
| prevPodIP := prev.Subsets[0].Addresses[0].IP | ||
| if prevPodIP == podIP { | ||
| return | ||
| } | ||
| c.withdrawRules(svc.Status.LoadBalancer.Ingress[0].IP, prevPodIP, ctx+" (stale endpoint)") | ||
| } | ||
|
|
||
| // Start initializes the NAT, runs the service and endpoint informers, and cleans up removed services. | ||
|
|
@@ -190,12 +267,15 @@ func (c *ServicesController) Start(ctx context.Context) error { | |
| } | ||
| log.Info("endpoints synchronization completed") | ||
|
|
||
| // Run cleanup for removed services. | ||
| // Run cleanup for removed services. A failure here is logged but does not | ||
| // abort: exiting takes the pod down and leaves the node's datapath | ||
| // half-programmed, whereas the informers below converge on the next event. | ||
| log.Info("running cleanup for removed services") | ||
| if err := c.cleanupRemovedServices(); err != nil { | ||
| return fmt.Errorf("failed to cleanup removed services: %w", err) | ||
| log.Error(err, "cleanup of removed services failed, continuing with reconciliation") | ||
| } else { | ||
| log.Info("cleanup of removed services completed") | ||
| } | ||
|
Comment on lines
+270
to
278
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Retry failed startup cleanup. The informer caches are already synced before this call. A cleanup failure is only logged, and 🤖 Prompt for AI Agents |
||
| log.Info("cleanup of removed services completed") | ||
|
|
||
| <-ctx.Done() | ||
| log.Info("shutting down services-controller") | ||
|
|
@@ -231,11 +311,7 @@ func (c *ServicesController) addServiceFunc(obj interface{}) { | |
| if err == nil && ep != nil && hasValidEndpointIP(ep) && hasValidServiceIP(svc) { | ||
| se.Endpoint = ep | ||
| c.Services.Set(svc.Namespace, svc.Name, se) | ||
| svcIP := svc.Status.LoadBalancer.Ingress[0].IP | ||
| podIP := ep.Subsets[0].Addresses[0].IP | ||
| // Ensure NAT mapping rules are set. | ||
| c.Proxy.EnsureRules(svcIP, podIP) | ||
| c.reconcilePortFilter(svc, svcIP, podIP, "on svc add") | ||
| c.applyRules(svc, ep, "on svc add") | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -330,10 +406,10 @@ func (c *ServicesController) updateServiceFunc(oldObj, newObj interface{}) { | |
|
|
||
| // At this point, both the Service and Endpoint have valid IPs. | ||
| // Ensure NAT mapping is up-to-date. | ||
| svcIP := svc.Status.LoadBalancer.Ingress[0].IP | ||
| podIP := ep.Subsets[0].Addresses[0].IP | ||
| c.Proxy.EnsureRules(svcIP, podIP) | ||
| c.reconcilePortFilter(svc, svcIP, podIP, "on svc update") | ||
| if se, exists := c.Services.Get(svc.Namespace, svc.Name); exists { | ||
| c.withdrawStaleEndpoint(svc, se.Endpoint, ep.Subsets[0].Addresses[0].IP, "on svc update") | ||
| } | ||
| c.applyRules(svc, ep, "on svc update") | ||
|
|
||
| // Update or add the service mapping with the new endpoint. | ||
| c.Services.Set(svc.Namespace, svc.Name, &ServiceEndpoints{Service: svc, Endpoint: ep}) | ||
|
|
@@ -360,10 +436,8 @@ func (c *ServicesController) addEndpointFunc(obj interface{}) { | |
|
|
||
| // If both the Service and the Endpoint have valid IPs, ensure NAT mapping rules. | ||
| if hasValidServiceIP(se.Service) && hasValidEndpointIP(ep) { | ||
| svcIP := se.Service.Status.LoadBalancer.Ingress[0].IP | ||
| podIP := ep.Subsets[0].Addresses[0].IP | ||
| c.Proxy.EnsureRules(svcIP, podIP) | ||
| c.reconcilePortFilter(se.Service, svcIP, podIP, "on endpoint add") | ||
| c.withdrawStaleEndpoint(se.Service, se.Endpoint, ep.Subsets[0].Addresses[0].IP, "on endpoint add") | ||
| c.applyRules(se.Service, ep, "on endpoint add") | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -420,10 +494,8 @@ func (c *ServicesController) updateEndpointFunc(oldObj, newObj interface{}) { | |
| if !hasValidEndpointIP(ep) { | ||
| return | ||
| } | ||
| svcIP := se.Service.Status.LoadBalancer.Ingress[0].IP | ||
| podIP := ep.Subsets[0].Addresses[0].IP | ||
| c.Proxy.EnsureRules(svcIP, podIP) | ||
| c.reconcilePortFilter(se.Service, svcIP, podIP, "on endpoint update") | ||
| c.withdrawStaleEndpoint(se.Service, se.Endpoint, ep.Subsets[0].Addresses[0].IP, "on endpoint update") | ||
| c.applyRules(se.Service, ep, "on endpoint update") | ||
| c.Services.SetEndpoint(ep.Namespace, ep.Name, ep) | ||
| } | ||
|
|
||
|
|
@@ -555,6 +627,13 @@ func (c *ServicesController) cleanupRemovedServices() error { | |
| if serviceEndpoints.Service != nil && serviceEndpoints.Endpoint != nil { | ||
| var serviceIP, endpointIP string | ||
|
|
||
| // Backends hosted elsewhere are not ours to program, so they must | ||
| // not be kept: this is what purges entries inherited from a build | ||
| // that programmed every service on every node. | ||
| if !c.servesEndpoint(serviceEndpoints.Endpoint) { | ||
| continue | ||
| } | ||
|
|
||
| if len(serviceEndpoints.Service.Status.LoadBalancer.Ingress) > 0 { | ||
| serviceIP = serviceEndpoints.Service.Status.LoadBalancer.Ingress[0].IP | ||
| } | ||
|
|
@@ -582,7 +661,7 @@ func (c *ServicesController) cleanupRemovedServices() error { | |
| if !hasValidServiceIP(se.Service) || !hasValidEndpointIP(se.Endpoint) { | ||
| continue | ||
| } | ||
| if wholeIPPassthrough(se.Service) { | ||
| if wholeIPPassthrough(se.Service) || !c.servesEndpoint(se.Endpoint) { | ||
| continue | ||
| } | ||
| keepFilters[se.Service.Status.LoadBalancer.Ingress[0].IP] = nat.PortFilterEntry{ | ||
|
|
@@ -603,7 +682,7 @@ func (c *ServicesController) cleanupRemovedServices() error { | |
| if !hasValidServiceIP(se.Service) || !hasValidEndpointIP(se.Endpoint) { | ||
| continue | ||
| } | ||
| if wholeIPPassthrough(se.Service) || !allowICMP(se.Service) { | ||
| if wholeIPPassthrough(se.Service) || !allowICMP(se.Service) || !c.servesEndpoint(se.Endpoint) { | ||
| continue | ||
| } | ||
| keepICMP[se.Service.Status.LoadBalancer.Ingress[0].IP] = se.Endpoint.Subsets[0].Addresses[0].IP | ||
|
|
||
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.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: cozystack/cozy-proxy
Length of output: 7094
🌐 Web query:
Kubernetes 1.33 Endpoints API deprecated EndpointSlice replacement💡 Result:
As of Kubernetes 1.33, the Endpoints API is officially deprecated in favor of the EndpointSlice API [1][2][3]. Key details regarding this change: Official Deprecation: The v1 Endpoints API has been marked as deprecated starting in Kubernetes 1.33 [1][4]. The Kubernetes API server now issues warnings to users who read from or write to Endpoints resources [1][5]. Reasoning: The Endpoints API is considered a legacy resource that lacks support for modern service features, such as dual-stack networking, advanced traffic distribution, and scalability for services with large numbers of endpoints [1][4][5]. The EndpointSlice API was introduced to address these limitations and provide a more scalable way to track network endpoints [1][6]. Future of the Endpoints API: While deprecated, the Endpoints API is expected to remain available to maintain backward compatibility for existing workloads and scripts [1][3]. However, it is no longer the recommended practice for managing service traffic [1][2]. Migration: Users and controllers are encouraged to transition to the discovery.k8s.io/v1 EndpointSlice API [1][7]. For most use cases, migrating involves updating YAML manifests or code to use the EndpointSlice resource type, which organizes endpoint information differently than the Endpoints API (e.g., using conditions like ready: false instead of separate lists for ready and not-ready endpoints) [1]. Mirroring: To maintain compatibility, the control plane continues to mirror most user-created Endpoints resources to corresponding EndpointSlices, though this mirroring behavior is also deprecated alongside the Endpoints API itself [6].
Citations:
Plan migration from the deprecated Endpoints API.
Kubernetes deprecated
v1.Endpointsin Kubernetes 1.33;discoveryv1.EndpointSliceis the replacement API. This dependency is already at k8s.io/api v0.36.2, so plan EndpointSlice reconciliation when supporting clusters that stop serving Endpoints. EndpointSlice replacement must aggregate slices and endpoints per service rather than reading only the firstv1.Endpointssubset/address.Affected sites:
pkg/controllers/services_controller.go#L106-L115pkg/controllers/services_controller_test.go#L15-L22🧰 Tools
🪛 golangci-lint (2.12.2)
[error] 107-107: SA1019: v1.Endpoints is deprecated: This API is deprecated in v1.33+. Use discoveryv1.EndpointSlice.
(staticcheck)
📍 Affects 2 files
pkg/controllers/services_controller.go#L106-L115(this comment)pkg/controllers/services_controller_test.go#L15-L22🤖 Prompt for AI Agents
Source: Linters/SAST tools