You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2026/10/18 17:48:02 UTC
(superset-kubernetes-operator) branch main updated: fix(networking): route a hostless Ingress to every host (#461)
This is an automated email from the ASF dual-hosted git repository.
villebro 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 19debd1 fix(networking): route a hostless Ingress to every host (#461)
19debd1 is described below
commit 19debd1954e7e2b3a1e585bf5801801dc78ec8c6
Author: Younsung Lee <cy...@kakao.com>
AuthorDate: Mon Oct 19 02:47:56 2026 +0900
fix(networking): route a hostless Ingress to every host (#461)
When spec.networking.ingress has neither host nor hosts, for example
only className: alb, the operator built an Ingress with no rules and
no default backend. The Kubernetes API server rejects that object with
"either `defaultBackend` or `rules` must be specified", so every
reconcile failed and no Ingress was ever created. Both fields are
optional in the CRD, and running an Ingress without a hostname is a
common setup with AWS Load Balancer Controller and ingress-nginx.
The operator now emits a single rule with an empty host when no host
is given. An empty host matches every hostname, and the rule carries
the same per-component path fan-out that a host without explicit
paths already gets. Specs that set host or hosts render exactly as
before.
The existing unit tests use a fake client that does not run API server
validation, which is why this was not caught. This change adds a unit
test for the hostless spec and extends the e2e journey to drop the
Ingress host on a real cluster and check the hostless rule. The
networking guide now documents the hostless behavior.
Signed-off-by: younsl <cy...@kakao.com>
---
docs/user-guide/networking-and-monitoring.md | 2 ++
internal/controller/networking.go | 4 +++
internal/controller/networking_test.go | 47 ++++++++++++++++++++++++++++
test/e2e/superset_journey_test.go | 7 +++++
4 files changed, 60 insertions(+)
diff --git a/docs/user-guide/networking-and-monitoring.md b/docs/user-guide/networking-and-monitoring.md
index 80f2b58..5a4ddcc 100644
--- a/docs/user-guide/networking-and-monitoring.md
+++ b/docs/user-guide/networking-and-monitoring.md
@@ -88,6 +88,8 @@ spec:
host: superset.example.com # no explicit paths -> all components routed by path
```
+Without `host` or `hosts`, the operator emits a single rule with no host. It matches every hostname and carries the same per-component fan-out.
+
A host **with** explicit `paths` is treated as a user-controlled override and those paths route to the web server only — use this when you want full control of the path rules for a host:
```yaml
diff --git a/internal/controller/networking.go b/internal/controller/networking.go
index f02a7b3..408e2a2 100644
--- a/internal/controller/networking.go
+++ b/internal/controller/networking.go
@@ -382,6 +382,10 @@ func (r *SupersetReconciler) reconcileIngress(ctx context.Context, superset *sup
{Host: ing.Host},
}
}
+ // The apiserver rejects an Ingress with neither rules nor a default backend.
+ if len(hosts) == 0 {
+ hosts = []supersetv1alpha1.IngressHost{{}}
+ }
// Build rules from hosts. A host without explicit Paths gets the full
// per-component fan-out (web "/" plus /flower, /mcp, /ws for present
diff --git a/internal/controller/networking_test.go b/internal/controller/networking_test.go
index 4cd28ff..865493d 100644
--- a/internal/controller/networking_test.go
+++ b/internal/controller/networking_test.go
@@ -719,6 +719,53 @@ func TestReconcileIngress_HostFallback(t *testing.T) {
}
}
+func TestReconcileIngress_HostlessFanout(t *testing.T) {
+ scheme := testScheme(t)
+
+ superset := &supersetv1alpha1.Superset{
+ Name: "test", Namespace: "default", UID: "uid-1",
+ Spec: supersetv1alpha1.SupersetSpec{
+ Image: supersetv1alpha1.ImageSpec{Repository: "apache/superset", Tag: "latest"},
+ WebServer: &supersetv1alpha1.WebServerComponentSpec{},
+ McpServer: &supersetv1alpha1.McpServerComponentSpec{},
+ Lifecycle: &supersetv1alpha1.LifecycleSpec{Disabled: new(true)},
+ Networking: &supersetv1alpha1.NetworkingSpec{
+ Ingress: &supersetv1alpha1.IngressSpec{ClassName: new("alb")},
+ },
+ },
+ }
+
+ c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(superset).Build()
+ r := &SupersetReconciler{Client: c, Scheme: scheme, Recorder: events.NewFakeRecorder(10)}
+
+ if err := r.reconcileIngress(context.Background(), superset); err != nil {
+ t.Fatalf("reconcileIngress: %v", err)
+ }
+
+ ingress := &networkingv1.Ingress{}
+ if err := c.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: "default"}, ingress); err != nil {
+ t.Fatalf("expected Ingress: %v", err)
+ }
+
+ if len(ingress.Spec.Rules) != 1 {
+ t.Fatalf("expected 1 hostless rule, got %d", len(ingress.Spec.Rules))
+ }
+ if ingress.Spec.Rules[0].Host != "" {
+ t.Errorf("expected empty host, got %q", ingress.Spec.Rules[0].Host)
+ }
+ got := map[string]string{}
+ for _, p := range ingress.Spec.Rules[0].HTTP.Paths {
+ got[p.Path] = p.Backend.Service.Name
+ }
+ want := map[string]string{
+ "/mcp": "test-mcp-server",
+ "/": "test-web-server",
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("ingress paths = %+v, want %+v", got, want)
+ }
+}
+
func TestReconcileIngress_WithTLS(t *testing.T) {
scheme := testScheme(t)
diff --git a/test/e2e/superset_journey_test.go b/test/e2e/superset_journey_test.go
index 4cc4a7a..784fd60 100644
--- a/test/e2e/superset_journey_test.go
+++ b/test/e2e/superset_journey_test.go
@@ -242,6 +242,13 @@ spec:
expectJSONPathContains("ingress", crName, "{.spec.rules[0].http.paths[*].path}", path)
}
+ By("dropping the Ingress host")
+ patchSuperset(crName, "json", `[{"op":"remove","path":"/spec/networking/ingress/host"}]`)
+ expectJSONPath("ingress", crName, "{.spec.rules[0].host}", "", time.Minute)
+ for _, path := range []string{"/ws", "/mcp", "/flower"} {
+ expectJSONPathContains("ingress", crName, "{.spec.rules[0].http.paths[*].path}", path)
+ }
+
By("switching networking from Ingress to Gateway")
patchSuperset(crName, "json", `[
{"op":"remove","path":"/spec/networking/ingress"},