You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/09/22 00:47:06 UTC
(superset-kubernetes-operator) branch main updated: fix(controller): preserve auto-allocated Service node ports (#360)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git
The following commit(s) were added to refs/heads/main by this push:
new e863c44 fix(controller): preserve auto-allocated Service node ports (#360)
e863c44 is described below
commit e863c44c1967f45bad059bf2a293eee118ba3d57
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Mon Sep 21 17:47:00 2026 -0700
fix(controller): preserve auto-allocated Service node ports (#360)
* fix(controller): preserve auto-allocated Service node ports
For a component whose `service.type` is `NodePort` or `LoadBalancer` and
that does not pin `service.nodePort`, the apiserver auto-allocates a node
port on create. The reconciler rebuilt the Service spec from scratch on
every reconcile with `nodePort: 0` and did not carry the allocated value
over, so the apiserver re-allocated a different node port each time —
churning the Service and intermittently breaking external access (and the
LoadBalancer target).
`preserveServiceAllocatedFields` already carries over apiserver-allocated
fields like clusterIP and healthCheckNodePort; extend it to also preserve
per-port node ports for NodePort/LoadBalancer Services when the user did
not pin one, mirroring that logic. ClusterIP Services are left untouched
so no node port is set on a type that rejects it.
* docs(changelog): note Service node port preservation fix
---
docs/reference/releases.md | 1 +
internal/controller/component_reconciler.go | 29 +++++++++++++++
internal/controller/component_reconciler_test.go | 45 ++++++++++++++++++++++++
3 files changed, 75 insertions(+)
diff --git a/docs/reference/releases.md b/docs/reference/releases.md
index fbfddf2..93cdd40 100644
--- a/docs/reference/releases.md
+++ b/docs/reference/releases.md
@@ -29,6 +29,7 @@ This page tracks notable changes in Apache Superset Kubernetes Operator releases
### Fixed
+- Component Services of type `NodePort` or `LoadBalancer` that do not pin `service.nodePort` no longer churn: the operator now preserves the apiserver-allocated node port across reconciles instead of triggering a re-allocation each time, which previously changed the external port and broke external access ([#360](https://github.com/apache/superset-kubernetes-operator/pull/360), [@villebro](https://github.com/villebro)).
- The operator now requests `update` on `supersets/finalizers`, so reconciliation no longer fails on clusters with the `OwnerReferencesPermissionEnforcement` admission plugin enabled (e.g. OpenShift), where the API server previously rejected the `blockOwnerDeletion` owner references the operator sets on child resources ([#359](https://github.com/apache/superset-kubernetes-operator/pull/359), [@villebro](https://github.com/villebro)).
- The chart no longer rejects top-level values it does not declare, so shared values files and deploy tooling that injects release metadata no longer fail schema validation. Nested value blocks are still strictly validated.
diff --git a/internal/controller/component_reconciler.go b/internal/controller/component_reconciler.go
index e6821ee..0f34860 100644
--- a/internal/controller/component_reconciler.go
+++ b/internal/controller/component_reconciler.go
@@ -211,6 +211,35 @@ func preserveServiceAllocatedFields(desired *corev1.ServiceSpec, existing corev1
desired.IPFamilies = nil
desired.IPFamilyPolicy = nil
}
+
+ // Preserve auto-allocated node ports. For NodePort/LoadBalancer Services the
+ // apiserver allocates a node port when the user did not pin one; carrying it
+ // over prevents a new port being allocated on every reconcile (which would
+ // churn the Service and break external access). Only meaningful for types
+ // that own node ports — leaving ClusterIP ports untouched avoids setting a
+ // node port the apiserver would reject.
+ if desired.Type == corev1.ServiceTypeNodePort || desired.Type == corev1.ServiceTypeLoadBalancer {
+ for i := range desired.Ports {
+ if desired.Ports[i].NodePort != 0 {
+ continue
+ }
+ if existingPort := matchingServicePort(existing.Ports, desired.Ports[i]); existingPort != nil {
+ desired.Ports[i].NodePort = existingPort.NodePort
+ }
+ }
+ }
+}
+
+// matchingServicePort returns the existing port that corresponds to want,
+// matched by name and protocol (the operator names its single port "http"), or
+// nil if there is no match.
+func matchingServicePort(ports []corev1.ServicePort, want corev1.ServicePort) *corev1.ServicePort {
+ for i := range ports {
+ if ports[i].Name == want.Name && ports[i].Protocol == want.Protocol {
+ return &ports[i]
+ }
+ }
+ return nil
}
// buildChecksumAnnotations builds pod annotations from checksum fields.
diff --git a/internal/controller/component_reconciler_test.go b/internal/controller/component_reconciler_test.go
index 2262363..1b98ba8 100644
--- a/internal/controller/component_reconciler_test.go
+++ b/internal/controller/component_reconciler_test.go
@@ -97,6 +97,51 @@ func TestPreserveServiceAllocatedFields(t *testing.T) {
ExternalName: "superset.example.com",
},
},
+ {
+ name: "preserves auto-allocated node port when unpinned",
+ desired: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeNodePort,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088}},
+ },
+ existing: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeNodePort,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088, NodePort: 31234}},
+ },
+ want: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeNodePort,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088, NodePort: 31234}},
+ },
+ },
+ {
+ name: "keeps user-pinned node port over existing",
+ desired: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeLoadBalancer,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088, NodePort: 30500}},
+ },
+ existing: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeLoadBalancer,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088, NodePort: 31234}},
+ },
+ want: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeLoadBalancer,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088, NodePort: 30500}},
+ },
+ },
+ {
+ name: "does not carry a node port onto a ClusterIP Service",
+ desired: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeClusterIP,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088}},
+ },
+ existing: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeNodePort,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088, NodePort: 31234}},
+ },
+ want: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeClusterIP,
+ Ports: []corev1.ServicePort{{Name: "http", Protocol: corev1.ProtocolTCP, Port: 8088}},
+ },
+ },
}
for _, tt := range tests {