Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import anyio.from_thread
from data_designer.engine.errors import SecretResolutionError
from data_designer_nemo.errors import NDDInternalError, NDDInvalidConfigError
from nemo_platform import AsyncNeMoPlatform, NeMoPlatform, NotFoundError, PermissionDeniedError
from nemo_platform import AsyncNeMoPlatform, NeMoPlatform
from nemo_platform_plugin.client.adapter import client_from_platform
from nemo_platform_plugin.client.errors import NotFoundError, PermissionDeniedError
from nemo_platform_plugin.secrets.client import AsyncSecretsClient, SecretsClient

logger = logging.getLogger(__name__)

Expand All @@ -20,8 +23,9 @@ async def validate_secret(sdk: AsyncNeMoPlatform, secret: str, default_workspace
NMPSecretResolver).
"""
workspace, name = _parse_secret_reference(secret, default_workspace)
secrets = client_from_platform(sdk, AsyncSecretsClient)
try:
await sdk.secrets.access(name=name, workspace=workspace)
await secrets.access_secret(name=name, workspace=workspace)
except NotFoundError as e:
raise NDDInvalidConfigError(f"Could not find secret {name!r} in workspace {workspace!r}") from e
except PermissionDeniedError as e:
Expand Down Expand Up @@ -58,11 +62,14 @@ def resolve(self, secret: str) -> str:
workspace, name = _parse_secret_reference(secret, self._default_workspace)
if isinstance(self._sdk, AsyncNeMoPlatform):
# ``anyio.from_thread.run`` only forwards positional args, so wrap the
# kwargs-only SDK call in a no-arg coroutine factory.
async_sdk = self._sdk
result = anyio.from_thread.run(lambda: async_sdk.secrets.access(name=name, workspace=workspace))
# kwargs-only client call in a no-arg coroutine factory.
async_secrets = client_from_platform(self._sdk, AsyncSecretsClient)
result = anyio.from_thread.run(
lambda: async_secrets.access_secret(name=name, workspace=workspace)
).data()
else:
result = self._sdk.secrets.access(name=name, workspace=workspace)
secrets = client_from_platform(self._sdk, SecretsClient)
result = secrets.access_secret(name=name, workspace=workspace).data()
return result.value
except Exception as e:
raise SecretResolutionError(f"Error resolving secret {secret!r}: {e}") from e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
import typer
import yaml as _yaml
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, PlatformSecretUpdateRequest
from nmp.common.config import nmp_user_data_dir
from pydantic import SecretStr
from rich import box
from rich.console import Console
from rich.panel import Panel
Expand Down Expand Up @@ -385,19 +389,22 @@ def _provider_exists(client: NeMoPlatform, name: str, workspace: str) -> bool:

def _secret_exists(client: NeMoPlatform, name: str, workspace: str) -> bool:
"""Return True if a secret with *name* already exists."""
secrets = client_from_platform(client, SecretsClient)
try:
client.secrets.retrieve(name, workspace=workspace)
secrets.get_secret(name=name, workspace=workspace)
return True
except Exception:
return False
Comment thread
matthewgrossman marked this conversation as resolved.


def _create_secret(client: NeMoPlatform, name: str, value: str, workspace: str) -> None:
client.secrets.create(name=name, value=value, workspace=workspace)
secrets = client_from_platform(client, SecretsClient)
secrets.create_secret(body=PlatformSecretCreateRequest(name=name, value=SecretStr(value)), workspace=workspace)


def _update_secret(client: NeMoPlatform, name: str, value: str, workspace: str) -> None:
client.secrets.update(name, value=value, workspace=workspace)
secrets = client_from_platform(client, SecretsClient)
secrets.update_secret(name=name, body=PlatformSecretUpdateRequest(value=SecretStr(value)), workspace=workspace)


def _create_provider(
Expand Down
97 changes: 80 additions & 17 deletions packages/nemo_platform_ext/tests/cli/commands/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
Context,
ContextDefinition,
)
from nemo_platform_plugin.client.errors import NotFoundError
from nemo_platform_plugin.secrets.types import PlatformSecretCreateRequest, PlatformSecretUpdateRequest

SETUP_MOD = "nemo_platform_ext.cli.commands.setup"

Expand Down Expand Up @@ -241,21 +243,69 @@ def test_ollama_not_running(self):
# ---------------------------------------------------------------------------


def _not_found_error() -> NotFoundError:
"""Build a NotFoundError backed by a mock 404 response for `get_secret`."""
resp = MagicMock(spec=httpx.Response)
resp.status_code = 404
resp.json.side_effect = ValueError("no body")
resp.text = "not found"
resp.reason_phrase = "Not Found"
return NotFoundError(resp)


def _make_mock_secrets_client(*, secret_exists: bool = False) -> MagicMock:
"""Build a mock typed SecretsClient with get/create/update as MagicMocks."""
secrets = MagicMock()
secrets.get_secret = MagicMock()
secrets.create_secret = MagicMock()
secrets.update_secret = MagicMock()
if secret_exists:
secrets.get_secret.return_value = MagicMock()
else:
secrets.get_secret.side_effect = _not_found_error()
return secrets


def _make_mock_client(*, provider_exists: bool = False, secret_exists: bool = False) -> MagicMock:
"""Build a mock NeMoPlatform client with configurable provider/secret state."""
"""Build a mock NeMoPlatform client with configurable provider/secret state.

