11import asyncio
22import heapq
3+ import json
34import time
45from collections .abc import Iterable , Iterator
56from typing import Callable , Coroutine , Dict , List , Optional , Tuple
67from uuid import UUID
78
89from cachetools import TTLCache
10+ from pydantic import Field , ValidationError
911from sqlalchemy import delete , update
1012from sqlalchemy .ext .asyncio import AsyncSession
13+ from typing_extensions import Annotated
1114
1215from dstack ._internal .core .backends .base .backend import Backend
1316from dstack ._internal .core .backends .base .configurator import (
3336 ServerClientError ,
3437)
3538from dstack ._internal .core .models .backends .base import BackendType
39+ from dstack ._internal .core .models .common import CoreModel
3640from dstack ._internal .core .models .instances import (
3741 InstanceOfferWithAvailability ,
3842)
4650logger = get_logger (__name__ )
4751
4852
53+ class _BackendConfigWithCreds (CoreModel ):
54+ __root__ : Annotated [AnyBackendConfigWithCreds , Field (..., discriminator = "type" )]
55+
56+
57+ def serialize_source_backend_config (
58+ config : AnyBackendConfigWithCreds ,
59+ ) -> Tuple [str , Optional [str ]]:
60+ """Split user-intent backend config into non-sensitive and sensitive JSON blobs."""
61+ source_config_dict = config .dict ()
62+ source_auth = source_config_dict .pop ("creds" , None )
63+ source_auth_json = None if source_auth is None else json .dumps (source_auth )
64+ return json .dumps (source_config_dict ), source_auth_json
65+
66+
4967async def create_backend (
5068 session : AsyncSession ,
5169 project : ProjectModel ,
@@ -89,6 +107,8 @@ async def update_backend(
89107 .values (
90108 config = backend .config ,
91109 auth = backend .auth ,
110+ source_config = backend .source_config ,
111+ source_auth = backend .source_auth ,
92112 )
93113 )
94114 return config
@@ -99,6 +119,9 @@ async def validate_and_create_backend_model(
99119 configurator : Configurator ,
100120 config : AnyBackendConfigWithCreds ,
101121) -> BackendModel :
122+ # Configurators may mutate `config` while building the effective stored backend config,
123+ # so capture the user-intent payload before validation/create_backend runs.
124+ source_config , source_auth = serialize_source_backend_config (config )
102125 await run_async (
103126 configurator .validate_config , config , default_creds_enabled = settings .DEFAULT_CREDS_ENABLED
104127 )
@@ -112,6 +135,8 @@ async def validate_and_create_backend_model(
112135 type = configurator .TYPE ,
113136 config = backend_record .config ,
114137 auth = DecryptedString (plaintext = backend_record .auth ),
138+ source_config = source_config ,
139+ source_auth = None if source_auth is None else DecryptedString (plaintext = source_auth ),
115140 )
116141
117142
@@ -134,6 +159,16 @@ async def get_backend_config(
134159 return None
135160
136161
162+ async def get_source_backend_config (
163+ project : ProjectModel ,
164+ backend_type : BackendType ,
165+ ) -> Optional [AnyBackendConfigWithCreds ]:
166+ backend_model = await get_project_backend_model_by_type (project , backend_type )
167+ if backend_model is None :
168+ return None
169+ return get_source_backend_config_from_backend_model (backend_model )
170+
171+
137172def get_backend_config_with_creds_from_backend_model (
138173 configurator : Configurator ,
139174 backend_model : BackendModel ,
@@ -152,6 +187,48 @@ def get_backend_config_without_creds_from_backend_model(
152187 return backend_config
153188
154189
190+ def get_source_backend_config_from_backend_model (
191+ backend_model : BackendModel ,
192+ ) -> Optional [AnyBackendConfigWithCreds ]:
193+ """Reconstruct user-intent backend config from `source_config`/`source_auth`."""
194+
195+ if backend_model .source_config is None :
196+ return None
197+ try :
198+ source_config_dict = json .loads (backend_model .source_config )
199+ except ValueError :
200+ logger .warning (
201+ "Failed to parse source config for %s backend. Falling back to stored config." ,
202+ backend_model .type .value ,
203+ )
204+ return None
205+ if backend_model .source_auth is not None :
206+ if not backend_model .source_auth .decrypted :
207+ logger .warning (
208+ "Failed to decrypt source creds for %s backend. Falling back to stored config." ,
209+ backend_model .type .value ,
210+ )
211+ return None
212+ try :
213+ source_config_dict ["creds" ] = json .loads (
214+ backend_model .source_auth .get_plaintext_or_error ()
215+ )
216+ except ValueError :
217+ logger .warning (
218+ "Failed to parse source creds for %s backend. Falling back to stored config." ,
219+ backend_model .type .value ,
220+ )
221+ return None
222+ try :
223+ return _BackendConfigWithCreds .parse_obj (source_config_dict ).__root__
224+ except ValidationError :
225+ logger .warning (
226+ "Failed to validate source config for %s backend. Falling back to stored config." ,
227+ backend_model .type .value ,
228+ )
229+ return None
230+
231+
155232def get_stored_backend_record (backend_model : BackendModel ) -> StoredBackendRecord :
156233 return StoredBackendRecord (
157234 config = backend_model .config ,
0 commit comments