diff --git a/e2e/conftest.py b/e2e/conftest.py index ab39544e5f..db2100063f 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -76,6 +76,8 @@ from nemo_platform import NeMoPlatform from nemo_platform_plugin.client.adapter import client_from_platform from nemo_platform_plugin.files.client import FilesClient +from nemo_platform_plugin.secrets.client import SecretsClient +from nemo_platform_plugin.secrets.types import PlatformSecretCreateRequest from e2e.services_pool_fixtures import ( # noqa: F401 _services, @@ -125,10 +127,11 @@ def ngc_api_key() -> str: def ngc_secret(sdk: NeMoPlatform, workspace: str, ngc_api_key: str) -> Iterator[str]: """Create a secret containing the NGC API key, cleaned up after test.""" secret_name = f"e2e-ngc-key-{uuid.uuid4().hex[:8]}" - sdk.secrets.create(workspace=workspace, name=secret_name, value=ngc_api_key) + secrets = client_from_platform(sdk, SecretsClient) + secrets.create_secret(workspace=workspace, body=PlatformSecretCreateRequest(name=secret_name, value=ngc_api_key)) yield secret_name try: - sdk.secrets.delete(workspace=workspace, name=secret_name) + secrets.delete_secret(workspace=workspace, name=secret_name) except Exception: pass # Best-effort cleanup; the workspace is deleted anyway diff --git a/e2e/files/test_storage_backends.py b/e2e/files/test_storage_backends.py index 45e7a7b92a..99575a6e65 100644 --- a/e2e/files/test_storage_backends.py +++ b/e2e/files/test_storage_backends.py @@ -19,10 +19,13 @@ import pytest from nemo_platform import NeMoPlatform +from nemo_platform_plugin.client.adapter import client_from_platform from nemo_platform_plugin.client.errors import BadRequestError from nemo_platform_plugin.files.client import FilesClient from nemo_platform_plugin.files.storage_config import HuggingfaceStorageConfig, NGCStorageConfig from nemo_platform_plugin.files.types import CreateFilesetRequest +from nemo_platform_plugin.secrets.client import SecretsClient +from nemo_platform_plugin.secrets.types import PlatformSecretCreateRequest # --------------------------------------------------------------------------- # NGC configuration @@ -82,10 +85,11 @@ def hf_token() -> str: def hf_secret(sdk: NeMoPlatform, workspace: str, hf_token: str) -> Iterator[str]: """Create a secret containing the HF token, cleaned up after test.""" secret_name = f"e2e-hf-tok-{uuid.uuid4().hex[:8]}" - sdk.secrets.create(workspace=workspace, name=secret_name, value=hf_token) + secrets = client_from_platform(sdk, SecretsClient) + secrets.create_secret(workspace=workspace, body=PlatformSecretCreateRequest(name=secret_name, value=hf_token)) yield secret_name try: - sdk.secrets.delete(workspace=workspace, name=secret_name) + secrets.delete_secret(workspace=workspace, name=secret_name) except Exception: pass # Best-effort cleanup; the workspace is deleted anyway @@ -202,7 +206,8 @@ def test_create_error( """Bad NGC configurations are rejected with 400.""" value = secret_value if secret_value is not None else ngc_api_key secret_name = f"e2e-ngc-err-{uuid.uuid4().hex[:8]}" - sdk.secrets.create(workspace=workspace, name=secret_name, value=value) + secrets = client_from_platform(sdk, SecretsClient) + secrets.create_secret(workspace=workspace, body=PlatformSecretCreateRequest(name=secret_name, value=value)) try: storage = NGCStorageConfig( api_key_secret=secret_name, @@ -220,7 +225,7 @@ def test_create_error( ), ) finally: - sdk.secrets.delete(workspace=workspace, name=secret_name) + secrets.delete_secret(workspace=workspace, name=secret_name) def test_create_error_nonexistent_secret(self, files_client: FilesClient, workspace: str): """Referencing a secret that doesn't exist is rejected with 400.""" diff --git a/e2e/test_jobs.py b/e2e/test_jobs.py index f15ecc443a..d4445cc37a 100644 --- a/e2e/test_jobs.py +++ b/e2e/test_jobs.py @@ -15,11 +15,14 @@ import uuid import pytest -from nemo_platform import NeMoPlatform, NotFoundError +from nemo_platform import NeMoPlatform from nemo_platform_plugin.client.adapter import client_from_platform +from nemo_platform_plugin.client.errors import NotFoundError from nemo_platform_plugin.jobs.client import JobsClient from nemo_platform_plugin.jobs.constants import DEFAULT_JOB_STORAGE_PATH from nemo_platform_plugin.jobs.types import CreatePlatformJobRequest +from nemo_platform_plugin.secrets.client import SecretsClient +from nemo_platform_plugin.secrets.types import PlatformSecretCreateRequest from nmp.testing.e2e import wait_for_job_logs, wait_for_platform_job from e2e.services_pool import RunningServices @@ -258,7 +261,10 @@ def test_job_using_secret_environment_variable(sdk: NeMoPlatform, workspace: str secret_name = f"e2e-secret-{uuid.uuid4().hex[:8]}" secret_value = "s3cret-val" - secret = sdk.secrets.create(workspace=workspace, name=secret_name, value=secret_value) + secrets = client_from_platform(sdk, SecretsClient) + secret = secrets.create_secret( + workspace=workspace, body=PlatformSecretCreateRequest(name=secret_name, value=secret_value) + ).data() assert secret.name is not None, "Failed to create platform secret" secret_deleted = False @@ -303,14 +309,14 @@ def test_job_using_secret_environment_variable(sdk: NeMoPlatform, workspace: str all_messages = " ".join(log.message for log in step_logs.data) assert secret_value in all_messages, "Step logs do not show secret environment variable was used" - sdk.secrets.delete(workspace=workspace, name=secret_name) + secrets.delete_secret(workspace=workspace, name=secret_name) secret_deleted = True with pytest.raises(NotFoundError): - sdk.secrets.retrieve(secret_name, workspace=workspace) + secrets.get_secret(name=secret_name, workspace=workspace).data() finally: if not secret_deleted: try: - sdk.secrets.delete(workspace=workspace, name=secret_name) + secrets.delete_secret(workspace=workspace, name=secret_name) except Exception: pass diff --git a/e2e/test_secrets.py b/e2e/test_secrets.py index d8e3446d80..65b3293b40 100644 --- a/e2e/test_secrets.py +++ b/e2e/test_secrets.py @@ -10,6 +10,9 @@ import uuid from nemo_platform import NeMoPlatform +from nemo_platform_plugin.client.adapter import client_from_platform +from nemo_platform_plugin.secrets.client import SecretsClient +from nemo_platform_plugin.secrets.types import PlatformSecretCreateRequest def test_secret_create_and_list(sdk: NeMoPlatform, workspace: str): @@ -22,23 +25,23 @@ def test_secret_create_and_list(sdk: NeMoPlatform, workspace: str): """ secret_name = f"e2e-secret-{uuid.uuid4().hex[:8]}" secret_value = "e2e-test-secret-value" + secrets = client_from_platform(sdk, SecretsClient) # Create a secret - secret = sdk.secrets.create( + secret = secrets.create_secret( workspace=workspace, - name=secret_name, - value=secret_value, - ) + body=PlatformSecretCreateRequest(name=secret_name, value=secret_value), + ).data() assert secret.name == secret_name assert secret.workspace == workspace # List secrets and verify the new secret appears - list_response = sdk.secrets.list(workspace=workspace) - secret_names = [s.name for s in list_response.data] + list_response = secrets.list_secrets(workspace=workspace) + secret_names = [s.name for s in list_response.items()] assert secret_name in secret_names # Retrieve the secret to verify it was created correctly - retrieved_secret = sdk.secrets.retrieve(secret_name, workspace=workspace) + retrieved_secret = secrets.get_secret(name=secret_name, workspace=workspace).data() assert retrieved_secret.name == secret_name assert retrieved_secret.workspace == workspace @@ -53,18 +56,17 @@ def test_secret_create_duplicate_fails(sdk: NeMoPlatform, workspace: str): secret_value = "e2e-duplicate-test-secret-value" # Create the initial secret - sdk.secrets.create( + secrets = client_from_platform(sdk, SecretsClient) + secrets.create_secret( workspace=workspace, - name=secret_name, - value=secret_value, + body=PlatformSecretCreateRequest(name=secret_name, value=secret_value), ) # Attempt to create a duplicate secret and expect failure try: - sdk.secrets.create( + secrets.create_secret( workspace=workspace, - name=secret_name, - value="some-other-value", + body=PlatformSecretCreateRequest(name=secret_name, value="some-other-value"), ) assert False, "Expected an exception when creating a duplicate secret" except Exception as e: @@ -82,26 +84,26 @@ def test_secret_create_and_delete(sdk: NeMoPlatform, workspace: str): secret_value = "e2e-delete-test-secret-value" # Create a secret - sdk.secrets.create( + secrets = client_from_platform(sdk, SecretsClient) + secrets.create_secret( workspace=workspace, - name=secret_name, - value=secret_value, + body=PlatformSecretCreateRequest(name=secret_name, value=secret_value), ) # Verify the secret appears in the list - list_response = sdk.secrets.list(workspace=workspace) - secret_names = [s.name for s in list_response.data] + list_response = secrets.list_secrets(workspace=workspace) + secret_names = [s.name for s in list_response.items()] assert secret_name in secret_names # Delete the secret - sdk.secrets.delete( + secrets.delete_secret( workspace=workspace, name=secret_name, ) # Verify the secret no longer appears in the list - list_response = sdk.secrets.list(workspace=workspace) - secret_names = [s.name for s in list_response.data] + list_response = secrets.list_secrets(workspace=workspace) + secret_names = [s.name for s in list_response.items()] assert secret_name not in secret_names @@ -114,10 +116,13 @@ def test_secret_data_not_in_create_response(sdk: NeMoPlatform, workspace: str): secret_name = f"e2e-no-data-create-{uuid.uuid4().hex[:8]}" secret_value = "this-should-not-appear-in-response" - secret = sdk.secrets.create( - workspace=workspace, - name=secret_name, - value=secret_value, + secret = ( + client_from_platform(sdk, SecretsClient) + .create_secret( + workspace=workspace, + body=PlatformSecretCreateRequest(name=secret_name, value=secret_value), + ) + .data() ) # Verify name and workspace are present @@ -139,15 +144,15 @@ def test_secret_data_not_in_retrieve_response(sdk: NeMoPlatform, workspace: str) """ secret_name = f"e2e-no-data-retrieve-{uuid.uuid4().hex[:8]}" secret_value = "this-should-not-appear-in-retrieve" + secrets = client_from_platform(sdk, SecretsClient) - sdk.secrets.create( + secrets.create_secret( workspace=workspace, - name=secret_name, - value=secret_value, + body=PlatformSecretCreateRequest(name=secret_name, value=secret_value), ) # Retrieve the secret - retrieved = sdk.secrets.retrieve(secret_name, workspace=workspace) + retrieved = secrets.get_secret(name=secret_name, workspace=workspace).data() # Verify name and workspace are present assert retrieved.name == secret_name @@ -168,21 +173,22 @@ def test_secret_data_not_in_list_response(sdk: NeMoPlatform, workspace: str): secret_name = f"e2e-no-data-list-{uuid.uuid4().hex[:8]}" secret_value = "this-should-not-appear-in-list" - sdk.secrets.create( + secrets = client_from_platform(sdk, SecretsClient) + secrets.create_secret( workspace=workspace, - name=secret_name, - value=secret_value, + body=PlatformSecretCreateRequest(name=secret_name, value=secret_value), ) # List secrets - list_response = sdk.secrets.list(workspace=workspace) + list_response = secrets.list_secrets(workspace=workspace) # Find our secret in the list - our_secret = next((s for s in list_response.data if s.name == secret_name), None) + listed_secrets = list(list_response.items()) + our_secret = next((s for s in listed_secrets if s.name == secret_name), None) assert our_secret is not None, "Created secret should appear in list" # Verify no secrets in the list expose their values - for secret in list_response.data: + for secret in listed_secrets: secret_dict = secret.model_dump() assert "data" not in secret_dict or secret_dict.get("data") is None assert "_data" not in secret_dict diff --git a/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/credentials.py b/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/credentials.py index db04eab4b5..64ee226360 100644 --- a/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/credentials.py +++ b/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/credentials.py @@ -20,6 +20,8 @@ read_env_file, write_env_file, ) +from nemo_platform_plugin.client.adapter import client_from_platform +from nemo_platform_plugin.secrets.client import SecretsClient def resolve_inference_key(config: IronSwarmConfig) -> tuple[str | None, str]: @@ -29,8 +31,8 @@ def resolve_inference_key(config: IronSwarmConfig) -> tuple[str | None, str]: explicit ``INFERENCE_API_KEY`` still wins at run time, where the job injects via ``setdefault``. """ try: - sdk = make_sdk(base_url()) - secret = sdk.secrets.access(config.inference_secret_name, workspace=config.default_workspace) + secrets = client_from_platform(make_sdk(base_url()), SecretsClient) + secret = secrets.access_secret(name=config.inference_secret_name, workspace=config.default_workspace).data() if secret and secret.value: return secret.value, f"secret '{config.inference_secret_name}'" except Exception: # Secrets store unreachable/absent → fall back to env diff --git a/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py b/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py index e7b92359cd..d7fe87554f 100644 --- a/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py +++ b/plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py @@ -31,7 +31,9 @@ IronSwarmRunError, ) from nemo_iron_swarm_plugin.model_config import ModelChoice, WarGameModels +from nemo_platform_plugin.client.adapter import client_from_platform from nemo_platform_plugin.job_context import JobContext +from nemo_platform_plugin.secrets.client import SecretsClient def require_provisioned(plugin_config: IronSwarmConfig) -> None: @@ -81,7 +83,7 @@ def _resolve_secret(sdk: Any, name: str, workspace: str) -> str | None: """Fetch a Secret's plaintext value via the platform SDK; None if unavailable (caller warns/fails).""" if sdk is None: return None - secret = sdk.secrets.access(name, workspace=workspace) + secret = client_from_platform(sdk, SecretsClient).access_secret(name=name, workspace=workspace).data() value = getattr(secret, "value", None) return str(value) if value else None diff --git a/plugins/nemo-iron-swarm/tests/unit/test_model_config.py b/plugins/nemo-iron-swarm/tests/unit/test_model_config.py index fe8a066355..14483988e2 100644 --- a/plugins/nemo-iron-swarm/tests/unit/test_model_config.py +++ b/plugins/nemo-iron-swarm/tests/unit/test_model_config.py @@ -9,6 +9,7 @@ from types import SimpleNamespace from typing import Any +import pytest from _doubles import make_entity, make_job_context, make_sdk from nemo_iron_swarm_plugin.agent_resolver import inject_gateway_url from nemo_iron_swarm_plugin.jobs import _common @@ -17,11 +18,22 @@ class _FakeSecrets: + """Stands in for the typed ``SecretsClient``, not the old ``sdk.secrets`` resource.""" + def __init__(self, values: dict[str, str]) -> None: self._values = values - def access(self, name: str, *, workspace: str) -> Any: - return SimpleNamespace(value=self._values.get(name)) + def access_secret(self, *, name: str, workspace: str) -> Any: + return SimpleNamespace(data=lambda: SimpleNamespace(value=self._values.get(name))) + + +@pytest.fixture(autouse=True) +def _route_secrets_client(monkeypatch: Any) -> None: + """``build_model_env`` resolves keys through ``client_from_platform(sdk, SecretsClient)``. + + The sdk doubles here carry their client on ``.secrets``, so hand that back directly. + """ + monkeypatch.setattr(_common, "client_from_platform", lambda sdk, _cls: sdk.secrets) def _sdk(secrets: dict[str, str]) -> Any: @@ -113,12 +125,18 @@ class _Secrets: def __init__(self, values: dict[str, str], *, reachable: bool = True) -> None: self._values, self._reachable = values, reachable - def access(self, name: str, *, workspace: str) -> Any: + def access_secret(self, *, name: str, workspace: str) -> Any: if not self._reachable: raise RuntimeError("secrets store unreachable") if name not in self._values: raise KeyError(name) - return SimpleNamespace(value=self._values[name]) + return SimpleNamespace(data=lambda: SimpleNamespace(value=self._values[name])) + + +def _sdk_with(secrets: _Secrets, monkeypatch: Any) -> Any: + """An sdk double carrying its typed client on ``.secrets``, per ``_route_secrets_client``.""" + del monkeypatch # routing is handled by the autouse fixture + return SimpleNamespace(secrets=secrets) def _config_double(monkeypatch: Any, dotenv_key: str | None) -> None: @@ -138,7 +156,7 @@ def test_resolve_model_key_prefers_the_named_secret(monkeypatch: Any) -> None: from nemo_iron_swarm_plugin.jobs import _common as common _config_double(monkeypatch, "from-dotenv") - sdk = SimpleNamespace(secrets=_Secrets({"my-key": "chosen", "iron-swarm-inference-key": "provisioned"})) + sdk = _sdk_with(_Secrets({"my-key": "chosen", "iron-swarm-inference-key": "provisioned"}), monkeypatch) assert common.resolve_model_key(sdk, "my-key", workspace="default") == "chosen" @@ -148,7 +166,7 @@ def test_resolve_model_key_falls_back_to_the_provisioned_secret(monkeypatch: Any from nemo_iron_swarm_plugin.jobs import _common as common _config_double(monkeypatch, "from-dotenv") - sdk = SimpleNamespace(secrets=_Secrets({"iron-swarm-inference-key": "provisioned"})) + sdk = _sdk_with(_Secrets({"iron-swarm-inference-key": "provisioned"}), monkeypatch) assert common.resolve_model_key(sdk, None, workspace="default") == "provisioned" @@ -159,10 +177,10 @@ def test_resolve_model_key_falls_back_to_the_dotenv(monkeypatch: Any) -> None: _config_double(monkeypatch, "from-dotenv") - absent = SimpleNamespace(secrets=_Secrets({})) + absent = _sdk_with(_Secrets({}), monkeypatch) assert common.resolve_model_key(absent, None, workspace="default") == "from-dotenv" - unreachable = SimpleNamespace(secrets=_Secrets({}, reachable=False)) + unreachable = _sdk_with(_Secrets({}, reachable=False), monkeypatch) assert common.resolve_model_key(unreachable, None, workspace="default") == "from-dotenv" @@ -170,4 +188,4 @@ def test_resolve_model_key_none_when_nothing_resolves(monkeypatch: Any) -> None: from nemo_iron_swarm_plugin.jobs import _common as common _config_double(monkeypatch, None) - assert common.resolve_model_key(SimpleNamespace(secrets=_Secrets({})), None, workspace="default") is None + assert common.resolve_model_key(_sdk_with(_Secrets({}), monkeypatch), None, workspace="default") is None diff --git a/plugins/nemo-iron-swarm/tests/unit/test_operator_env.py b/plugins/nemo-iron-swarm/tests/unit/test_operator_env.py index 6e1c0bcd0b..d78bd21ede 100644 --- a/plugins/nemo-iron-swarm/tests/unit/test_operator_env.py +++ b/plugins/nemo-iron-swarm/tests/unit/test_operator_env.py @@ -67,8 +67,11 @@ def test_read_env_file_parses_comments_blank_export_quotes(tmp_path: Path) -> No def test_resolve_inference_key_prefers_secrets_over_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv(INFERENCE_API_KEY_ENVVAR, "env-value") # store must still win secret = types.SimpleNamespace(value="secret-value") - fake_sdk = types.SimpleNamespace(secrets=types.SimpleNamespace(access=lambda name, workspace: secret)) - monkeypatch.setattr(credentials, "make_sdk", lambda base: fake_sdk) + fake_secrets = types.SimpleNamespace( + access_secret=lambda name, workspace: types.SimpleNamespace(data=lambda: secret) + ) + monkeypatch.setattr(credentials, "make_sdk", lambda base: types.SimpleNamespace()) + monkeypatch.setattr(credentials, "client_from_platform", lambda sdk, cls: fake_secrets) value, source = credentials.resolve_inference_key(_config(tmp_path)) assert value == "secret-value"