From ecc30304b7b6375c4eed61af3db3421ae8abc43d Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 10 Oct 2025 11:47:36 +0500 Subject: [PATCH 1/3] Support specifying kubeconfig directly as yaml --- .../_internal/core/backends/kubernetes/compute.py | 4 ++-- .../core/backends/kubernetes/configurator.py | 2 +- .../_internal/core/backends/kubernetes/models.py | 14 +++++++++++--- .../_internal/core/backends/kubernetes/utils.py | 6 ------ .../core/backends/kubernetes/test_configurator.py | 8 ++++---- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/dstack/_internal/core/backends/kubernetes/compute.py b/src/dstack/_internal/core/backends/kubernetes/compute.py index 9668a17f31..de8b91bf43 100644 --- a/src/dstack/_internal/core/backends/kubernetes/compute.py +++ b/src/dstack/_internal/core/backends/kubernetes/compute.py @@ -27,7 +27,7 @@ ) from dstack._internal.core.backends.kubernetes.utils import ( call_api_method, - get_api_from_config_data, + get_api_from_config_dict, get_cluster_public_ip, get_value, ) @@ -99,7 +99,7 @@ def __init__(self, config: KubernetesConfig): if proxy_jump is None: proxy_jump = KubernetesProxyJumpConfig() self.proxy_jump = proxy_jump - self.api = get_api_from_config_data(config.kubeconfig.data) + self.api = get_api_from_config_dict(config.kubeconfig.data) def get_offers_by_requirements( self, requirements: Requirements diff --git a/src/dstack/_internal/core/backends/kubernetes/configurator.py b/src/dstack/_internal/core/backends/kubernetes/configurator.py index 93c9965362..02273d7482 100644 --- a/src/dstack/_internal/core/backends/kubernetes/configurator.py +++ b/src/dstack/_internal/core/backends/kubernetes/configurator.py @@ -30,7 +30,7 @@ def validate_config( self, config: KubernetesBackendConfigWithCreds, default_creds_enabled: bool ): try: - api = kubernetes_utils.get_api_from_config_data(config.kubeconfig.data) + api = kubernetes_utils.get_api_from_config_dict(config.kubeconfig.data) api.list_node() except Exception as e: logger.debug("Invalid kubeconfig: %s", str(e)) diff --git a/src/dstack/_internal/core/backends/kubernetes/models.py b/src/dstack/_internal/core/backends/kubernetes/models.py index 09e505f0af..6dac5c91d1 100644 --- a/src/dstack/_internal/core/backends/kubernetes/models.py +++ b/src/dstack/_internal/core/backends/kubernetes/models.py @@ -1,6 +1,7 @@ from typing import Annotated, Literal, Optional, Union -from pydantic import Field, root_validator +import yaml +from pydantic import Field, root_validator, validator from dstack._internal.core.backends.base.models import fill_data from dstack._internal.core.models.common import CoreModel @@ -19,7 +20,13 @@ class KubernetesProxyJumpConfig(CoreModel): class KubeconfigConfig(CoreModel): filename: Annotated[str, Field(description="The path to the kubeconfig file")] = "" - data: Annotated[str, Field(description="The contents of the kubeconfig file")] + data: Annotated[dict, Field(description="The contents of the kubeconfig file")] + + @validator("data", pre=True) + def convert_data(cls, v: Union[str, dict]) -> dict: + if isinstance(v, dict): + return v + return yaml.load(v, yaml.FullLoader) class KubernetesBackendConfig(CoreModel): @@ -39,7 +46,8 @@ class KubernetesBackendConfigWithCreds(KubernetesBackendConfig): class KubeconfigFileConfig(CoreModel): filename: Annotated[str, Field(description="The path to the kubeconfig file")] data: Annotated[ - Optional[str], + # str data converted to dict when parsed as KubeconfigConfig + Optional[Union[str, dict]], Field( description=( "The contents of the kubeconfig file." diff --git a/src/dstack/_internal/core/backends/kubernetes/utils.py b/src/dstack/_internal/core/backends/kubernetes/utils.py index d50489c8fa..2d84d4ef83 100644 --- a/src/dstack/_internal/core/backends/kubernetes/utils.py +++ b/src/dstack/_internal/core/backends/kubernetes/utils.py @@ -1,7 +1,6 @@ import ast from typing import Any, Callable, List, Literal, Optional, TypeVar, Union, get_origin, overload -import yaml from kubernetes import client as kubernetes_client from kubernetes import config as kubernetes_config from typing_extensions import ParamSpec @@ -10,11 +9,6 @@ P = ParamSpec("P") -def get_api_from_config_data(kubeconfig_data: str) -> kubernetes_client.CoreV1Api: - config_dict = yaml.load(kubeconfig_data, yaml.FullLoader) - return get_api_from_config_dict(config_dict) - - def get_api_from_config_dict(kubeconfig: dict) -> kubernetes_client.CoreV1Api: api_client = kubernetes_config.new_client_from_config_dict(config_dict=kubeconfig) return kubernetes_client.CoreV1Api(api_client=api_client) diff --git a/src/tests/_internal/core/backends/kubernetes/test_configurator.py b/src/tests/_internal/core/backends/kubernetes/test_configurator.py index 3605b82fc3..ce99faa769 100644 --- a/src/tests/_internal/core/backends/kubernetes/test_configurator.py +++ b/src/tests/_internal/core/backends/kubernetes/test_configurator.py @@ -16,11 +16,11 @@ class TestKubernetesConfigurator: def test_validate_config_valid(self): config = KubernetesBackendConfigWithCreds( - kubeconfig=KubeconfigConfig(data="valid", filename="-"), + kubeconfig=KubeconfigConfig(data={"config": "valid"}, filename="-"), proxy_jump=KubernetesProxyJumpConfig(hostname=None, port=None), ) with patch( - "dstack._internal.core.backends.kubernetes.utils.get_api_from_config_data" + "dstack._internal.core.backends.kubernetes.utils.get_api_from_config_dict" ) as get_api_mock: api_mock = Mock() api_mock.list_node.return_value = Mock() @@ -29,12 +29,12 @@ def test_validate_config_valid(self): def test_validate_config_invalid_config(self): config = KubernetesBackendConfigWithCreds( - kubeconfig=KubeconfigConfig(data="invalid", filename="-"), + kubeconfig=KubeconfigConfig(data={"config": "invalid"}, filename="-"), proxy_jump=KubernetesProxyJumpConfig(hostname=None, port=None), ) with ( patch( - "dstack._internal.core.backends.kubernetes.utils.get_api_from_config_data" + "dstack._internal.core.backends.kubernetes.utils.get_api_from_config_dict" ) as get_api_mock, pytest.raises(BackendInvalidCredentialsError) as exc_info, ): From 490efd4b7c2ee64bdf5ba87d8f2a62c42b7636cc Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 10 Oct 2025 11:56:18 +0500 Subject: [PATCH 2/3] Document kubeconfig as yaml --- docs/docs/reference/server/config.yml.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/docs/reference/server/config.yml.md b/docs/docs/reference/server/config.yml.md index 995507de2c..90ebfcc7f4 100644 --- a/docs/docs/reference/server/config.yml.md +++ b/docs/docs/reference/server/config.yml.md @@ -287,12 +287,28 @@ to configure [backends](../../concepts/backends.md) and other [server-level sett show_root_heading: false ??? info "Specifying `data`" - To specify kubeconfig contents directly via `data`, convert it to a string: + To specify kubeconfig contents directly via `data`, you can convert it to a string: ```shell yq -o=json ~/.kube/config | jq -c | jq -R ``` + or copy kubeconfig contents under `data` as-is: + + ```yaml + type: kubernetes + kubeconfig: + data: + apiVersion: v1 + clusters: + - cluster: + # ... + contexts: + - context: + # ... + # ... + ``` + ###### `projects[n].backends[type=kubernetes].proxy_jump` { #kubernetes-proxy_jump data-toc-label="proxy_jump" } #SCHEMA# dstack._internal.core.backends.kubernetes.models.KubernetesProxyJumpConfig From 46f7d7d2f347e2a9c42c9f8f7857367b8ac35f38 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 10 Oct 2025 11:58:03 +0500 Subject: [PATCH 3/3] Update data description --- src/dstack/_internal/core/backends/kubernetes/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dstack/_internal/core/backends/kubernetes/models.py b/src/dstack/_internal/core/backends/kubernetes/models.py index 6dac5c91d1..ebc91c6173 100644 --- a/src/dstack/_internal/core/backends/kubernetes/models.py +++ b/src/dstack/_internal/core/backends/kubernetes/models.py @@ -50,7 +50,7 @@ class KubeconfigFileConfig(CoreModel): Optional[Union[str, dict]], Field( description=( - "The contents of the kubeconfig file." + "The contents of the kubeconfig file specified as yaml or a string." " When configuring via `server/config.yml`, it's automatically filled from `filename`." " When configuring via UI, it has to be specified explicitly" )