Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/sap_cloud_sdk/adms/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
_DocumentRelationApi,
)
from sap_cloud_sdk.adms._token_cache import TokenCache
from sap_cloud_sdk.adms.config import AdmsConfig, load_from_env_or_mount
from sap_cloud_sdk.adms.config import AdmsConfig, load_secrets


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -171,7 +171,7 @@ def create_client(
raise ValueError(
"instance must not be an empty string; omit it to use 'default'"
)
binding = config or load_from_env_or_mount(instance)
binding = config or load_secrets(instance)
token_fetcher = IasTokenFetcher(config=binding, cache=token_cache)
http = AdmsHttp(config=binding, token_fetcher=token_fetcher, user_jwt=user_jwt)
return AdmsClient(http)
Expand Down Expand Up @@ -206,7 +206,7 @@ def create_async_client(
raise ValueError(
"instance must not be an empty string; omit it to use 'default'"
)
binding = config or load_from_env_or_mount(instance)
binding = config or load_secrets(instance)
token_fetcher = IasTokenFetcher(config=binding, cache=token_cache)
http = AsyncAdmsHttp(
config=binding,
Expand Down
15 changes: 4 additions & 11 deletions src/sap_cloud_sdk/adms/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@

from dataclasses import dataclass

from sap_cloud_sdk.core.secret_resolver.resolver import (
read_from_mount_and_fallback_to_env_var,
)
from sap_cloud_sdk.adms.exceptions import ConfigError
from sap_cloud_sdk.core.secret_resolver import get_resolver

_DEFAULT_INSTANCE = "default"
_SECRET_MOUNT_BASE = "/etc/secrets/appfnd"
Expand Down Expand Up @@ -96,7 +94,7 @@ def to_config(self) -> AdmsConfig:
)


def load_from_env_or_mount(instance: str | None = None) -> AdmsConfig:
def load_secrets(instance: str | None = None) -> AdmsConfig:
"""Load ADMS configuration from a mounted secret volume or environment variables.

Args:
Expand All @@ -111,13 +109,8 @@ def load_from_env_or_mount(instance: str | None = None) -> AdmsConfig:
instance = instance or _DEFAULT_INSTANCE
raw = _BindingData()
try:
read_from_mount_and_fallback_to_env_var(
base_volume_mount=_SECRET_MOUNT_BASE,
base_var_name=_ENV_VAR_BASE,
module="adms",
instance=instance,
target=raw,
)
get_resolver().resolve(module="adms", instance=instance, target=raw)

except Exception as exc:
raise ConfigError(
f"failed to load ADMS binding for instance '{instance}': {exc}"
Expand Down
4 changes: 2 additions & 2 deletions src/sap_cloud_sdk/agent_memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from sap_cloud_sdk.agent_memory._http_transport import HttpTransport
from sap_cloud_sdk.agent_memory.client import AgentMemoryClient
from sap_cloud_sdk.agent_memory.config import AgentMemoryConfig, _load_config_from_env
from sap_cloud_sdk.agent_memory.config import AgentMemoryConfig, _load_secrets
from sap_cloud_sdk.agent_memory.exceptions import (
AgentMemoryConfigError,
AgentMemoryError,
Expand Down Expand Up @@ -49,7 +49,7 @@ def create_client(*, config: Optional[AgentMemoryConfig] = None) -> AgentMemoryC
AgentMemoryConfigError: If configuration is missing or invalid.
"""
try:
resolved_config = config if config is not None else _load_config_from_env()
resolved_config = config if config is not None else _load_secrets()
transport = HttpTransport(resolved_config)
return AgentMemoryClient(transport)
except AgentMemoryConfigError:
Expand Down
16 changes: 5 additions & 11 deletions src/sap_cloud_sdk/agent_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import Optional

from sap_cloud_sdk.agent_memory.exceptions import AgentMemoryConfigError
from sap_cloud_sdk.core.secret_resolver import get_resolver


@dataclass
Expand Down Expand Up @@ -113,7 +114,7 @@ def extract_config(self) -> AgentMemoryConfig:
raise AgentMemoryConfigError(f"Missing required field in uaa JSON: {e}")


def _load_config_from_env() -> AgentMemoryConfig:
def _load_secrets() -> AgentMemoryConfig:
"""Load Agent Memory configuration from a mounted volume or environment variables.

Uses the secret resolver with fallback order:
Expand All @@ -126,19 +127,12 @@ def _load_config_from_env() -> AgentMemoryConfig:
Raises:
AgentMemoryConfigError: If configuration cannot be loaded or is incomplete.
"""
from sap_cloud_sdk.core.secret_resolver import (
read_from_mount_and_fallback_to_env_var,
)

try:
binding = BindingData()
read_from_mount_and_fallback_to_env_var(
base_volume_mount="/etc/secrets/appfnd",
base_var_name="CLOUD_SDK_CFG",
module="hana-agent-memory",
instance="default",
target=binding,
get_resolver().resolve(
module="agent_memory", instance="default", target=binding
)

binding.validate()
return binding.extract_config()
except AgentMemoryConfigError:
Expand Down
4 changes: 2 additions & 2 deletions src/sap_cloud_sdk/core/auditlog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
ChangeAttribute,
DeletedAttribute,
)
from sap_cloud_sdk.core.auditlog.config import AuditLogConfig, _load_config_from_env
from sap_cloud_sdk.core.auditlog.config import AuditLogConfig, _load_secrets
from sap_cloud_sdk.core.auditlog.exceptions import (
AuditLogError,
ClientCreationError,
Expand Down Expand Up @@ -67,7 +67,7 @@ def create_client(
transport = HttpTransport(config)
return AuditLogClient(transport, _telemetry_source=_telemetry_source)

transport = HttpTransport(_load_config_from_env())
transport = HttpTransport(_load_secrets())
return AuditLogClient(transport, _telemetry_source=_telemetry_source)

except Exception as e:
Expand Down
14 changes: 4 additions & 10 deletions src/sap_cloud_sdk/core/auditlog/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dataclasses import dataclass

from sap_cloud_sdk.core.auditlog.exceptions import ClientCreationError
from sap_cloud_sdk.core.secret_resolver import get_resolver


@dataclass
Expand Down Expand Up @@ -85,7 +86,7 @@ def extract_config(self) -> AuditLogConfig:
raise ClientCreationError(f"Missing required field in UAA JSON: {e}")


def _load_config_from_env() -> AuditLogConfig:
def _load_secrets() -> AuditLogConfig:
"""Load audit log configuration from environment/mounts.

Uses the secret resolver to load configuration from:
Expand All @@ -100,20 +101,13 @@ def _load_config_from_env() -> AuditLogConfig:
Raises:
ClientCreationError: If loading or parsing fails
"""
from sap_cloud_sdk.core.secret_resolver import (
read_from_mount_and_fallback_to_env_var,
)

try:
# Load raw config data using secret resolver
binding_data: BindingData = BindingData("", "")

read_from_mount_and_fallback_to_env_var(
base_volume_mount="/etc/secrets/appfnd",
base_var_name="CLOUD_SDK_CFG",
module="auditlog",
instance="default",
target=binding_data,
get_resolver().resolve(
module="auditlog", instance="default", target=binding_data
)

binding_data.validate()
Expand Down
4 changes: 2 additions & 2 deletions src/sap_cloud_sdk/core/data_anonymization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from sap_cloud_sdk.core.data_anonymization.client import DataAnonymizationClient
from sap_cloud_sdk.core.data_anonymization.config import (
DataAnonymizationConfig,
_load_config_from_env,
_load_secrets,
)
from sap_cloud_sdk.core.data_anonymization._http_transport import HttpTransport
from sap_cloud_sdk.core.data_anonymization.models import (
Expand Down Expand Up @@ -101,7 +101,7 @@ def create_client(
never includes configuration values or processed user content.
"""
try:
resolved = config if config is not None else _load_config_from_env(instance)
resolved = config if config is not None else _load_secrets(instance)
transport = HttpTransport(resolved)
return DataAnonymizationClient(transport, _telemetry_source=_telemetry_source)
except Exception as e:
Expand Down
14 changes: 4 additions & 10 deletions src/sap_cloud_sdk/core/data_anonymization/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import Optional

from sap_cloud_sdk.core.data_anonymization.exceptions import ClientCreationError
from sap_cloud_sdk.core.secret_resolver import get_resolver


@dataclass
Expand Down Expand Up @@ -111,7 +112,7 @@ def extract_config(self) -> DataAnonymizationConfig:
)


def _load_config_from_env(instance: str = "default") -> DataAnonymizationConfig:
def _load_secrets(instance: str = "default") -> DataAnonymizationConfig:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this encapsulation is not needed. We could also have an central enum for module names if we need it, instead of magic strings. What do you think?

"""Load anonymization config from environment / mount path.

Uses the secret resolver to read from:
Expand All @@ -126,18 +127,11 @@ def _load_config_from_env(instance: str = "default") -> DataAnonymizationConfig:
Raises:
ClientCreationError: If loading or parsing fails.
"""
from sap_cloud_sdk.core.secret_resolver import (
read_from_mount_and_fallback_to_env_var,
)

try:
binding = _BindingData("")
read_from_mount_and_fallback_to_env_var(
"/etc/secrets/appfnd",
"CLOUD_SDK_CFG",
"data-anonymization",
instance,
binding,
get_resolver().resolve(
module="data-anonymization", instance=instance, target=binding
)
binding.validate()
return binding.extract_config()
Expand Down
66 changes: 52 additions & 14 deletions src/sap_cloud_sdk/core/secret_resolver/__init__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
"""
Secret resolver: load configuration/secrets from mounted files or environment variables
Secret resolver: load configuration/secrets from mounted files or environment variables.

Usage:
from dataclasses import dataclass, field
from sap_cloud_sdk.secret_resolver import read_from_mount_and_fallback_to_env_var
Built-in resolvers and chain builder::

@dataclass
class MyConfig:
username: str = field(metadata={"secret": "username"})
password: str = field(metadata={"secret": "password"})
endpoint: str = "http://localhost"
from sap_cloud_sdk.core.secret_resolver import (
MountResolver,
EnvVarResolver,
ChainedResolver,
)

# Build a chain explicitly
resolver = ChainedResolver([MountResolver(), EnvVarResolver()])
resolver.resolve("destination", "default", binding)

Legacy function-based API (still supported)::

from sap_cloud_sdk.core.secret_resolver import read_from_mount_and_fallback_to_env_var

cfg = MyConfig()
read_from_mount_and_fallback_to_env_var(
base_volume_mount="/etc/secrets/appfnd",
base_var_name="CLOUD_SDK_CFG",
module="objectstore",
module="destination",
instance="default",
target=cfg
target=binding,
)
"""

from .resolver import read_from_mount_and_fallback_to_env_var, resolve_base_mount
from sap_cloud_sdk.core.secret_resolver.resolver import (
read_from_mount_and_fallback_to_env_var,
)
from sap_cloud_sdk.core.secret_resolver._resolvers import (
Resolver,
ChainedResolver,
)

from sap_cloud_sdk.core.secret_resolver.mount_resolver import (
MountResolver,
resolve_base_mount,
)
from sap_cloud_sdk.core.secret_resolver.env_resolver import EnvVarResolver

from sap_cloud_sdk.core.secret_resolver.sdk_config import (
SdkConfig,
configure,
get_sdk_config,
get_resolver,
)

__all__ = ["read_from_mount_and_fallback_to_env_var", "resolve_base_mount"]
__all__ = [
# Class-based API
"Resolver",
"MountResolver",
"EnvVarResolver",
"ChainedResolver",
# Global configuration
"SdkConfig",
"configure",
"get_sdk_config",
"get_resolver",
# Legacy function-based API
"read_from_mount_and_fallback_to_env_var",
"resolve_base_mount",
]
32 changes: 32 additions & 0 deletions src/sap_cloud_sdk/core/secret_resolver/_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Utilities for mapping dataclass fields to secret store keys."""

from typing import Any, Dict, Tuple
from dataclasses import fields, is_dataclass


def _get_field_map(target: Any) -> dict[str, tuple[str, type]]:
"""
Build a mapping from secret key -> (attribute_name, attribute_type) for a dataclass instance.

Priority:
1. Use field.metadata["secret"] if present as the key
2. Fallback to the lowercase dataclass field name
Only string-typed fields are supported.
"""
if not is_dataclass(target) or isinstance(target, type):
raise TypeError("target must be a dataclass instance")

mapping: Dict[str, Tuple[str, type]] = {}
for f in fields(target):
# Only support string fields for secrets (consistent with Go SDK)
# Allow plain 'str' annotations; reject others to keep behavior predictable
if f.type is not str:
raise TypeError(
f"target field '{f.name}' is not a string (only str fields are supported)"
)
key = f.metadata.get("secret") if hasattr(f, "metadata") else None
if key and isinstance(key, str) and key.strip():
mapping[key] = (f.name, f.type)
else:
mapping[f.name.lower()] = (f.name, f.type)
return mapping
Loading
Loading