diff --git a/README.md b/README.md index 6901679..b79bdc2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,9 @@ pip install sap-cloud-sdk The SDK automatically resolves configuration from multiple sources with the following priority: -1. **Kubernetes-mounted secrets**: `/etc/secrets/appfnd///` +1. **Kubernetes-mounted secrets**: `$SERVICE_BINDING_ROOT///` + - `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set (follows the [servicebinding.io](https://servicebinding.io/spec/core/1.1.0/) spec). See the [Secret Resolver guide](../core/secret_resolver/user-guide.md) for details. + 2. **Environment variables**: `CLOUD_SDK_CFG___` - For instance names, hyphens (`"-"`) are replaced with underscores (`"_"`) for compatibility with system environment variables. - You can see examples in our [env_integration_tests.example](.env_integration_tests.example) diff --git a/pyproject.toml b/pyproject.toml index 78a2f1b..f21dd75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.14.2" +version = "0.14.3" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/aicore/__init__.py b/src/sap_cloud_sdk/aicore/__init__.py index 5995f12..85b7d50 100644 --- a/src/sap_cloud_sdk/aicore/__init__.py +++ b/src/sap_cloud_sdk/aicore/__init__.py @@ -9,6 +9,7 @@ import os from typing import Optional +from sap_cloud_sdk.core.secret_resolver import resolve_base_mount from sap_cloud_sdk.core.telemetry.metrics_decorator import record_metrics from sap_cloud_sdk.core.telemetry.module import Module from sap_cloud_sdk.core.telemetry.operation import Operation @@ -35,7 +36,8 @@ def _get_secret( instance_name: Name of the aicore instance defined in app.yaml. Defaults to aicore-instance """ - secrets_base_path = f"/etc/secrets/appfnd/aicore/{instance_name}" + resolved_base_path = resolve_base_mount() + secrets_base_path = f"{resolved_base_path}/aicore/{instance_name}" secret_file_name = file_name if file_name else env_var_name secret_file_path = os.path.join(secrets_base_path, secret_file_name) @@ -70,7 +72,8 @@ def _get_aicore_base_url(instance_name: str = "aicore-instance") -> str: Returns: Base URL for AI Core service """ - secrets_base_path = f"/etc/secrets/appfnd/aicore/{instance_name}" + resolved_base_path = resolve_base_mount() + secrets_base_path = f"{resolved_base_path}/aicore/{instance_name}" serviceurls_file = os.path.join(secrets_base_path, "serviceurls") # Try reading from serviceurls file diff --git a/src/sap_cloud_sdk/aicore/user-guide.md b/src/sap_cloud_sdk/aicore/user-guide.md index ad2f5e3..647f402 100644 --- a/src/sap_cloud_sdk/aicore/user-guide.md +++ b/src/sap_cloud_sdk/aicore/user-guide.md @@ -69,46 +69,6 @@ The function loads and configures these credentials: --- -## Configuration - -### Cloud Mode (Mounted Secrets) - -In Kubernetes environments, secrets are automatically loaded from: - -``` -/etc/secrets/appfnd/aicore/{instance_name}/ -├── clientid # OAuth2 client ID -├── clientsecret # OAuth2 client secret -├── url # Authentication server URL -└── serviceurls # JSON file with AI_API_URL field -``` - -**serviceurls file format:** -```json -{ - "AI_API_URL": "https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com" -} -``` - -### Environment Variable Fallback - -If mounted secrets are not available, the function falls back to environment variables: - -```bash -# Authentication credentials -export AICORE_CLIENT_ID="your-client-id" -export AICORE_CLIENT_SECRET="your-client-secret" - -# Service endpoints -export AICORE_AUTH_URL="https://your-subdomain.authentication.eu10.hana.ondemand.com/oauth/token" -export AICORE_BASE_URL="https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com/v2" - -# Optional: Resource group (defaults to "default") -export AICORE_RESOURCE_GROUP="my-resource-group" -``` - ---- - ## Usage with LiteLLM After calling `set_aicore_config()`, LiteLLM automatically uses the configured AI Core credentials: @@ -174,38 +134,6 @@ embedding_response = embedding( --- -## Multiple AI Core Instances - -If you have multiple AI Core instances (e.g., development, staging, production), specify the instance name: - -```python -from sap_cloud_sdk.aicore import set_aicore_config - -# Development environment -set_aicore_config(instance_name="aicore-dev") - -# Production environment -set_aicore_config(instance_name="aicore-prod") -``` - -Each instance should have its own mounted secrets or environment variables. - ---- - -## URL Normalization - -The function automatically normalizes URLs to ensure compatibility: - -### Authentication URL -- **Input**: `https://subdomain.authentication.region.hana.ondemand.com` -- **Output**: `https://subdomain.authentication.region.hana.ondemand.com/oauth/token` - -### Base URL -- **Input**: `https://api.ai.prod.region.aws.ml.hana.ondemand.com` -- **Output**: `https://api.ai.prod.region.aws.ml.hana.ondemand.com/v2` - ---- - ## Complete Example ```python @@ -365,6 +293,65 @@ set_aicore_config() --- +## Configuration + +### Service Binding + +- **Mount path**: `$SERVICE_BINDING_ROOT/aicore/{instance}/` (defaults to `/etc/secrets/appfnd/aicore/{instance}/`) +- **Required Keys**: `clientid`, `clientsecret`, `url` (auth server), `serviceurls` (JSON with `AI_API_URL`) +- **Env var fallback**: `CLOUD_SDK_CFG_AICORE_{INSTANCE}_{FIELD}` (uppercased, hyphens in instance replaced with `_`) + +> **Note:** `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set. See the [Secret Resolver guide](../core/secret_resolver/user-guide.md) for details. + +#### Mounted Secrets (Kubernetes) + +``` +$SERVICE_BINDING_ROOT/aicore/{instance}/ +├── clientid # OAuth2 client ID +├── clientsecret # OAuth2 client secret +├── url # Authentication server URL +└── serviceurls # JSON file with AI_API_URL field +``` + +#### Environment Variables + +```bash +# Authentication credentials +export AICORE_CLIENT_ID="your-client-id" +export AICORE_CLIENT_SECRET="your-client-secret" + +# Service endpoints +export AICORE_AUTH_URL="https://your-subdomain.authentication.eu10.hana.ondemand.com/oauth/token" +export AICORE_BASE_URL="https://aicore.example.com" + +# Optional: Resource group (defaults to "default") +export AICORE_RESOURCE_GROUP="my-resource-group" +``` + +#### ServiceURLs JSON Schema + +The `serviceurls` file must contain: + +```json +{ + "AI_API_URL": "https://aicore.example.com" +} +``` + +#### URL Normalization + +This module automatically normalizes URLs to ensure compatibility: + +##### Authentication URL +- **Input**: `https://subdomain.authentication.region.hana.ondemand.com` +- **Output**: `https://subdomain.authentication.region.hana.ondemand.com/oauth/token` + +##### Base URL +- **Input**: `https://api.ai.prod.region.aws.ml.hana.ondemand.com` +- **Output**: `https://api.ai.prod.region.aws.ml.hana.ondemand.com/v2` + +--- + ## Notes - The `set_aicore_config()` function sets environment variables that persist for the lifetime of the Python process diff --git a/src/sap_cloud_sdk/core/auditlog/config.py b/src/sap_cloud_sdk/core/auditlog/config.py index 44fb9df..745a497 100644 --- a/src/sap_cloud_sdk/core/auditlog/config.py +++ b/src/sap_cloud_sdk/core/auditlog/config.py @@ -109,7 +109,11 @@ def _load_config_from_env() -> AuditLogConfig: binding_data: BindingData = BindingData("", "") read_from_mount_and_fallback_to_env_var( - "/etc/secrets/appfnd", "CLOUD_SDK_CFG", "auditlog", "default", binding_data + base_volume_mount="/etc/secrets/appfnd", + base_var_name="CLOUD_SDK_CFG", + module="auditlog", + instance="default", + target=binding_data, ) binding_data.validate() diff --git a/src/sap_cloud_sdk/core/auditlog/user-guide.md b/src/sap_cloud_sdk/core/auditlog/user-guide.md index a84ecec..6edf3fb 100644 --- a/src/sap_cloud_sdk/core/auditlog/user-guide.md +++ b/src/sap_cloud_sdk/core/auditlog/user-guide.md @@ -386,6 +386,39 @@ for failed in failed_messages: print(f"Failed to log event: {failed.error}") ``` -## Environment Configuration +## Configuration -The audit log module automatically detects the environment and configures itself accordingly, events are sent to the SAP Audit Log Service using OAuth2 authentication with automatic credential resolution from service bindings +### Service Binding + +- **Mount path**: `$SERVICE_BINDING_ROOT/auditlog/default/` (defaults to `/etc/secrets/appfnd/auditlog/default/`) +- **Required Keys**: `url` (Audit Log service URL), `uaa` (JSON string with XSUAA credentials) +- **Env var fallback**: `CLOUD_SDK_CFG_AUDITLOG_DEFAULT_{FIELD}` (uppercased) + +> **Note:** `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set. See the [Secret Resolver guide](../secret_resolver/user-guide.md) for details. + +#### Mounted Secrets (Kubernetes) + +``` +$SERVICE_BINDING_ROOT/auditlog/default/ +├── url +└── uaa +``` + +#### Environment Variables + +```bash +export CLOUD_SDK_CFG_AUDITLOG_DEFAULT_URL="https://auditlog.example.com" +export CLOUD_SDK_CFG_AUDITLOG_DEFAULT_UAA='{"clientid":"...","clientsecret":"...","url":"https://..."}' +``` + +#### UAA JSON Schema + +The `uaa` key must contain a JSON string with the XSUAA credentials: + +```json +{ + "clientid": "sb-xxx", + "clientsecret": "xxx", + "url": "https://subdomain.authentication.region.hana.ondemand.com" +} +``` diff --git a/src/sap_cloud_sdk/core/secret_resolver/__init__.py b/src/sap_cloud_sdk/core/secret_resolver/__init__.py index 9a98572..79cf79f 100644 --- a/src/sap_cloud_sdk/core/secret_resolver/__init__.py +++ b/src/sap_cloud_sdk/core/secret_resolver/__init__.py @@ -21,6 +21,6 @@ class MyConfig: ) """ -from .resolver import read_from_mount_and_fallback_to_env_var +from .resolver import read_from_mount_and_fallback_to_env_var, resolve_base_mount -__all__ = ["read_from_mount_and_fallback_to_env_var"] +__all__ = ["read_from_mount_and_fallback_to_env_var", "resolve_base_mount"] diff --git a/src/sap_cloud_sdk/core/secret_resolver/constants.py b/src/sap_cloud_sdk/core/secret_resolver/constants.py new file mode 100644 index 0000000..7d66cf4 --- /dev/null +++ b/src/sap_cloud_sdk/core/secret_resolver/constants.py @@ -0,0 +1,5 @@ +""" +Constants for secret resolver module. +""" + +BASE_MOUNT_PATH = "/etc/secrets/appfnd" diff --git a/src/sap_cloud_sdk/core/secret_resolver/resolver.py b/src/sap_cloud_sdk/core/secret_resolver/resolver.py index 4577c75..fa16322 100644 --- a/src/sap_cloud_sdk/core/secret_resolver/resolver.py +++ b/src/sap_cloud_sdk/core/secret_resolver/resolver.py @@ -5,6 +5,25 @@ import os from dataclasses import fields, is_dataclass from typing import Any, Dict, Tuple +from .constants import BASE_MOUNT_PATH + + +def resolve_base_mount(base_volume_mount: str = BASE_MOUNT_PATH) -> str: + """Resolve the base mount path for service binding discovery. + + Checks the ``SERVICE_BINDING_ROOT`` environment variable first (as defined + by the `servicebinding.io `_ + specification). Falls back to ``base_volume_mount`` when the env var is + absent. + + Args: + base_volume_mount: Default base path used when ``SERVICE_BINDING_ROOT`` + is not set. Defaults to ``/etc/secrets/appfnd``. + + Returns: + The effective base path for secret mount resolution. + """ + return os.environ.get("SERVICE_BINDING_ROOT", base_volume_mount) def _validate_inputs(module: str, instance: str) -> None: @@ -116,6 +135,8 @@ def read_from_mount_and_fallback_to_env_var( Load secrets for a given module and instance into the provided dataclass instance `target`. Fallback order: 1. Mounted volume path: {base_volume_mount}/{module}/{instance}/{field_key} + (``SERVICE_BINDING_ROOT`` env var overrides ``base_volume_mount`` — see + :func:`resolve_base_mount`) 2. Environment variables: {base_var_name}_{module}_{instance}_{field_key} (uppercased) Raises: @@ -126,12 +147,13 @@ def read_from_mount_and_fallback_to_env_var( """ _validate_inputs(module, instance) + resolved_base_path = resolve_base_mount(base_volume_mount) errors: list[str] = [] normalized_module = module.replace("-", "_") normalized_instance = instance.replace("-", "_") try: - _load_from_mount(base_volume_mount, module, instance, target) + _load_from_mount(resolved_base_path, module, instance, target) return except Exception as e: errors.append(f"mount failed: {e};") @@ -144,7 +166,7 @@ def read_from_mount_and_fallback_to_env_var( # Aggregate errors with actionable guidance for local dev and env fallback prefix_upper = f"{base_var_name}_{normalized_module}_{normalized_instance}".upper() - mount_dir = os.path.join(base_volume_mount, module, instance) + "/" + mount_dir = os.path.join(resolved_base_path, module, instance) + "/" guidance_parts: list[str] = [] guidance_parts.append("Secrets could not be loaded from mount or environment.") guidance_parts.append("Options:") diff --git a/src/sap_cloud_sdk/core/secret_resolver/user-guide.md b/src/sap_cloud_sdk/core/secret_resolver/user-guide.md index f70838a..aeea94b 100644 --- a/src/sap_cloud_sdk/core/secret_resolver/user-guide.md +++ b/src/sap_cloud_sdk/core/secret_resolver/user-guide.md @@ -34,10 +34,10 @@ class DatabaseConfig: config = DatabaseConfig() read_from_mount_and_fallback_to_env_var( base_volume_mount="/etc/secrets", # Base mount path - base_var_name="DB", # Environment variable prefix - module="database", # Module/service name - instance="primary", # Instance name - target=config # Target dataclass instance + base_var_name="DB", # Environment variable prefix + module="database", # Module/service name + instance="primary", # Instance name + target=config # Target dataclass instance ) print(f"Database: {config.username}@{config.host}:{config.port}") @@ -61,6 +61,18 @@ The Secret Resolver expects mounted secrets to follow this hierarchy: └── password ``` +### Base Path Resolution + +By default, the resolver looks for secrets under `/etc/secrets/appfnd`. You can override this by setting the `SERVICE_BINDING_ROOT` environment variable, which follows the [servicebinding.io](https://servicebinding.io) specification used across SAP SDKs and Kubernetes-native tooling. + +When `SERVICE_BINDING_ROOT` is set, it takes precedence over the default `/etc/secrets/appfnd` path: + +```bash +export SERVICE_BINDING_ROOT=/bindings +``` + +With this set, the resolver looks for secrets at `$SERVICE_BINDING_ROOT///` instead of `/etc/secrets/appfnd///`. + Example for the above configuration: ``` /etc/secrets/appfnd diff --git a/src/sap_cloud_sdk/destination/user-guide.md b/src/sap_cloud_sdk/destination/user-guide.md index d124a11..7d5c9ef 100644 --- a/src/sap_cloud_sdk/destination/user-guide.md +++ b/src/sap_cloud_sdk/destination/user-guide.md @@ -19,7 +19,6 @@ from sap_cloud_sdk.destination import ( ConsumptionOptions, ) -# Auto-detection based on environment; in cloud mode it will load credentials client = create_client(instance="default") fragment_client = create_fragment_client(instance="default") certificate_client = create_certificate_client(instance="default") @@ -636,15 +635,43 @@ mocks/certificates.json Entries with a `"tenant"` field are treated as subscriber-specific. Entries without `"tenant"` are provider entries. -## Secret Resolution +## Error Handling + +- `DestinationNotFoundError`: mapped from HTTP 404 where applicable +- `DestinationOperationError`: general operation failures +- `HttpError`: HTTP-related or local store read/write errors with `status_code` and `response_text` when applicable + +## Configuration ### Service Binding -- Mount path: `/etc/secrets/appfnd/destination/{instance}/` -- Keys: `clientid`, `clientsecret`, `url` (auth base), `uri` (service base), `identityzone` -- Fallback env vars: `CLOUD_SDK_CFG_DESTINATION_{INSTANCE}_{FIELD_KEY}` (uppercased) -- The config loader normalizes to a unified binding: - - `DestinationConfig(url=..., token_url=..., client_id=..., client_secret=..., identityzone=...)` +- **Mount path**: `$SERVICE_BINDING_ROOT/destination/{instance}/` (defaults to `/etc/secrets/appfnd/destination/{instance}/`) +- *Required Keys**: `clientid`, `clientsecret`, `url` (auth base), `uri` (service base), `identityzone` +- **Env var fallback**: `CLOUD_SDK_CFG_DESTINATION_{INSTANCE}_{FIELD}` (uppercased, hyphens in instance replaced with `_`) + +> **Note:** `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set. See the [Secret Resolver guide](../core/secret_resolver/user-guide.md) for details. + +#### Mounted Secrets (Kubernetes) + +``` +$SERVICE_BINDING_ROOT/destination/{instance}/ +├── clientid +├── clientsecret +├── url +├── uri +└── identityzone +``` + +#### Environment Variables + +```bash +# Example for Destination with instance name "default" +export CLOUD_SDK_CFG_DESTINATION_DEFAULT_CLIENTID="your-client-id" +export CLOUD_SDK_CFG_DESTINATION_DEFAULT_CLIENTSECRET="your-client-secret" +export CLOUD_SDK_CFG_DESTINATION_DEFAULT_URL="https://subdomain.authentication.region.hana.ondemand.com" +export CLOUD_SDK_CFG_DESTINATION_DEFAULT_URI="https://destination.cf.region.hana.ondemand.com" +export CLOUD_SDK_CFG_DESTINATION_DEFAULT_IDENTITYZONE="subdomain" +``` ### Transparent Proxy @@ -653,16 +680,10 @@ Entries with a `"tenant"` field are treated as subscriber-specific. Entries with - The proxy configuration is loaded and validated when the client is created - Proxy reachability is tested via HTTP HEAD request to `http://{proxy_name}.{namespace}` -## Tokens and Access Strategy (Cloud Mode) +### Tokens and Access Strategy The OAuth2 token URL is derived from service binding (`DestinationConfig.token_url`). For subscriber context, when a `tenant` is provided, the token provider constructs the subscriber token URL by replacing the identityzone segment with the tenant sub-domain. -## Error Handling - -- `DestinationNotFoundError`: mapped from HTTP 404 where applicable -- `DestinationOperationError`: general operation failures -- `HttpError`: HTTP-related or local store read/write errors with `status_code` and `response_text` when applicable - ## Notes - Current implementation omits explicit HTTP retries/timeouts for simplicity. diff --git a/src/sap_cloud_sdk/dms/user-guide.md b/src/sap_cloud_sdk/dms/user-guide.md index 81910de..b761f7a 100644 --- a/src/sap_cloud_sdk/dms/user-guide.md +++ b/src/sap_cloud_sdk/dms/user-guide.md @@ -568,19 +568,33 @@ The SDK extracts the server's error message from JSON responses (the `"message"` ## Configuration -The DMS module automatically resolves credentials from the environment. +### Service Binding -### Cloud Mode +- **Mount path**: `$SERVICE_BINDING_ROOT/sdm/{instance}/` (defaults to `/etc/secrets/appfnd/sdm/{instance}/`) +- **Required Keys**: `uri` (DMS API base URL), `uaa` (JSON string with XSUAA credentials) +- **Env var fallback**: `CLOUD_SDK_CFG_SDM_{INSTANCE}_{FIELD}` (uppercased, hyphens in instance replaced with `_`) -Reads secrets from mounted files or environment variables: -- **Kubernetes-mounted secret** at `/etc/secrets/appfnd/sdm//` -- **Fallback** to environment variables with pattern `CLOUD_SDK_CFG_SDM__` +> **Note:** `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set. See the [Secret Resolver guide](../core/secret_resolver/user-guide.md) for details. -The binding provides: -- `uri`: DMS API base URL -- `uaa`: JSON string with XSUAA credentials (`clientid`, `clientsecret`, `url`, `identityzone`) +#### Mounted Secrets (Kubernetes) -### Service Binding (UAA JSON) +``` +$SERVICE_BINDING_ROOT/sdm/{instance}/ +├── uri +└── uaa +``` + +#### Environment Variables + +```bash +# Example for DMS with instance name "default" +export CLOUD_SDK_CFG_SDM_DEFAULT_URI="https://api.dms.example.com" +export CLOUD_SDK_CFG_SDM_DEFAULT_UAA='{"clientid":"...","clientsecret":"...","url":"https://...","identityzone":"..."}' +``` + +#### UAA JSON Schema + +The `uaa` key must contain a JSON string with the XSUAA credentials: ```json { diff --git a/src/sap_cloud_sdk/objectstore/user-guide.md b/src/sap_cloud_sdk/objectstore/user-guide.md index 5b6a7f5..1e7e669 100644 --- a/src/sap_cloud_sdk/objectstore/user-guide.md +++ b/src/sap_cloud_sdk/objectstore/user-guide.md @@ -214,32 +214,30 @@ except ListObjectsError as e: ## Configuration -This module automatically resolves credentials and configuration from the environment. +### Service Binding -### Cloud Mode +- **Mount path**: `$SERVICE_BINDING_ROOT/objectstore/{instance}/` (defaults to `/etc/secrets/appfnd/objectstore/{instance}/`) +- **Required Keys**: `access_key_id`, `secret_access_key`, `bucket`, `host` +- **Env var fallback**: `CLOUD_SDK_CFG_OBJECTSTORE_{INSTANCE}_{FIELD}` (uppercased, hyphens in instance replaced with `_`) -- Reads secrets from mounted files or environment variables - - **Kubernetes-mounted secret** at `/etc/secrets/appfnd///` - - Fallback to environment variables with pattern `CLOUD_SDK_CFG___` -- Uses the configured S3-compatible host (e.g., AWS S3, MinIO in cloud) -- No manual setup required when deployed in Application Foundation - -#### Environment Variables for Cloud Mode - -```bash -# Example for ObjectStore with instance name "credentials" -export OBJECTSTORE_CREDENTIALS_ACCESS_KEY_ID="your-access-key" -export OBJECTSTORE_CREDENTIALS_SECRET_ACCESS_KEY="your-secret-key" -export OBJECTSTORE_CREDENTIALS_BUCKET="your-bucket-name" -export OBJECTSTORE_CREDENTIALS_HOST="s3.amazonaws.com" -``` +> **Note:** `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set. See the [Secret Resolver guide](../core/secret_resolver/user-guide.md) for details. #### Mounted Secrets (Kubernetes) ``` -/etc/secrets/appfnd/objectstore/credentials/ +$SERVICE_BINDING_ROOT/objectstore/{instance}/ ├── access_key_id ├── secret_access_key ├── bucket └── host ``` + +#### Environment Variables + +```bash +# Example for ObjectStore with instance name "credentials" +export CLOUD_SDK_CFG_OBJECTSTORE_CREDENTIALS_ACCESS_KEY_ID="your-access-key" +export CLOUD_SDK_CFG_OBJECTSTORE_CREDENTIALS_SECRET_ACCESS_KEY="your-secret-key" +export CLOUD_SDK_CFG_OBJECTSTORE_CREDENTIALS_BUCKET="your-bucket-name" +export CLOUD_SDK_CFG_OBJECTSTORE_CREDENTIALS_HOST="s3.amazonaws.com" +``` diff --git a/tests/core/unit/auditlog/unit/test_config.py b/tests/core/unit/auditlog/unit/test_config.py index 460b7e2..63cc797 100644 --- a/tests/core/unit/auditlog/unit/test_config.py +++ b/tests/core/unit/auditlog/unit/test_config.py @@ -201,7 +201,8 @@ def test_load_config_success(self, mock_read): uaa='{"clientid": "test", "clientsecret": "secret", "url": "oauth"}' ) - def mock_read_side_effect(mount_path, env_var, service, instance, binding_data): + def mock_read_side_effect(*args, **kwargs): + binding_data: BindingData = kwargs["target"] binding_data.url = mock_binding.url binding_data.uaa = mock_binding.uaa @@ -215,16 +216,17 @@ def mock_read_side_effect(mount_path, env_var, service, instance, binding_data): assert config.service_url == "https://service.example.com" mock_read.assert_called_once_with( - "/etc/secrets/appfnd", - "CLOUD_SDK_CFG", - "auditlog", - "default", - mock_read.call_args[0][4] + base_volume_mount="/etc/secrets/appfnd", + base_var_name="CLOUD_SDK_CFG", + module="auditlog", + instance="default", + target=mock_read.call_args.kwargs["target"] ) @patch('sap_cloud_sdk.core.secret_resolver.read_from_mount_and_fallback_to_env_var') def test_load_config_validation_error(self, mock_read): - def mock_read_side_effect(mount_path, env_var, service, instance, binding_data): + def mock_read_side_effect(*args, **kwargs): + binding_data: BindingData = kwargs["target"] binding_data.url = "" binding_data.uaa = "" @@ -242,7 +244,8 @@ def test_load_config_read_exception(self, mock_read): @patch('sap_cloud_sdk.core.secret_resolver.read_from_mount_and_fallback_to_env_var') def test_load_config_invalid_uaa(self, mock_read): - def mock_read_side_effect(mount_path, env_var, service, instance, binding_data): + def mock_read_side_effect(*args, **kwargs): + binding_data: BindingData = kwargs["target"] binding_data.url = "https://service.example.com" binding_data.uaa = "invalid json" diff --git a/tests/core/unit/secret_resolver/unit/test_secret_resolver.py b/tests/core/unit/secret_resolver/unit/test_secret_resolver.py index 9bf6fca..8afe074 100644 --- a/tests/core/unit/secret_resolver/unit/test_secret_resolver.py +++ b/tests/core/unit/secret_resolver/unit/test_secret_resolver.py @@ -185,3 +185,33 @@ def test_env_instance_name_hyphen_normalization(self): assert config.username == "env_user_hyphen" assert config.password == "env_pass_hyphen" assert config.endpoint == "env_endpoint_hyphen" + + @patch.dict(os.environ, {"SERVICE_BINDING_ROOT": "/custom/root"}) + @patch('os.path.isdir', return_value=True) + @patch('os.stat') + @patch('builtins.open', new_callable=mock_open) + def test_service_binding_root_overrides_base_mount(self, mock_file, mock_stat, mock_isdir): + mock_file.side_effect = [ + mock_open(read_data="u").return_value, + mock_open(read_data="p").return_value, + mock_open(read_data="e").return_value, + ] + config = SampleConfig() + read_from_mount_and_fallback_to_env_var("/etc/secrets/appfnd", "VAR", "module", "instance", config) + first_call_path = mock_file.call_args_list[0][0][0] + assert first_call_path.startswith("/custom/root") + + @patch.dict(os.environ, {}, clear=True) + @patch('os.path.isdir', return_value=True) + @patch('os.stat') + @patch('builtins.open', new_callable=mock_open) + def test_default_base_mount_used_when_no_service_binding_root(self, mock_file, mock_stat, mock_isdir): + mock_file.side_effect = [ + mock_open(read_data="u").return_value, + mock_open(read_data="p").return_value, + mock_open(read_data="e").return_value, + ] + config = SampleConfig() + read_from_mount_and_fallback_to_env_var("/etc/secrets/appfnd", "VAR", "module", "instance", config) + first_call_path = mock_file.call_args_list[0][0][0] + assert first_call_path.startswith("/etc/secrets/appfnd") diff --git a/uv.lock b/uv.lock index fffc17c..ff7a4a9 100644 --- a/uv.lock +++ b/uv.lock @@ -2398,7 +2398,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.14.2" +version = "0.14.3" source = { editable = "." } dependencies = [ { name = "grpcio" },