Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/dstack/_internal/core/backends/aws/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ def terminate_gateway(
return

try:
backend_data_parsed = AWSGatewayBackendData.parse_raw(backend_data)
backend_data_parsed = AWSGatewayBackendData.__response__.parse_raw(backend_data)
except ValidationError:
logger.exception(
"Failed to terminate all gateway %s resources. backend_data parsing error.",
Expand Down Expand Up @@ -1321,7 +1321,7 @@ def _parse_instance_backend_data(backend_data: Optional[str]) -> "AWSInstanceBac
if backend_data is None:
return AWSInstanceBackendData()
try:
return AWSInstanceBackendData.parse_raw(backend_data)
return AWSInstanceBackendData.__response__.parse_raw(backend_data)
except ValidationError:
logger.exception("Failed to parse AWS instance backend_data; treating as empty")
return AWSInstanceBackendData()
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/aws/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def get_backend(self, record: BackendRecord) -> AWSBackend:
def _get_config(self, record: BackendRecord) -> AWSConfig:
return AWSConfig.__response__(
**json.loads(record.config),
creds=AWSCreds.parse_raw(record.auth).__root__,
creds=AWSCreds.__response__.parse_raw(record.auth).__root__,
)

def _check_config_tags(self, config: AWSBackendConfigWithCreds):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/aws/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class AWSStoredConfig(AWSBackendConfig):


class AWSConfig(AWSStoredConfig):
creds: AnyAWSCreds
creds: Annotated[AnyAWSCreds, Field(discriminator="type")]

@property
def allocate_public_ips(self) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/azure/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def _get_config(self, record: BackendRecord) -> AzureConfig:
return AzureConfig.__response__(
**config_dict,
regions=regions,
creds=AzureCreds.parse_raw(record.auth).__root__,
creds=AzureCreds.__response__.parse_raw(record.auth).__root__,
)

def _check_config_tenant_id(
Expand Down
4 changes: 2 additions & 2 deletions src/dstack/_internal/core/backends/azure/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class AzureClientCreds(CoreModel):
client_id: Annotated[str, Field(description="The client ID")]
client_secret: Annotated[str, Field(description="The client secret")]
# if tenant_id is missing, it will be populated from config info
tenant_id: Optional[str]
tenant_id: Optional[str] = None


class AzureDefaultCreds(CoreModel):
Expand Down Expand Up @@ -99,7 +99,7 @@ class AzureStoredConfig(AzureBackendConfig):


class AzureConfig(AzureStoredConfig):
creds: AnyAzureCreds
creds: Annotated[AnyAzureCreds, Field(discriminator="type")]

@property
def allocate_public_ips(self) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def get_backend(self, record: BackendRecord) -> CloudRiftBackend:
def _get_config(self, record: BackendRecord) -> CloudRiftConfig:
return CloudRiftConfig.__response__(
**json.loads(record.config),
creds=CloudRiftCreds.parse_raw(record.auth),
creds=CloudRiftCreds.__response__.parse_raw(record.auth),
)

def _validate_creds(self, creds: AnyCloudRiftCreds):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/crusoe/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ def get_backend(self, record: BackendRecord) -> CrusoeBackend:
def _get_config(self, record: BackendRecord) -> CrusoeConfig:
return CrusoeConfig.__response__(
**json.loads(record.config),
creds=CrusoeCreds.parse_raw(record.auth),
creds=CrusoeCreds.__response__.parse_raw(record.auth),
)
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def get_backend(self, record: BackendRecord) -> BaseDigitalOceanBackend:
def _get_config(self, record: BackendRecord) -> BaseDigitalOceanConfig:
return BaseDigitalOceanConfig.__response__(
**json.loads(record.config),
creds=BaseDigitalOceanCreds.parse_raw(record.auth),
creds=BaseDigitalOceanCreds.__response__.parse_raw(record.auth),
)

def _validate_creds(self, creds: AnyBaseDigitalOceanCreds, project_name: Optional[str] = None):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/gcp/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def get_backend(self, record: BackendRecord) -> GCPBackend:
def _get_config(self, record: BackendRecord) -> GCPConfig:
return GCPConfig.__response__(
**json.loads(record.config),
creds=GCPCreds.parse_raw(record.auth).__root__,
creds=GCPCreds.__response__.parse_raw(record.auth).__root__,
)

def _check_config_tags(self, config: GCPBackendConfigWithCreds):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/gcp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class GCPStoredConfig(GCPBackendConfig):


class GCPConfig(GCPStoredConfig):
creds: AnyGCPCreds
creds: Annotated[AnyGCPCreds, Field(discriminator="type")]

@property
def allocate_public_ips(self) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def get_backend(self, record: BackendRecord) -> HotAisleBackend:
def _get_config(self, record: BackendRecord) -> HotAisleConfig:
return HotAisleConfig.__response__(
**json.loads(record.config),
creds=HotAisleCreds.parse_raw(record.auth),
creds=HotAisleCreds.__response__.parse_raw(record.auth),
)

def _validate_creds(self, creds: AnyHotAisleCreds, team_handle: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def get_backend(self, record: BackendRecord) -> JarvisLabsBackend:
def _get_config(self, record: BackendRecord) -> JarvisLabsConfig:
return JarvisLabsConfig.__response__(
**json.loads(record.config),
creds=JarvisLabsCreds.parse_raw(record.auth),
creds=JarvisLabsCreds.__response__.parse_raw(record.auth),
)

def _validate_api_key(self, api_key: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def get_backend(self, record: BackendRecord) -> LambdaBackend:
def _get_config(self, record: BackendRecord) -> LambdaConfig:
return LambdaConfig.__response__(
**json.loads(record.config),
creds=LambdaCreds.parse_raw(record.auth),
creds=LambdaCreds.__response__.parse_raw(record.auth),
)

def _validate_lambda_api_key(self, api_key: str):
Expand Down
41 changes: 40 additions & 1 deletion src/dstack/_internal/core/backends/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Union
from typing import Annotated, Union

from pydantic import Field

from dstack._internal.core.backends.aws.models import (
AWSBackendConfig,
Expand Down Expand Up @@ -140,6 +142,43 @@
DstackBackendConfig,
]

# Permissive counterpart of `AnyBackendConfigWithCreds` for parsing server responses.
# A newer server may add config fields that an older client's models don't know about;
# parsing with the strict variant would reject the response outright.
#
# Discriminated on `type`: without it, arm selection would depend on trying each of the 20
# arms in order, which only works because every arm happens to declare a `Literal` type.
# `AnyBackendConfigWithCreds` above stays a bare `Union` on purpose. Its two server-side users apply
# `Field(discriminator="type")` at the point of use, which is fine against a bare alias.
# Baking the discriminator into the alias would turn those into doubled `Annotated` `Field`s and
# fail with `ValueError: cannot specify multiple 'Annotated' 'Field's`.
# Discriminating here is because nothing else wraps this alias.
AnyBackendConfigWithCredsResponse = Annotated[
Union[
AWSBackendConfigWithCreds.__response__,
AzureBackendConfigWithCreds.__response__,
CloudRiftBackendConfigWithCreds.__response__,
CrusoeBackendConfigWithCreds.__response__,
CudoBackendConfigWithCreds.__response__,
VerdaBackendConfigWithCreds.__response__,
BaseDigitalOceanBackendConfigWithCreds.__response__,
GCPBackendConfigWithCreds.__response__,
HotAisleBackendConfigWithCreds.__response__,
JarvisLabsBackendConfigWithCreds.__response__,
KubernetesBackendConfigWithCreds.__response__,
LambdaBackendConfigWithCreds.__response__,
OCIBackendConfigWithCreds.__response__,
NebiusBackendConfigWithCreds.__response__,
RunpodBackendConfigWithCreds.__response__,
TensorDockBackendConfigWithCreds.__response__,
VastAIBackendConfigWithCreds.__response__,
VultrBackendConfigWithCreds.__response__,
SlurmBackendConfigWithCreds.__response__,
DstackBackendConfig.__response__,
],
Field(discriminator="type"),
]

# Backend config accepted in server/config.yaml.
# This can be different from the API config.
# For example, it can make creds data optional and resolve it by filename.
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/nebius/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ class NebiusClusterBackendData(CoreModel):


class NebiusPlacementGroupBackendData(CoreModel):
cluster: Optional[NebiusClusterBackendData]
cluster: Optional[NebiusClusterBackendData] = None

@classmethod
def load(cls, raw: Optional[str]) -> "NebiusPlacementGroupBackendData":
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/nebius/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,5 @@ def get_backend(self, record: BackendRecord) -> NebiusBackend:
def _get_config(self, record: BackendRecord) -> NebiusConfig:
return NebiusConfig.__response__(
**json.loads(record.config),
creds=NebiusCreds.parse_raw(record.auth),
creds=NebiusCreds.__response__.parse_raw(record.auth),
)
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/oci/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def get_backend(self, record: BackendRecord) -> OCIBackend:
def _get_config(self, record: BackendRecord) -> OCIConfig:
return OCIConfig.__response__(
**json.loads(record.config),
creds=OCICreds.parse_raw(record.auth).__root__,
creds=OCICreds.__response__.parse_raw(record.auth).__root__,
)


Expand Down
8 changes: 4 additions & 4 deletions src/dstack/_internal/core/backends/oci/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ class OCIClientCreds(CoreModel):
Field(
description="Path to the user's private PEM key. Either this or `key_content` should be set"
),
]
] = None
key_content: Annotated[
Optional[str],
Field(
description="Content of the user's private PEM key. Either this or `key_file` should be set"
),
]
] = None
pass_phrase: Annotated[
Optional[str], Field(description="Passphrase for the private PEM key if it is encrypted")
]
] = None
fingerprint: Annotated[str, Field(description="User's public key fingerprint")]
region: Annotated[
str, Field(description="Name or key of any region the tenancy is subscribed to")
Expand Down Expand Up @@ -84,4 +84,4 @@ class OCIStoredConfig(OCIBackendConfig):


class OCIConfig(OCIStoredConfig):
creds: AnyOCICreds
creds: Annotated[AnyOCICreds, Field(discriminator="type")]
4 changes: 2 additions & 2 deletions src/dstack/_internal/core/backends/runpod/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ def get_backend(self, record: BackendRecord) -> RunpodBackend:
return RunpodBackend(config=config)

def _get_config(self, record: BackendRecord) -> RunpodConfig:
return RunpodConfig(
return RunpodConfig.__response__(
**json.loads(record.config),
creds=RunpodCreds.parse_raw(record.auth),
creds=RunpodCreds.__response__.parse_raw(record.auth),
)

def _validate_runpod_api_key(self, api_key: str):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/vastai/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def get_backend(self, record: BackendRecord) -> VastAIBackend:
def _get_config(self, record: BackendRecord) -> VastAIConfig:
return VastAIConfig.__response__(
**json.loads(record.config),
creds=VastAICreds.parse_raw(record.auth),
creds=VastAICreds.__response__.parse_raw(record.auth),
)

def _validate_vastai_creds(self, api_key: str):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/verda/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def get_backend(self, record: BackendRecord) -> VerdaBackend:
def _get_config(self, record: BackendRecord) -> VerdaConfig:
return VerdaConfig.__response__(
**json.loads(record.config),
creds=VerdaCreds.parse_raw(record.auth),
creds=VerdaCreds.__response__.parse_raw(record.auth),
)

def _validate_creds(self, creds: VerdaCreds):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/backends/vultr/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def get_backend(self, record: BackendRecord) -> VultrBackend:
def _get_config(self, record: BackendRecord) -> VultrConfig:
return VultrConfig.__response__(
**json.loads(record.config),
creds=VultrCreds.parse_raw(record.auth),
creds=VultrCreds.__response__.parse_raw(record.auth),
)

def _validate_vultr_api_key(self, api_key: str):
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class EntityReference(CoreModel):
project: Annotated[
Optional[str],
Field(description="The project name. If unspecified, refers to the current project"),
]
] = None
name: Annotated[str, Field(description="The entity name")]

@classmethod
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ProjectConfig(CoreModel):
name: str
url: str
token: str
default: Optional[bool]
default: Optional[bool] = None


# Not used since 0.20.0. Can be removed when most users update their `config.yml` (it's updated
Expand Down
9 changes: 7 additions & 2 deletions src/dstack/_internal/core/models/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,7 @@ class ReplicaGroup(CoreModel):
Field(
description="The name of the replica group. If not provided, defaults to '0', '1', etc. based on position."
),
]
] = None
count: Annotated[
Range[int],
Field(
Expand Down Expand Up @@ -1015,15 +1015,20 @@ class ServiceConfigurationParams(CoreModel):
)
),
] = STRIP_PREFIX_DEFAULT
# A discriminator cannot read `format` off the documented `model: <name>` shorthand, so it
# would reject it. This works only because the `convert_model` pre-validator below expands a
# bare string into an `OpenAIChatModel` first — leaving the union something that carries
# `format` either way: an attribute on the expanded model, or a key in a user-supplied mapping.
model: Annotated[
Optional[AnyModel],
Field(
discriminator="format",
description=(
"Mapping of the model for the OpenAI-compatible endpoint provided by `dstack`."
" Can be a full model format definition or just a model name."
" If it's a name, the service is expected to expose an OpenAI-compatible"
" API at the `/v1` path"
)
),
),
] = None
https: Annotated[
Expand Down
8 changes: 4 additions & 4 deletions src/dstack/_internal/core/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class EventTarget(CoreModel):
" or `null` for target types not bound to a project (e.g., users)"
)
),
]
] = None
project_name: Annotated[
Optional[str],
Field(
Expand All @@ -48,7 +48,7 @@ class EventTarget(CoreModel):
" or `null` for target types not bound to a project (e.g., users)"
)
),
]
] = None
is_project_deleted: Annotated[
Optional[bool],
Field(
Expand All @@ -74,7 +74,7 @@ class Event(CoreModel):
" or `null` if the action was performed by the system"
)
),
]
] = None
actor_user: Annotated[
Optional[str],
Field(
Expand All @@ -83,7 +83,7 @@ class Event(CoreModel):
" or `null` if the action was performed by the system"
)
),
]
] = None
is_actor_user_deleted: Annotated[
Optional[bool],
Field(
Expand Down
11 changes: 6 additions & 5 deletions src/dstack/_internal/core/models/gateways.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ class GatewayConfiguration(CoreModel):
certificate: Annotated[
Optional[AnyGatewayCertificate],
Field(
discriminator="type",
description="The SSL certificate configuration."
" Set to `null` to disable. Defaults to `type: lets-encrypt`"
" Set to `null` to disable. Defaults to `type: lets-encrypt`",
),
] = LetsEncryptGatewayCertificate()
replicas: Annotated[
Expand Down Expand Up @@ -148,13 +149,13 @@ class Gateway(CoreModel):
configuration: GatewayConfiguration
created_at: datetime.datetime
status: GatewayStatus
status_message: Optional[str]
hostname: Optional[str]
status_message: Optional[str] = None
hostname: Optional[str] = None
"""Hostname of the load balancer.
Unset if there is no load balancer, in which case users are expected to point the gateway's
wildcard domain name to `replicas[i].hostname`.
"""
wildcard_domain: Optional[str]
wildcard_domain: Optional[str] = None
default: bool
replicas: list[GatewayReplica] = []
backend: Optional[BackendType] = None
Expand Down Expand Up @@ -203,7 +204,7 @@ class GatewayComputeConfiguration(CoreModel):
instance_type: Optional[str] = None
public_ip: bool
ssh_key_pub: str
certificate: Optional[AnyGatewayCertificate] = None
certificate: Annotated[Optional[AnyGatewayCertificate], Field(discriminator="type")] = None
tags: Optional[Dict[str, str]] = None
router: Optional[AnyGatewayRouterConfig] = None

Expand Down
Loading
Loading