Repository navigation
feat: add TrafficExtension for multi-tenancy #502
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
Merged
NohaIhab
merged 11 commits into
feat/add-multi-tenancy
from
KF-8906/feat/add-traffic-extenstion-multitenancy
Sep 29, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
ea6bfa2
feat: add custom authentication for multi-tenancy (#485)
MattiaSarti 3a47667
feat: remove NodePort config (#486)
MattiaSarti 361e1df
feat: allow user aliasing (#487)
MattiaSarti 3065fc0
feat: update authpolicies for multi-tenancy (#495)
NohaIhab 8b33b23
feat: add mlflow_client provider logic (#494)
MattiaSarti 634d424
feat: add TrafficExtension for multi-tenancy
NohaIhab 62db39a
Merge feat/add-multi-tenancy into traffic extension
NohaIhab 72db660
review: add unit test for relation broken
NohaIhab 9a55701
review: use tenacity for wait
NohaIhab ff3a36c
review: run curl requests from a reusable CurlPod helper
NohaIhab 3c337c2
test: double postgresql wait timeout in integration tests
NohaIhab File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| apiVersion: extensions.istio.io/v1alpha1 | ||
| kind: TrafficExtension | ||
| metadata: | ||
| name: {{ name }} | ||
| namespace: {{ namespace }} | ||
| spec: | ||
| # a waypoint only honours targetRefs, and a Service targetRef is waypoint-only: the extension | ||
| # therefore runs solely for traffic the waypoint handles on behalf of the tracking server. | ||
| targetRefs: | ||
| - kind: Service | ||
| group: "" | ||
| name: {{ service_name }} | ||
| # before the authorization filters, so that policies see the stamped identity | ||
| phase: AUTHN | ||
| lua: | ||
| inlineCode: | | ||
| local IDENTITY_HEADER = "{{ identity_header_name }}" | ||
|
|
||
| local function source_namespace(request_handle) | ||
| -- mTLS terminates on the outer HBONE listener; Istio propagates its verified peer URI SAN. | ||
| local principal = request_handle:streamInfo():filterState():get("io.istio.peer_principal") | ||
| if principal == nil then | ||
| return nil | ||
| end | ||
| -- spiffe://<trust-domain>/ns/<namespace>/sa/<service-account> | ||
| return string.match(principal, "^spiffe://[^/]+/ns/([^/]+)/sa/") | ||
| end | ||
|
|
||
| function envoy_on_request(request_handle) | ||
| -- any client-supplied user-ID header is dropped first, so that it cannot be spoofed: | ||
| request_handle:headers():remove(IDENTITY_HEADER) | ||
|
|
||
| local namespace = source_namespace(request_handle) | ||
| if namespace == nil then | ||
| -- no verified mTLS peer (plaintext or out of the mesh): fail closed | ||
| request_handle:respond( | ||
| {[":status"] = "401"}, "Unauthenticated: no verified source namespace" | ||
| ) | ||
| return | ||
| end | ||
|
|
||
| request_handle:headers():add(IDENTITY_HEADER, namespace) | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # Copyright 2026 Canonical Ltd. | ||
| # See LICENSE file for licensing details. | ||
|
|
||
| """Shared helpers for the integration tests.""" | ||
|
|
||
| import logging | ||
| import subprocess | ||
| from random import choices | ||
| from string import ascii_lowercase | ||
|
|
||
| from tenacity import Retrying, retry_if_exception_type, stop_after_delay, wait_fixed | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class CurlPod: | ||
| """A long-running pod issuing curl requests from within a given namespace. | ||
|
|
||
| Use it as a context manager, so the pod is created on entry and deleted on exit. | ||
| """ | ||
|
|
||
| IMAGE = "curlimages/curl:8.8.0" | ||
|
|
||
| def __init__(self, namespace: str): | ||
| self.namespace = namespace | ||
| self.name = f"curl-{''.join(choices(ascii_lowercase, k=6))}" | ||
|
|
||
| def __enter__(self) -> "CurlPod": | ||
| self.create() | ||
| return self | ||
|
|
||
| def __exit__(self, *exc) -> None: | ||
| self.delete() | ||
|
|
||
| def create(self) -> None: | ||
| """Create the pod and wait until it is ready, deleting it if it never gets ready.""" | ||
| # a fresh namespace's default service account may not exist yet, rejecting the pod: | ||
| for attempt in Retrying( | ||
| stop=stop_after_delay(60), | ||
| wait=wait_fixed(2), | ||
| retry=retry_if_exception_type(subprocess.CalledProcessError), | ||
| reraise=True, | ||
| ): | ||
| with attempt: | ||
| self._kubectl( | ||
| "run", | ||
| self.name, | ||
| f"--image={self.IMAGE}", | ||
| "--restart=Never", | ||
| "--command", | ||
| "--", | ||
| "sleep", | ||
| "86400", | ||
| ) | ||
| try: | ||
| self._kubectl("wait", f"pod/{self.name}", "--for=condition=Ready", "--timeout=180s") | ||
| except subprocess.CalledProcessError: | ||
| self.delete() | ||
| raise | ||
|
|
||
| def delete(self) -> None: | ||
| """Delete the pod, tolerating it being already gone.""" | ||
| self._kubectl( | ||
| "delete", "pod", self.name, "--ignore-not-found", "--wait=false", check=False | ||
| ) | ||
|
|
||
| def curl(self, *args: str, check: bool = True) -> subprocess.CompletedProcess: | ||
| """Run curl in the pod; `kubectl exec` propagates curl's own exit code.""" | ||
| result = self._kubectl("exec", self.name, "--", "curl", *args, check=False) | ||
| logger.info( | ||
| "curl from pod %s/%s: exit_code=%s stdout=%r stderr=%r", | ||
| self.namespace, | ||
| self.name, | ||
| result.returncode, | ||
| result.stdout, | ||
| result.stderr, | ||
| ) | ||
| if check: | ||
| result.check_returncode() | ||
| return result | ||
|
|
||
| def _kubectl(self, *args: str, check: bool = True) -> subprocess.CompletedProcess: | ||
| return subprocess.run( | ||
| ["kubectl", "-n", self.namespace, *args], | ||
| check=check, | ||
| capture_output=True, | ||
| text=True, | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.