Skip to content

Commit c2dbd4e

Browse files
HumairAKmprahl
authored andcommitted
add client side kubernetes auth plugin
This mlflow plugin is intended to be installed and used by the client, when headers are missing workspace/tokens it will search for these values in the pod's filesystem (when running in a pod), or use the local kubeconfig. The plugin is intentionally added as part of the mlflow package to avoid having to package it separately. This is something we may do in the future. (cherry picked from commit 40260db) Signed-off-by: Humair Khan <HumairAK@users.noreply.github.com>
1 parent 73c98dc commit c2dbd4e

File tree

8 files changed

+675
-0
lines changed

8 files changed

+675
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Request Auth Provider Plugin
2+
3+
This plugin system allows you to add custom authentication to MLflow tracking requests.
4+
5+
## Usage
6+
7+
Set the `MLFLOW_TRACKING_AUTH` environment variable to the name of your auth provider:
8+
9+
```bash
10+
export MLFLOW_TRACKING_AUTH=kubernetes
11+
```
12+
13+
MLflow will then use that provider to add authentication headers to all outgoing tracking requests.
14+
15+
## Built-in Providers
16+
17+
### Kubernetes
18+
19+
Adds authentication headers for Kubernetes environments.
20+
21+
```bash
22+
export MLFLOW_TRACKING_AUTH=kubernetes
23+
```
24+
25+
This provider automatically adds:
26+
27+
- `X-MLFLOW-WORKSPACE`: The Kubernetes namespace (from service account or kubeconfig)
28+
- `Authorization`: Bearer token (from service account or kubeconfig)
29+
30+
It first checks for in-cluster service account credentials, then falls back to your local kubeconfig.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""Request auth provider for Kubernetes environments.
2+
3+
This provider automatically adds workspace and authorization headers when running
4+
in Kubernetes environments or when kubeconfig is available.
5+
6+
To use this provider, set the environment variable:
7+
MLFLOW_TRACKING_AUTH=kubernetes
8+
"""
9+
10+
import logging
11+
from pathlib import Path
12+
13+
from cachetools import TTLCache
14+
from kubernetes import client, config
15+
16+
from mlflow.exceptions import MlflowException
17+
from mlflow.tracking.request_auth.abstract_request_auth_provider import RequestAuthProvider
18+
19+
# Cache for file reads (1 minute TTL)
20+
_FILE_CACHE_TTL = 60
21+
_file_cache: TTLCache = TTLCache(maxsize=10, ttl=_FILE_CACHE_TTL)
22+
23+
_logger = logging.getLogger(__name__)
24+
25+
# Kubernetes service account paths
26+
_SERVICE_ACCOUNT_NAMESPACE_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
27+
_SERVICE_ACCOUNT_TOKEN_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
28+
29+
# Header names
30+
WORKSPACE_HEADER_NAME = "X-MLFLOW-WORKSPACE"
31+
AUTHORIZATION_HEADER_NAME = "Authorization"
32+
33+
34+
def _read_file_if_exists(path: Path) -> str | None:
35+
"""Read a file and return its contents stripped, or None if it doesn't exist."""
36+
cache_key = str(path)
37+
if cache_key in _file_cache:
38+
return _file_cache[cache_key]
39+
40+
result = None
41+
try:
42+
if path.exists():
43+
result = path.read_text().strip() or None
44+
except (OSError, PermissionError) as e:
45+
_logger.debug("Could not read file %s: %s", path, e)
46+
47+
_file_cache[cache_key] = result
48+
return result
49+
50+
51+
def _get_credentials_from_service_account() -> tuple[str, str] | None:
52+
"""Get namespace and token from mounted service account files.
53+
54+
Returns:
55+
Tuple of (namespace, token) if both are available, None otherwise.
56+
"""
57+
namespace = _read_file_if_exists(_SERVICE_ACCOUNT_NAMESPACE_PATH)
58+
token = _read_file_if_exists(_SERVICE_ACCOUNT_TOKEN_PATH)
59+
60+
if namespace and token:
61+
return namespace, f"Bearer {token}"
62+
return None
63+
64+
65+
def _get_credentials_from_kubeconfig() -> tuple[str, str] | None:
66+
"""Get namespace and token from kubeconfig.
67+
68+
Uses ApiClient so kubeconfig exec auth is resolved when possible
69+
(EKS, GKE, AKS, OpenShift, OIDC).
70+
71+
Returns:
72+
Tuple of (namespace, token) if both are available, None otherwise.
73+
"""
74+
config.load_kube_config()
75+
76+
# Get namespace from context
77+
_, active_context = config.list_kube_config_contexts()
78+
if not active_context:
79+
return None
80+
81+
namespace = active_context.get("context", {}).get("namespace", "").strip() or None
82+
if not namespace:
83+
return None
84+
85+
# Get token from ApiClient
86+
api_client = client.ApiClient()
87+
token = None
88+
89+
# 1) Try default_headers first (where most auth flows put the resolved token)
90+
auth = api_client.default_headers.get("Authorization") or api_client.default_headers.get(
91+
"authorization"
92+
)
93+
if isinstance(auth, str) and auth.lower().startswith("bearer "):
94+
token = auth[7:].strip()
95+
96+
# 2) Fallback: configuration.api_key (some versions/auth flows store it here)
97+
if not token:
98+
api_key = api_client.configuration.api_key.get("authorization")
99+
if isinstance(api_key, str):
100+
token = api_key.strip()
101+
if token.lower().startswith("bearer "):
102+
token = token[7:].strip()
103+
104+
if not token:
105+
return None
106+
107+
return namespace, f"Bearer {token}"
108+
109+
110+
def _get_credentials() -> tuple[str, str] | None:
111+
"""Get workspace and authorization credentials.
112+
113+
Tries service account files first, then falls back to kubeconfig.
114+
Both values must come from the same source for consistency.
115+
116+
Returns:
117+
Tuple of (namespace, authorization) if available, None otherwise.
118+
"""
119+
# Try service account files first (running in a pod)
120+
if creds := _get_credentials_from_service_account():
121+
return creds
122+
123+
# Fallback to kubeconfig
124+
return _get_credentials_from_kubeconfig()
125+
126+
127+
class KubernetesAuth:
128+
"""Custom authentication class for Kubernetes environments.
129+
130+
This class is callable and will be invoked by the requests library
131+
to add authentication headers to each request.
132+
"""
133+
134+
def __call__(self, request):
135+
"""Add Kubernetes authentication headers to the request.
136+
137+
Args:
138+
request: The prepared request object from the requests library.
139+
140+
Returns:
141+
The modified request object with authentication headers.
142+
143+
Raises:
144+
MlflowException: If workspace or authorization cannot be determined.
145+
"""
146+
# Skip if both headers are already set
147+
if (
148+
WORKSPACE_HEADER_NAME in request.headers
149+
and AUTHORIZATION_HEADER_NAME in request.headers
150+
):
151+
return request
152+
153+
credentials = _get_credentials()
154+
if not credentials:
155+
raise MlflowException(
156+
"Could not determine Kubernetes credentials. "
157+
"Ensure you are running in a Kubernetes pod with a service account "
158+
"or have a valid kubeconfig with a namespace and credentials set."
159+
)
160+
161+
namespace, authorization = credentials
162+
163+
if WORKSPACE_HEADER_NAME not in request.headers:
164+
request.headers[WORKSPACE_HEADER_NAME] = namespace
165+
166+
if AUTHORIZATION_HEADER_NAME not in request.headers:
167+
request.headers[AUTHORIZATION_HEADER_NAME] = authorization
168+
169+
return request
170+
171+
172+
class KubernetesRequestAuthProvider(RequestAuthProvider):
173+
"""Provides authentication for Kubernetes environments.
174+
175+
This provider adds headers based on Kubernetes environment:
176+
- X-MLFLOW-WORKSPACE: Set from service account namespace file or kubeconfig context
177+
- Authorization: Set from service account token file or kubeconfig credentials
178+
179+
Credentials are sourced consistently - either both from mounted service account
180+
files (when running in a pod) or both from kubeconfig (when running locally).
181+
182+
To enable this provider, set:
183+
MLFLOW_TRACKING_AUTH=kubernetes
184+
"""
185+
186+
def get_name(self) -> str:
187+
return "kubernetes"
188+
189+
def get_auth(self):
190+
return KubernetesAuth()