The typed secrets client returned by ``client_from_platform`` (patched via
the ``_patch_secrets_client`` fixture) is stashed on ``client.mock_secrets``
so tests can assert on ``get_secret``/``create_secret``/``update_secret``.
"""
client = MagicMock()
client.inference.providers = MagicMock(spec=ProvidersResource)
if secret_exists:
client.secrets.retrieve.return_value = MagicMock()
else:
client.secrets.retrieve.side_effect = Exception("not found")
client.mock_secrets = _make_mock_secrets_client(secret_exists=secret_exists)
if provider_exists:
client.inference.providers.retrieve.return_value = MagicMock()
else:
client.inference.providers.retrieve.side_effect = Exception("not found")
return client


@pytest.fixture(autouse=True)
def _patch_secrets_client():
"""Route ``client_from_platform(client, SecretsClient)`` to ``client.mock_secrets``.

The secrets helpers in setup.py obtain a typed ``SecretsClient`` via
``client_from_platform``. Real adaptation would run against a MagicMock and
blow up in ``raise_for_status``, so tests patch it to return the mock secrets
client attached to the mock platform client (falling back to a fresh mock for
plain ``MagicMock()`` clients that lack ``mock_secrets``).
"""

def _resolve(client, _client_cls):
secrets = getattr(client, "mock_secrets", None)
if isinstance(secrets, MagicMock):
return secrets
fallback = _make_mock_secrets_client()
client.mock_secrets = fallback
return fallback

with patch(f"{SETUP_MOD}.client_from_platform", side_effect=_resolve):
yield


# ---------------------------------------------------------------------------
# Provider creation
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -343,7 +393,7 @@ def test_openai_key_creates_provider(self):
with patch.dict("os.environ", {"OPENAI_API_KEY": "sk-test123"}, clear=True):
result = _auto_setup(client, "default")
assert result is True
client.secrets.create.assert_called_once()
client.mock_secrets.create_secret.assert_called_once()
client.inference.providers.create.assert_called_once()
create_kwargs = client.inference.providers.create.call_args
assert create_kwargs.kwargs["name"] == "openai"
Expand Down Expand Up @@ -1188,10 +1238,13 @@ def test_existing_secret_gets_updated_with_new_value(self):
api_key="new-key-value",
workspace="default",
)
client.secrets.update.assert_called_once_with(
"nvidia-build-api-key", value="new-key-value", workspace="default"
)
client.secrets.create.assert_not_called()
client.mock_secrets.update_secret.assert_called_once()
update_call = client.mock_secrets.update_secret.call_args
assert update_call.kwargs["name"] == "nvidia-build-api-key"
assert update_call.kwargs["workspace"] == "default"
assert isinstance(update_call.kwargs["body"], PlatformSecretUpdateRequest)
assert update_call.kwargs["body"].value.get_secret_value() == "new-key-value"
client.mock_secrets.create_secret.assert_not_called()
client.inference.providers.create.assert_called_once()
client.inference.providers.update.assert_not_called()

Expand All @@ -1205,8 +1258,13 @@ def test_existing_provider_gets_updated_with_secret(self):
api_key="my-key",
workspace="default",
)
client.secrets.create.assert_called_once_with(name="nvidia-build-api-key", value="my-key", workspace="default")
client.secrets.update.assert_not_called()
client.mock_secrets.create_secret.assert_called_once()
create_call = client.mock_secrets.create_secret.call_args
assert create_call.kwargs["workspace"] == "default"
assert isinstance(create_call.kwargs["body"], PlatformSecretCreateRequest)
assert create_call.kwargs["body"].name == "nvidia-build-api-key"
assert create_call.kwargs["body"].value.get_secret_value() == "my-key"
client.mock_secrets.update_secret.assert_not_called()
client.inference.providers.update.assert_called_once()
call_kwargs = client.inference.providers.update.call_args.kwargs
assert call_kwargs["api_key_secret_name"] == "nvidia-build-api-key"
Expand All @@ -1223,9 +1281,9 @@ def test_fresh_install_creates_both(self):
api_key="sk-test",
workspace="default",
)
client.secrets.create.assert_called_once()
client.mock_secrets.create_secret.assert_called_once()
client.inference.providers.create.assert_called_once()
client.secrets.update.assert_not_called()
client.mock_secrets.update_secret.assert_not_called()
client.inference.providers.update.assert_not_called()

def test_existing_provider_updated_with_extra_headers(self):
Expand All @@ -1252,7 +1310,7 @@ def test_auto_setup_updates_existing_provider_secret_binding(self):
with patch.dict("os.environ", {"OPENAI_API_KEY": "sk-new-key"}, clear=True):
result = _auto_setup(client, "default")
assert result is True
client.secrets.create.assert_called_once()
client.mock_secrets.create_secret.assert_called_once()
client.inference.providers.update.assert_called_once()
call_kwargs = client.inference.providers.update.call_args.kwargs
assert call_kwargs["api_key_secret_name"] == "openai-api-key"
Expand All @@ -1263,8 +1321,13 @@ def test_auto_setup_updates_existing_secret_value(self):
with patch.dict("os.environ", {"NVIDIA_API_KEY": "nvapi-new"}, clear=True):
result = _auto_setup(client, "default")
assert result is True
client.secrets.update.assert_called_once_with("nvidia-build-api-key", value="nvapi-new", workspace="default")
client.secrets.create.assert_not_called()
client.mock_secrets.update_secret.assert_called_once()
update_call = client.mock_secrets.update_secret.call_args
assert update_call.kwargs["name"] == "nvidia-build-api-key"
assert update_call.kwargs["workspace"] == "default"
assert isinstance(update_call.kwargs["body"], PlatformSecretUpdateRequest)
assert update_call.kwargs["body"].value.get_secret_value() == "nvapi-new"
client.mock_secrets.create_secret.assert_not_called()


# ---------------------------------------------------------------------------
Expand Down
Loading
Loading