From e95724c417fba707000e33b2425dc73088385f71 Mon Sep 17 00:00:00 2001 From: Kit Chan Date: Sun, 25 Oct 2026 21:13:34 -0700 Subject: [PATCH] Make integration fixtures respect per-namespace hostname ownership Since #382 a hostname is owned by the first namespace whose Ingress claims it, and routes for that hostname from other namespaces are skipped. The integration fixtures shared test.edge.com between trafficserver-test-2 and trafficserver-test-3, and test.media.com between trafficserver-test-2 and cache-test-ns, so the controller dropped test.edge.com/app1 and test.media.com/cache-test and ATS answered ERR_INVALID_URL/404. test_basic_routing_edge_app1, test_cache_app1 and test_cache_app1_beyond_ttl failed as a result. - Give test.edge.com entirely to trafficserver-test-3: add the /app1 route to its Ingress (and to the snippet variant) and remove the test.edge.com rules from every trafficserver-test-2 Ingress. - Give the caching app its own hostname, test.cache.com. The caching policy matches on URL path only, so it still applies. - Dump the controller container logs during setup so rejected host claims are visible in CI output. - Document the hostname ownership rule in the tutorial. Co-Authored-By: Claude Fable 5.1 --- docs/TUTORIAL.md | 7 +++++++ tests/data/ats-ingress-add.yaml | 12 ------------ tests/data/ats-ingress-delete.yaml | 12 ------------ tests/data/ats-ingress-snippet.yaml | 7 +++++++ tests/data/ats-ingress-update.yaml | 12 ------------ tests/data/caching-app/cache-test.yaml | 2 +- tests/data/setup/ingresses/ats-ingress-2.yaml | 7 +++++++ tests/data/setup/ingresses/ats-ingress.yaml | 18 ------------------ tests/suite/test_ingress.py | 6 ++++-- 9 files changed, 26 insertions(+), 57 deletions(-) diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 3ca2adb1..676b64c3 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -21,6 +21,7 @@ - [Usage](#usage) - [ConfigMap](#configmap) - [Namespaces for Ingresses](#namespaces-for-ingresses) + - [Hostname Ownership](#hostname-ownership) - [Snippet](#snippet) - [Ingress Class](#ingress-class) - [Customizing Logging and TLS](#customizing-logging-and-tls) @@ -51,6 +52,12 @@ Putting the extension in the pod spec keeps the decision with the cluster operat You can specifiy the list of namespaces to look for ingress object by providing an environment variable called `INGRESS_NS`. The default is `all`, which tells the controller to look for ingress objects in all namespaces. Alternatively you can provide a comma-separated list of namespaces for the controller to look for ingresses. Similarly you can specifiy a comma-separated list of namespaces to ignore while the controller is looking for ingresses by providing `INGRESS_IGNORE_NS`. +#### Hostname Ownership + +Every hostname in an ingress rule (and `*` for rules without a host or for a default backend) is owned by a single namespace. The first ingress object the controller sees for a hostname claims it for its namespace; ingress objects in other namespaces that reference the same hostname have the routes for that hostname skipped, and the controller logs a message such as `Host "example.com" in Ingress other-ns/my-ingress is already owned by another namespace; skipping its routes`. Routes for hostnames that are not in conflict are still applied. A hostname is released once every ingress object in the owning namespace that references it has been deleted. + +This prevents a tenant from attaching its own backend to a path under another tenant's hostname. It also means that the paths of one hostname cannot be split across ingress objects in different namespaces; if several teams need to route the same hostname, put their ingress objects in the same namespace, or give each namespace its own hostname. Ingress objects in the same namespace can share a hostname freely. There is no setting that allows a hostname to be shared across namespaces. + #### Snippet You can attach [ATS lua script](https://docs.trafficserver.apache.org/en/9.2.x/admin-guide/plugins/lua.en.html) to an ingress object and ATS will execute it for requests matching the routing rules defined in the ingress object. This can be enabled by providing an environment variable called `SNIPPET` in the deployment. diff --git a/tests/data/ats-ingress-add.yaml b/tests/data/ats-ingress-add.yaml index 4474e518..64e4833a 100644 --- a/tests/data/ats-ingress-add.yaml +++ b/tests/data/ats-ingress-add.yaml @@ -37,15 +37,3 @@ spec: name: appsvc1 port: number: 8080 - - - host: test.edge.com - http: - paths: - - path: /app1 - pathType: Exact - backend: - service: - name: appsvc1 - port: - number: 8080 - diff --git a/tests/data/ats-ingress-delete.yaml b/tests/data/ats-ingress-delete.yaml index 7570f38e..0afeccfc 100644 --- a/tests/data/ats-ingress-delete.yaml +++ b/tests/data/ats-ingress-delete.yaml @@ -30,15 +30,3 @@ spec: name: appsvc1 port: number: 8080 - - - host: test.edge.com - http: - paths: - - path: /app1 - pathType: Exact - backend: - service: - name: appsvc1 - port: - number: 8080 - diff --git a/tests/data/ats-ingress-snippet.yaml b/tests/data/ats-ingress-snippet.yaml index 5695c2cd..af747ff6 100644 --- a/tests/data/ats-ingress-snippet.yaml +++ b/tests/data/ats-ingress-snippet.yaml @@ -30,6 +30,13 @@ spec: - host: test.edge.com http: paths: + - path: /app1 + pathType: Exact + backend: + service: + name: appsvc1 + port: + number: 8080 - path: /app2 pathType: Exact backend: diff --git a/tests/data/ats-ingress-update.yaml b/tests/data/ats-ingress-update.yaml index 205fed46..a98ad80f 100644 --- a/tests/data/ats-ingress-update.yaml +++ b/tests/data/ats-ingress-update.yaml @@ -37,15 +37,3 @@ spec: name: appsvc1 port: number: 8080 - - - host: test.edge.com - http: - paths: - - path: /app1 - pathType: Exact - backend: - service: - name: appsvc1 - port: - number: 8080 - diff --git a/tests/data/caching-app/cache-test.yaml b/tests/data/caching-app/cache-test.yaml index 3982a99a..1c247e9a 100644 --- a/tests/data/caching-app/cache-test.yaml +++ b/tests/data/caching-app/cache-test.yaml @@ -57,7 +57,7 @@ metadata: spec: #ingressClassName: ats rules: - - host: test.media.com + - host: test.cache.com http: paths: - path: /cache-test diff --git a/tests/data/setup/ingresses/ats-ingress-2.yaml b/tests/data/setup/ingresses/ats-ingress-2.yaml index 43fbdde5..f3637d89 100644 --- a/tests/data/setup/ingresses/ats-ingress-2.yaml +++ b/tests/data/setup/ingresses/ats-ingress-2.yaml @@ -41,6 +41,13 @@ spec: - host: test.edge.com http: paths: + - path: /app1 + pathType: Exact + backend: + service: + name: appsvc1 + port: + number: 8080 - path: /app2 pathType: Exact backend: diff --git a/tests/data/setup/ingresses/ats-ingress.yaml b/tests/data/setup/ingresses/ats-ingress.yaml index 12e39717..7dea81b1 100644 --- a/tests/data/setup/ingresses/ats-ingress.yaml +++ b/tests/data/setup/ingresses/ats-ingress.yaml @@ -46,21 +46,3 @@ spec: name: appsvc2 port: number: 8080 - - - host: test.edge.com - http: - paths: - - path: /app1 - pathType: Exact - backend: - service: - name: appsvc1 - port: - number: 8080 - - path: /app2 - pathType: Exact - backend: - service: - name: appsvc2 - port: - number: 8080 diff --git a/tests/suite/test_ingress.py b/tests/suite/test_ingress.py index 13a5993e..891c0501 100644 --- a/tests/suite/test_ingress.py +++ b/tests/suite/test_ingress.py @@ -195,6 +195,8 @@ def setup_module(module): misc_command('kubectl get all -A') misc_command('kubectl get pod -A -o wide') misc_command('kubectl logs $(kubectl get pod -n trafficserver-test-2 -o name | head -1) -n trafficserver-test-2') + # controller logs: shows e.g. host claims rejected by tenant isolation + misc_command('kubectl logs $(kubectl get pod -n trafficserver-test -o name) -n trafficserver-test -c trafficserver-test') misc_command('kubectl exec $(kubectl get pod -n trafficserver-test-2 -o name | head -1) -n trafficserver-test-2 -- ps auxxx') misc_command('kubectl exec $(kubectl get pod -n trafficserver-test-2 -o name | head -1) -n trafficserver-test-2 -- curl -v localhost:8080/app1') misc_command('kubectl exec $(kubectl get pod -n trafficserver-test-2 -o name | head -1) -n trafficserver-test-2 -- curl -v $(kubectl get pod -n trafficserver-test-2 -o jsonpath={.items[0].status.podIP}):8080/app1') @@ -345,7 +347,7 @@ def test_basic_routing_edge_app2_https(self, minikubeip): assert ' '.join(resp.text.split()) == get_expected_response_app2() def test_cache_app1(self, minikubeip): - command = f'curl -i -v -H "Host: test.media.com" http://{minikubeip}:30080/cache-test' + command = f'curl -i -v -H "Host: test.cache.com" http://{minikubeip}:30080/cache-test' response_1 = subprocess.run(command, shell=True, capture_output=True, text=True) response1 = response_1.stdout.strip() response1_list = response1.split('\n') @@ -391,7 +393,7 @@ def test_cache_app1_beyond_ttl(self, minikubeip): # waiting for cache from previous test case to expire time.sleep(20) - command = f'curl -i -v -H "Host: test.media.com" http://{minikubeip}:30080/cache-test' + command = f'curl -i -v -H "Host: test.cache.com" http://{minikubeip}:30080/cache-test' response_1 = subprocess.run(command, shell=True, capture_output=True, text=True) response1 = response_1.stdout.strip() response1_list = response1.split('\n')