mlflow/tracking/request_auth/registry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import warnings
22

3+
from mlflow.tracking.request_auth.kubernetes_request_auth_provider import (
4+
KubernetesRequestAuthProvider,
5+
)
36
from mlflow.utils.plugins import get_entry_points
47

58
REQUEST_AUTH_PROVIDER_ENTRYPOINT = "mlflow.request_auth_provider"
@@ -29,6 +32,7 @@ def __iter__(self):
2932

3033

3134
_request_auth_provider_registry = RequestAuthProviderRegistry()
35+
_request_auth_provider_registry.register(KubernetesRequestAuthProvider)
3236
_request_auth_provider_registry.register_entrypoints()
3337

3438

pyproject.release.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies = [
3737
"graphene<4",
3838
"gunicorn<24; platform_system != 'Windows'",
3939
"huey<3,>=2.5.0",
40+
"kubernetes<35",
4041
"matplotlib<4",
4142
"numpy<3",
4243
"pandas<3",

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies = [
4242
"gunicorn<24; platform_system != 'Windows'",
4343
"huey<3,>=2.5.0",
4444
"importlib_metadata<9,>=3.7.0,!=4.7.0",
45+
"kubernetes<35",
4546
"matplotlib<4",
4647
"numpy<3",
4748
"opentelemetry-api<3,>=1.9.0",

requirements/core-requirements.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,7 @@ huey:
8080
pip_release: huey
8181
minimum: "2.5.0"
8282
max_major_version: 2
83+
84+
kubernetes:
85+
pip_release: kubernetes
86+
max_major_version: 34

0 commit comments

Comments
 (0)