Skip to content

Commit beec2f2

Browse files
authored
[SDK] Fix setting token file name (mlrun#9150)
### 📝 Description To ensure token file names can be explicitly set and (correctly) auto set for runtimes / local development it was required to remove the ~/.igz.yml from config.py and set it according to running env (k8s, jupyter, local). This fixes Iguazio v4 OAuth token file auto-initialization to correctly distinguish between Kubernetes runtime environments and Jupyter environments. Previously, the token file was always overwritten to the k8s secret path when running inside Kubernetes, which broke authentication for Jupyter environments. --- ### 🛠️ Changes Made - Modified the token file initialization logic to check for Jupyter environment (`JPY_SESSION_NAME`) before overriding to k8s secret path - Changed default `token_file` config from `~/.igz.yml` to empty string, allowing dynamic initialization based on runtime context - Updated docstring in `sync_secret_tokens` to reflect the new behavior --- ### ✅ Checklist - [ ] I updated the documentation (if applicable) - [x] I have tested the changes in this PR - [ ] I confirmed whether my changes are covered by system tests - [ ] If yes, I ran all relevant system tests and ensured they passed before submitting this PR - [ ] I updated existing system tests and/or added new ones if needed to cover my changes - [ ] If I introduced a deprecation: - [ ] I followed the [Deprecation Guidelines](./DEPRECATION.md) - [ ] I updated the relevant Jira ticket for documentation --- ### 🧪 Testing - UT - igz4 envs --- ### 🔗 References - Ticket link: - Design docs links: - External links: --- ### 🚨 Breaking Changes? - [ ] Yes (explain below) - [x] No <!-- If yes, describe what needs to be changed downstream: --> --- ### 🔍️ Additional Notes <!-- Anything else reviewers should know (follow-up tasks, known issues, affected areas etc.). --> <!-- ### 📸 Screenshots / Logs -->
1 parent 8c7776d commit beec2f2

File tree

4 files changed

+111
-11
lines changed

4 files changed

+111
-11
lines changed

mlrun/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,7 +885,9 @@
885885
"enabled": False,
886886
"request_timeout": 5,
887887
"refresh_threshold": 0.75,
888-
"token_file": "~/.igz.yml",
888+
# Default is empty. automatically set based on configuration (end client vs jupyter vs runtime, etc)
889+
# can be set manually set using envvars
890+
"token_file": "",
889891
# Default is empty because if set, searches for the specific token name in the file, if empty, it will look
890892
# for a token named "default", if "default" does not exist, it will use the first token in the file
891893
"token_name": "",

mlrun/db/httpdb.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,23 @@ def connect(self, secrets=None) -> typing.Self:
648648
config.httpdb.authentication.mode
649649
== mlrun.common.types.AuthenticationMode.IGUAZIO_V4.value
650650
):
651+
if not config.auth_with_oauth_token.token_file:
652+
user_token_file = os.path.expanduser("~/.igz.yml")
653+
654+
# runtimes
655+
# TODO: change to os.getenv("MLRUN_RUNTIME_KIND")
656+
# when https://github.com/mlrun/mlrun/pull/9121 is done.
657+
if (
658+
mlrun.k8s_utils.is_running_inside_kubernetes_cluster()
659+
and not os.environ.get("JPY_SESSION_NAME")
660+
):
661+
user_token_file = os.path.join(
662+
mlrun.common.constants.MLRUN_JOB_AUTH_SECRET_PATH,
663+
mlrun.common.constants.MLRUN_JOB_AUTH_SECRET_FILE,
664+
)
665+
666+
config.auth_with_oauth_token.token_file = user_token_file
667+
651668
# if running inside kubernetes, use the internal endpoint, otherwise use the external endpoint
652669
if mlrun.k8s_utils.is_running_inside_kubernetes_cluster():
653670
config.auth_token_endpoint = server_cfg.get(
@@ -660,14 +677,6 @@ def connect(self, secrets=None) -> typing.Self:
660677

661678
config.auth_with_oauth_token.enabled = True
662679

663-
# TODO: change to os.getenv("MLRUN_RUNTIME_KIND") when https://github.com/mlrun/mlrun/pull/9121
664-
# is merged. The reason we can't do it for all k8s pods is dev-envs like jupyter.
665-
if mlrun.k8s_utils.is_running_inside_kubernetes_cluster():
666-
config.auth_with_oauth_token.token_file = os.path.join(
667-
mlrun.common.constants.MLRUN_JOB_AUTH_SECRET_PATH,
668-
mlrun.common.constants.MLRUN_JOB_AUTH_SECRET_FILE,
669-
)
670-
671680
except Exception as exc:
672681
logger.warning(
673682
"Failed syncing config from server",

mlrun/secrets.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,7 @@ def sync_secret_tokens() -> None:
262262
Synchronize local secret tokens with the backend.
263263
264264
This function:
265-
1. Reads the local token file (default: ~/.igz.yml, configurable via
266-
`mlrun.mlconf.auth_with_oauth_token.token_file`).
265+
1. Reads the local token file (defaults to `mlrun.mlconf.auth_with_oauth_token.token_file` value).
267266
2. Validates its content and converts validated tokens into `SecretToken` objects.
268267
3. Uploads the tokens to the backend.
269268
4. Logs a warning if any tokens were updated on the backend due to newer

tests/rundb/test_httpdb.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,96 @@ def test_iguazio_v4_oauth_config_uses_internal_endpoint_in_cluster(
579579
assert mlrun.mlconf.auth_with_oauth_token.token_file == expected_token_file
580580

581581

582+
@pytest.mark.parametrize(
583+
"is_k8s,has_jupyter_env,preconfigured_token_file,expected_token_file_suffix",
584+
[
585+
# Running in k8s (not Jupyter) - should use k8s secret path
586+
(True, False, None, "/var/mlrun-secrets/auth/.igz.yml"),
587+
# Running in k8s under Jupyter - should use user home token file
588+
(True, True, None, "~/.igz.yml"),
589+
# Running locally (not k8s) - should use user home token file
590+
(False, False, None, "~/.igz.yml"),
591+
# Token file already configured - should not change it
592+
(True, False, "/custom/path/token.yml", "/custom/path/token.yml"),
593+
],
594+
)
595+
def test_iguazio_v4_oauth_token_file_auto_initialization(
596+
requests_mock: requests_mock_package.Mocker,
597+
monkeypatch,
598+
is_k8s,
599+
has_jupyter_env,
600+
preconfigured_token_file,
601+
expected_token_file_suffix,
602+
):
603+
"""
604+
Test that token_file is correctly auto-initialized based on environment:
605+
- In k8s (not Jupyter): uses /var/mlrun-secrets/auth/.igz.yml
606+
- In k8s under Jupyter: uses ~/.igz.yml
607+
- Locally: uses ~/.igz.yml
608+
- Pre-configured: preserves the existing value
609+
"""
610+
611+
mlrun.mlconf.auth_with_oauth_token.token_file = preconfigured_token_file
612+
613+
external_token_endpoint = "https://dashboard.default-tenant.app.example.com/api/v1/authentication/refresh-access-token"
614+
internal_token_endpoint = "https://dashboard.default-tenant.svc.cluster.local/api/v1/authentication/refresh-access-token"
615+
616+
iat = int(time.time())
617+
exp = iat + 3600
618+
jwt_token = _encode_jwt({"iat": iat, "exp": exp})
619+
620+
# Mock token endpoint - use internal if k8s, external otherwise
621+
token_endpoint = internal_token_endpoint if is_k8s else external_token_endpoint
622+
requests_mock.post(token_endpoint, json={"spec": {"accessToken": jwt_token}})
623+
624+
server_cfg = {
625+
"version": mlrun.mlconf.version,
626+
"authentication_mode": mlrun.common.types.AuthenticationMode.IGUAZIO_V4.value,
627+
"oauth_enabled": True,
628+
"oauth_external_token_endpoint": external_token_endpoint,
629+
"oauth_internal_token_endpoint": internal_token_endpoint,
630+
}
631+
632+
# Mock kubernetes detection
633+
monkeypatch.setattr(
634+
mlrun.k8s_utils, "is_running_inside_kubernetes_cluster", lambda: is_k8s
635+
)
636+
637+
# Set or clear Jupyter environment variable
638+
if has_jupyter_env:
639+
monkeypatch.setenv("JPY_SESSION_NAME", "jupyter-session")
640+
else:
641+
monkeypatch.delenv("JPY_SESSION_NAME", raising=False)
642+
643+
# Mock secret tokens reading for k8s environments
644+
monkeypatch.setattr(
645+
mlrun.auth.utils,
646+
"read_secret_tokens_file",
647+
lambda raise_on_error: {
648+
"secretTokens": [{"name": "default", "token": "offline"}]
649+
},
650+
)
651+
652+
# Determine the expected token file path
653+
if preconfigured_token_file:
654+
expected_token_file = preconfigured_token_file
655+
elif expected_token_file_suffix == "~/.igz.yml":
656+
expected_token_file = os.path.expanduser("~/.igz.yml")
657+
else:
658+
expected_token_file = expected_token_file_suffix
659+
660+
with patch.object(mlrun.auth.utils, "load_offline_token", return_value="offline"):
661+
with patch.object(HTTPRunDB, "api_call") as api_call:
662+
api_response = MagicMock()
663+
api_response.json.return_value = server_cfg
664+
api_call.return_value = api_response
665+
db = HTTPRunDB("http://some-server:1919")
666+
db.connect()
667+
assert (
668+
mlrun.mlconf.auth_with_oauth_token.token_file == expected_token_file
669+
), f"Expected token_file to be {expected_token_file}, got {mlrun.mlconf.auth_with_oauth_token.token_file}"
670+
671+
582672
def test_init_token_provider_stores_username_and_password_from_add_or_refresh_credentials(
583673
monkeypatch,
584674
):

0 commit comments

Comments
 (0)