kubetorch 0.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. kubetorch/__init__.py +59 -0
  2. kubetorch/cli.py +1939 -0
  3. kubetorch/cli_utils.py +967 -0
  4. kubetorch/config.py +453 -0
  5. kubetorch/constants.py +18 -0
  6. kubetorch/docs/Makefile +18 -0
  7. kubetorch/docs/__init__.py +0 -0
  8. kubetorch/docs/_ext/json_globaltoc.py +42 -0
  9. kubetorch/docs/api/cli.rst +10 -0
  10. kubetorch/docs/api/python/app.rst +21 -0
  11. kubetorch/docs/api/python/cls.rst +19 -0
  12. kubetorch/docs/api/python/compute.rst +25 -0
  13. kubetorch/docs/api/python/config.rst +11 -0
  14. kubetorch/docs/api/python/fn.rst +19 -0
  15. kubetorch/docs/api/python/image.rst +14 -0
  16. kubetorch/docs/api/python/secret.rst +18 -0
  17. kubetorch/docs/api/python/volumes.rst +13 -0
  18. kubetorch/docs/api/python.rst +101 -0
  19. kubetorch/docs/conf.py +69 -0
  20. kubetorch/docs/index.rst +20 -0
  21. kubetorch/docs/requirements.txt +5 -0
  22. kubetorch/globals.py +269 -0
  23. kubetorch/logger.py +59 -0
  24. kubetorch/resources/__init__.py +0 -0
  25. kubetorch/resources/callables/__init__.py +0 -0
  26. kubetorch/resources/callables/cls/__init__.py +0 -0
  27. kubetorch/resources/callables/cls/cls.py +159 -0
  28. kubetorch/resources/callables/fn/__init__.py +0 -0
  29. kubetorch/resources/callables/fn/fn.py +140 -0
  30. kubetorch/resources/callables/module.py +1315 -0
  31. kubetorch/resources/callables/utils.py +203 -0
  32. kubetorch/resources/compute/__init__.py +0 -0
  33. kubetorch/resources/compute/app.py +253 -0
  34. kubetorch/resources/compute/compute.py +2414 -0
  35. kubetorch/resources/compute/decorators.py +137 -0
  36. kubetorch/resources/compute/utils.py +1026 -0
  37. kubetorch/resources/compute/websocket.py +135 -0
  38. kubetorch/resources/images/__init__.py +1 -0
  39. kubetorch/resources/images/image.py +412 -0
  40. kubetorch/resources/images/images.py +64 -0
  41. kubetorch/resources/secrets/__init__.py +2 -0
  42. kubetorch/resources/secrets/kubernetes_secrets_client.py +377 -0
  43. kubetorch/resources/secrets/provider_secrets/__init__.py +0 -0
  44. kubetorch/resources/secrets/provider_secrets/anthropic_secret.py +12 -0
  45. kubetorch/resources/secrets/provider_secrets/aws_secret.py +16 -0
  46. kubetorch/resources/secrets/provider_secrets/azure_secret.py +14 -0
  47. kubetorch/resources/secrets/provider_secrets/cohere_secret.py +12 -0
  48. kubetorch/resources/secrets/provider_secrets/gcp_secret.py +16 -0
  49. kubetorch/resources/secrets/provider_secrets/github_secret.py +13 -0
  50. kubetorch/resources/secrets/provider_secrets/huggingface_secret.py +20 -0
  51. kubetorch/resources/secrets/provider_secrets/kubeconfig_secret.py +12 -0
  52. kubetorch/resources/secrets/provider_secrets/lambda_secret.py +13 -0
  53. kubetorch/resources/secrets/provider_secrets/langchain_secret.py +12 -0
  54. kubetorch/resources/secrets/provider_secrets/openai_secret.py +11 -0
  55. kubetorch/resources/secrets/provider_secrets/pinecone_secret.py +12 -0
  56. kubetorch/resources/secrets/provider_secrets/providers.py +92 -0
  57. kubetorch/resources/secrets/provider_secrets/ssh_secret.py +12 -0
  58. kubetorch/resources/secrets/provider_secrets/wandb_secret.py +11 -0
  59. kubetorch/resources/secrets/secret.py +224 -0
  60. kubetorch/resources/secrets/secret_factory.py +64 -0
  61. kubetorch/resources/secrets/utils.py +222 -0
  62. kubetorch/resources/volumes/__init__.py +0 -0
  63. kubetorch/resources/volumes/volume.py +340 -0
  64. kubetorch/servers/__init__.py +0 -0
  65. kubetorch/servers/http/__init__.py +0 -0
  66. kubetorch/servers/http/distributed_utils.py +2968 -0
  67. kubetorch/servers/http/http_client.py +802 -0
  68. kubetorch/servers/http/http_server.py +1622 -0
  69. kubetorch/servers/http/server_metrics.py +255 -0
  70. kubetorch/servers/http/utils.py +722 -0
  71. kubetorch/serving/__init__.py +0 -0
  72. kubetorch/serving/autoscaling.py +153 -0
  73. kubetorch/serving/base_service_manager.py +344 -0
  74. kubetorch/serving/constants.py +77 -0
  75. kubetorch/serving/deployment_service_manager.py +431 -0
  76. kubetorch/serving/knative_service_manager.py +487 -0
  77. kubetorch/serving/raycluster_service_manager.py +526 -0
  78. kubetorch/serving/service_manager.py +18 -0
  79. kubetorch/serving/templates/deployment_template.yaml +17 -0
  80. kubetorch/serving/templates/knative_service_template.yaml +19 -0
  81. kubetorch/serving/templates/kt_setup_template.sh.j2 +91 -0
  82. kubetorch/serving/templates/pod_template.yaml +198 -0
  83. kubetorch/serving/templates/raycluster_service_template.yaml +42 -0
  84. kubetorch/serving/templates/raycluster_template.yaml +35 -0
  85. kubetorch/serving/templates/service_template.yaml +21 -0
  86. kubetorch/serving/templates/workerset_template.yaml +36 -0
  87. kubetorch/serving/utils.py +344 -0
  88. kubetorch/utils.py +263 -0
  89. kubetorch-0.2.5.dist-info/METADATA +75 -0
  90. kubetorch-0.2.5.dist-info/RECORD +92 -0
  91. kubetorch-0.2.5.dist-info/WHEEL +4 -0
  92. kubetorch-0.2.5.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,224 @@
1
+ import os
2
+ from typing import Dict, List, Optional, Tuple
3
+
4
+ from kubetorch.globals import config
5
+
6
+ from kubetorch.resources.secrets.utils import read_files_as_secrets_dict
7
+
8
+
9
+ class Secret:
10
+ _DEFAULT_PATH = None
11
+ _DEFAULT_FILENAMES = None
12
+ _DEFAULT_ENV_VARS = {}
13
+ _MAP_FILENAMES_TO_ENV_VARS = {}
14
+ _PROVIDER = None
15
+
16
+ def __init__(
17
+ self,
18
+ name: Optional[str] = None,
19
+ provider: Optional[str] = None,
20
+ values: Dict = None,
21
+ path: str = None,
22
+ env_vars: Dict = None,
23
+ override: bool = False,
24
+ **kwargs,
25
+ ):
26
+ """
27
+ Secret class. Built-in provider classes contain default path and/or environment variable mappings,
28
+ based on it's expected usage.
29
+
30
+ Note:
31
+ Currently supported built-in providers:
32
+ anthropic, aws, azure, gcp, github, huggingface, lambda, langchain, openai, pinecone, ssh, wandb.
33
+
34
+ Args:
35
+ name (str, optional): Name to assign the Kubetorch secret.
36
+ provider (str, optional): Provider corresponding to the secret (e.g. "aws", "gcp").
37
+ values (Dict, optional): Dictionary mapping secret keys to the corresponding secret values.
38
+ path (str, optional): Path where the secret values are held.
39
+ env_vars (Dict, optional): Dictionary mapping secret keys to the corresponding environment variable key.
40
+ override (bool, optional): If True, override the secret's values in Kubernetes if a secret with the same
41
+ name already exists.
42
+ """
43
+ name_prefix = (
44
+ f"{config.username}-" if config.username else ""
45
+ ) # we need the username as prefix in case diffrent users will create the same provider secret
46
+ self._name = name or f"{name_prefix}{provider}" or f"{name_prefix}{self._PROVIDER}"
47
+ self._name = self._name.replace("_", "-") # cleanup so the name will match k8 standards.
48
+ self._namespace = kwargs.get("namespace", None) or config.namespace
49
+ self._values = values
50
+
51
+ self.provider = provider or self._PROVIDER
52
+ self.path = path
53
+ if path:
54
+ filenames = kwargs.get(
55
+ "filenames", None
56
+ ) # we might get filenames as kwarg if we load the secret from name or form config
57
+ updated_path, filenames = self._split_path_if_needed(path=path, filenames=filenames)
58
+ self.path = updated_path
59
+ self.filenames = filenames
60
+ self.env_vars = env_vars
61
+ self._override = override
62
+
63
+ if not any([values, path, env_vars]):
64
+ if self._values_from_path():
65
+ pass
66
+ elif self._values_from_env(self._DEFAULT_ENV_VARS):
67
+ self.env_vars = self._DEFAULT_ENV_VARS
68
+ else:
69
+ raise ValueError(
70
+ "Secrets values not provided and could not be extracted from default file "
71
+ f"({self._DEFAULT_PATH}) or env vars ({self._DEFAULT_ENV_VARS.values()}) locations."
72
+ )
73
+
74
+ @property
75
+ def name(self):
76
+ """Name of the secret."""
77
+ return self._name
78
+
79
+ @property
80
+ def override(self):
81
+ """Should we override secret's values in Kubernetes if a secret with the same name already exists"""
82
+ return self._override
83
+
84
+ @property
85
+ def values(self):
86
+ """Secret values."""
87
+ if self._values:
88
+ return self._values
89
+ if self.path:
90
+ return self._values_from_path(self.path)
91
+ if self.env_vars:
92
+ return self._values_from_env(self.env_vars)
93
+ return {}
94
+
95
+ def _values_from_env(self, env_vars: Dict = None):
96
+ env_vars = env_vars or self.env_vars
97
+ if not env_vars:
98
+ return {}
99
+ return {key: os.environ[key] for key in env_vars}
100
+
101
+ def _values_from_path(self, path: str = None):
102
+ path = path or self.path or self._DEFAULT_PATH
103
+ if not path:
104
+ return {}
105
+
106
+ # Double-check that the path is a directory
107
+ path, filenames = self._split_path_if_needed(path)
108
+
109
+ values = read_files_as_secrets_dict(path=path, filenames=filenames)
110
+ if values:
111
+ # Only set if the values were successfully found
112
+ if self._MAP_FILENAMES_TO_ENV_VARS:
113
+ env_vars = []
114
+ for filename, env_var in self._MAP_FILENAMES_TO_ENV_VARS.items():
115
+ if filename in values:
116
+ values[env_var] = values[filename].strip()
117
+ del values[filename]
118
+ env_vars.append(env_var)
119
+ if env_vars:
120
+ self.env_vars = env_vars
121
+ self._values = values
122
+ return values
123
+
124
+ self.path = path
125
+ self.filenames = filenames
126
+ return values
127
+
128
+ def _split_path_if_needed(self, path: str, filenames: list = None) -> Tuple[str, List[str]]:
129
+ """Split path into path and filesnames if a single file is specified as a full path"""
130
+ updated_path = path
131
+ is_default_path = updated_path == self._DEFAULT_PATH
132
+ updated_filenames = getattr(self, "filenames", None) or filenames
133
+ if not updated_filenames:
134
+ if not is_default_path or not self._DEFAULT_FILENAMES:
135
+ # Reform single-file path a directory and filenames list
136
+ updated_filenames = [os.path.basename(path)]
137
+ updated_path = os.path.dirname(path)
138
+ else:
139
+ updated_filenames = self._DEFAULT_FILENAMES
140
+ return updated_path, updated_filenames
141
+
142
+ @classmethod
143
+ def from_config(cls, config: dict):
144
+ override_value = config.get("override", "False").lower()
145
+ bool_override_value = override_value == "true"
146
+ config["override"] = bool_override_value
147
+ if "provider" in config:
148
+ from .provider_secrets.providers import _get_provider_class
149
+
150
+ provider_class = _get_provider_class(config["provider"])
151
+ return provider_class.from_config(config)
152
+ return cls(**config)
153
+
154
+ @classmethod
155
+ def from_name(cls, name, namespace: str = config.namespace):
156
+
157
+ from kubetorch.resources.secrets.kubernetes_secrets_client import KubernetesSecretsClient
158
+
159
+ secrets_client = KubernetesSecretsClient(namespace=namespace)
160
+ secret = secrets_client.load_secret(name=name)
161
+ return secret
162
+
163
+ @classmethod
164
+ def builtin_providers(cls, as_str: bool = False) -> List:
165
+ """Return list of all Kubetorch providers (as class objects) supported out of the box.
166
+
167
+ Args:
168
+ as_str (bool, optional): Whether to return the providers as a string or as a class.
169
+ (Default: ``False``)
170
+ """
171
+ from .provider_secrets.providers import _str_to_provider_class
172
+
173
+ if as_str:
174
+ return list(_str_to_provider_class.keys())
175
+ return list(_str_to_provider_class.values())
176
+
177
+ @classmethod
178
+ def from_provider(cls, provider: str, name: str = None, path: str = None, override: bool = False):
179
+ """Return kubetorch provider secret object
180
+
181
+ Args:
182
+ provider (str): Provider's name
183
+ name (str, Optional): Secret name
184
+ path (str, optional): Path where the secret values are held.
185
+ override (Bool, optional): If True, override the secret's values in Kubernetes if a secret with the same name already exists.
186
+ """
187
+ from .provider_secrets.providers import _get_provider_class
188
+
189
+ secret_class = _get_provider_class(provider)
190
+ if not secret_class:
191
+ raise ValueError(f"{provider} is not a supported provider: {Secret.builtin_providers(as_str=True)}")
192
+ return secret_class(name=name, provider=provider, path=path, override=override)
193
+
194
+ @classmethod
195
+ def from_path(cls, path: str, name: str = None, override: bool = False):
196
+ """Return kubetorch provider secret object
197
+
198
+ Args:
199
+ path (str): Local path to the secret values file
200
+ name (str, Optional): Secret name
201
+ override (Bool, optional): If True, override the secret's values in Kubernetes if a secret with the same name already exists.
202
+ """
203
+ from .provider_secrets.providers import _get_provider_class
204
+
205
+ secret_class = _get_provider_class(path) or Secret
206
+ if not secret_class._PROVIDER and not name:
207
+ raise ValueError("secret name must be provided.")
208
+
209
+ return secret_class(name=name, path=path, override=override)
210
+
211
+ @classmethod
212
+ def from_env(cls, env_vars: dict, name: str = None, override: bool = False):
213
+ """Return kubetorch provider secret object
214
+
215
+ Args:
216
+ env_vars (dict): Dictionary mapping secret keys to the corresponding
217
+ environment variable key.
218
+ name (str, Optional): Secret name
219
+ override (Bool, optional): If True, override the secret's values in Kubernetes if a secret with the same name already exists.
220
+ """
221
+ from .provider_secrets.providers import _get_provider_class
222
+
223
+ secret_class = _get_provider_class(env_vars) or Secret
224
+ return secret_class(name=name, env_vars=env_vars, override=override)
@@ -0,0 +1,64 @@
1
+ from typing import Dict, Optional
2
+
3
+ from kubetorch.globals import config
4
+
5
+ from .secret import Secret
6
+
7
+
8
+ def secret(
9
+ name: Optional[str] = None,
10
+ provider: Optional[str] = None,
11
+ path: Optional[str] = None,
12
+ env_vars: Optional[Dict] = None,
13
+ namespace: Optional[str] = config.namespace,
14
+ override: Optional[bool] = False,
15
+ ) -> Secret:
16
+ """
17
+ Builds an instance of :class:`Secret`. At most one of `provider`, `path`, or `env_vars` can be provided, to maintain
18
+ one source of truth. For a provider, the values are inferred from the default path or environment variables for that
19
+ provider. To load a secret by name, provide its name and namespace.
20
+
21
+ Args:
22
+ namespace (str, optional): Namespace to load the secret from, if we create a secret from name. Default: "default".
23
+ name (str, optional): Name to assign the resource. If none is provided, resource name defaults to the
24
+ provider name.
25
+ provider (str, optional): Provider corresponding to the secret (e.g. "aws", "gcp"). To see all supported provider
26
+ types, run ``kt.Secret.builtin_providers(as_str=True)``.
27
+ path (str, optional): Path where the secret values are held.
28
+ env_vars (Dict, optional): Dictionary mapping secret keys to the corresponding
29
+ environment variable key.
30
+ override (Bool, optional): If True, override the secret's values in Kubernetes if a secret with the same name already exists.
31
+
32
+ Returns:
33
+ Secret: The resulting secret object.
34
+
35
+ Examples:
36
+
37
+ .. code-block:: python
38
+
39
+ import kubetorch as kt
40
+
41
+ local_secret = kt.secret(name="in_memory_secret", values={"secret_key": "secret_val"})
42
+ aws_secret = kt.secret(provider="aws")
43
+ gcp_secret = kt.secret(name="my-gcp-secret", path="~/.gcp/credentials")
44
+ lambda_secret = kt.secret(name= "my-lambda-secret", env_vars={"api_key": "LAMBDA_API_KEY"})
45
+ """
46
+
47
+ # env_vars or path or provider are provided
48
+ valid_input = sum([bool(x) for x in [provider, path, env_vars]]) == 1 or (provider and path)
49
+ valid_from_name_input = sum([bool(x) for x in [provider, path, env_vars]]) == 0 and name
50
+
51
+ if not (valid_from_name_input or valid_input):
52
+ raise ValueError(
53
+ "You must provide exactly one of: `provider`, `path`, or `env_vars`. Alternatively, you may provide `name` to load a secret from name."
54
+ )
55
+
56
+ if valid_input:
57
+ if provider:
58
+ return Secret.from_provider(provider=provider, name=name, path=path, override=override)
59
+ elif path and not provider: # the case where provider + path are provided are
60
+ return Secret.from_path(path=path, name=name, override=override)
61
+ elif env_vars:
62
+ return Secret.from_env(env_vars=env_vars, name=name, override=override)
63
+ else:
64
+ return Secret.from_name(name=name, namespace=namespace)
@@ -0,0 +1,222 @@
1
+ import os
2
+ import time
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+
6
+ import yaml
7
+ from kubernetes import client, config
8
+ from kubernetes.client import V1Pod
9
+ from kubernetes.stream import stream
10
+
11
+ from kubetorch.constants import DEFAULT_KUBECONFIG_PATH
12
+ from kubetorch.globals import config as kt_config
13
+
14
+ from kubetorch.logger import get_logger
15
+ from kubetorch.servers.http.utils import is_running_in_kubernetes
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ def get_k8s_identity_name() -> Optional[str]:
21
+ """Get Kubernetes user identity from kubeconfig file.
22
+
23
+ Returns:
24
+ User identity string (e.g., "user-{name}", "role-{name}", "sa-{name}") or None
25
+ """
26
+ try:
27
+ if is_running_in_kubernetes():
28
+ # Get the service account name
29
+ service_account_name = os.environ.get("SERVICE_ACCOUNT_NAME")
30
+ if service_account_name:
31
+ return "sa-" + service_account_name.lower()
32
+ return None
33
+
34
+ # Read from kubeconfig
35
+ kubeconfig_path = os.getenv("KUBECONFIG") or DEFAULT_KUBECONFIG_PATH
36
+ kubeconfig_file = Path(kubeconfig_path).expanduser()
37
+ if not kubeconfig_file.exists():
38
+ return None
39
+
40
+ with open(kubeconfig_file, "r") as f:
41
+ kubeconfig = yaml.safe_load(f)
42
+
43
+ current_context = kubeconfig.get("current-context")
44
+ if not current_context:
45
+ return None
46
+
47
+ # Find current context's user
48
+ for context in kubeconfig.get("contexts", []):
49
+ if context.get("name") == current_context:
50
+ user_name = context.get("context", {}).get("user")
51
+ if not user_name:
52
+ return None
53
+
54
+ # Parse AWS ARN format (EKS IAM users/roles)
55
+ if "assumed-role" in user_name:
56
+ parts = user_name.split("/")
57
+ if len(parts) >= 2:
58
+ return "role-" + parts[-2].lower()
59
+ elif "/" in user_name and (".amazonaws.com" in user_name or "arn:aws" in user_name):
60
+ parts = user_name.split("/")
61
+ return "user-" + parts[-1].lower()
62
+
63
+ # Check for exec-based auth with AWS role
64
+ for user in kubeconfig.get("users", []):
65
+ if user.get("name") == user_name:
66
+ exec_config = user.get("user", {}).get("exec", {})
67
+ for env_var in exec_config.get("env", []):
68
+ if env_var.get("name") == "AWS_ROLE_ARN":
69
+ role_arn = env_var.get("value", "")
70
+ if "/" in role_arn:
71
+ return "role-" + role_arn.split("/")[-1].lower()
72
+ break
73
+
74
+ # Default: use user name as-is
75
+ return "user-" + user_name.lower()
76
+
77
+ except Exception as e:
78
+ logger.debug(f"Failed to get Kubernetes identity name: {e}")
79
+
80
+ return None
81
+
82
+
83
+ def read_files_as_secrets_dict(path: str, filenames: List[str]):
84
+ values = {}
85
+ cred_path = os.path.expanduser(path)
86
+
87
+ for filename in filenames:
88
+ file_path = os.path.join(cred_path, filename)
89
+ # Read the files
90
+ content = _read_file_if_exists(file_path)
91
+ if content:
92
+ values[filename] = content
93
+ # # Base64 encode the content
94
+ # encoded = base64.b64encode(content).decode("utf-8")
95
+ # values[filename] = encoded
96
+
97
+ return values
98
+
99
+
100
+ def _read_file_if_exists(file_path: str) -> Optional[str]:
101
+ try:
102
+ with open(file_path, "r") as f: # "rb" if you encode above.
103
+ return f.read()
104
+ except FileNotFoundError:
105
+ logger.error(f"Warning: {file_path} not found, using empty content")
106
+ return None
107
+
108
+
109
+ # ------------------------------------------------------------------------------------------------
110
+ # Secret testing utils
111
+ # ------------------------------------------------------------------------------------------------
112
+
113
+
114
+ def check_path_on_kubernetes_pods(path: str, service_name: str, namespace: str = None) -> bool:
115
+ """
116
+ Check if a path exists on a specific Knative service's pods
117
+ """
118
+ namespace = namespace or kt_config.namespace
119
+ # Load Kubernetes configuration
120
+ config.load_kube_config()
121
+ # Initialize API clients
122
+ core_v1_api = client.CoreV1Api()
123
+
124
+ pods = _fetch_pods_for_kubernetes_service(service_name, namespace, core_v1_api)
125
+ if not pods:
126
+ logger.error(f"No pods found for service {service_name} in namespace {namespace}")
127
+ return False
128
+
129
+ path_found = True
130
+ for pod in pods:
131
+ pod_name = pod.metadata.name
132
+ command = ["/bin/bash", "-c", f"[ -f {path} ] && echo yes || echo no"]
133
+ try:
134
+ resp = stream(
135
+ core_v1_api.connect_get_namespaced_pod_exec,
136
+ name=pod_name,
137
+ namespace=namespace,
138
+ command=command,
139
+ container="kubetorch",
140
+ stderr=True,
141
+ stdout=True,
142
+ )
143
+ if "yes" in resp:
144
+ continue
145
+ except client.exceptions.ApiException as e:
146
+ logger.error(f"Error executing command on pod {pod_name}: {e}")
147
+
148
+ path_found = False
149
+
150
+ return path_found
151
+
152
+
153
+ def check_env_vars_on_kubernetes_pods(env_vars: list, service_name: str, namespace: str = None) -> dict:
154
+ """
155
+ Check if an AWS role is assumed on a specific Knative service's pods
156
+
157
+ :param namespace: Kubernetes namespace
158
+ :param service_name: Name of the Knative service
159
+ :return: Dictionary with role assumption details
160
+ """
161
+ namespace = namespace or kt_config.namespace
162
+ # Load Kubernetes configuration
163
+ config.load_kube_config()
164
+ # Initialize API clients
165
+ core_v1_api = client.CoreV1Api()
166
+
167
+ pods = _fetch_pods_for_kubernetes_service(service_name, namespace, core_v1_api)
168
+ if not pods:
169
+ logger.error(f"No pods found for service {service_name} in namespace {namespace}")
170
+ return {}
171
+
172
+ found_env_vars = {}
173
+
174
+ for pod in pods:
175
+ for env_var in env_vars:
176
+ if found_env_vars.get(env_var):
177
+ # Skip if already found on another pod
178
+ continue
179
+ pod_name = pod.metadata.name
180
+ command = ["/bin/bash", "-c", f"echo ${env_var}"]
181
+ try:
182
+ resp = stream(
183
+ core_v1_api.connect_get_namespaced_pod_exec,
184
+ name=pod_name,
185
+ namespace=namespace,
186
+ command=command,
187
+ container="kubetorch",
188
+ stderr=True,
189
+ stdout=True,
190
+ )
191
+ if len(resp.strip()) > 0:
192
+ found_env_vars[env_var] = resp.strip()
193
+ except client.exceptions.ApiException as e:
194
+ logger.error(f"Error executing command: {e}")
195
+
196
+ if set(found_env_vars.keys()) == set(env_vars):
197
+ # Found all env vars: skip the remaining pods
198
+ break
199
+
200
+ return found_env_vars
201
+
202
+
203
+ def _fetch_pods_for_kubernetes_service(service_name: str, namespace: str, client_api: client.CoreV1Api) -> List[V1Pod]:
204
+ """
205
+ Fetch pods for a specific Knative service with timeout
206
+ """
207
+ start_time = time.time()
208
+ while time.time() - start_time < 30:
209
+ try:
210
+ # List pods matching the service
211
+ pods = client_api.list_namespaced_pod(
212
+ namespace=namespace,
213
+ label_selector=f"kubetorch.com/service={service_name}",
214
+ )
215
+ ready_pods = [pod for pod in pods.items if pod.status.phase == "Running"]
216
+ if ready_pods:
217
+ return ready_pods
218
+ except Exception as e:
219
+ logger.error(f"Error fetching pods for service {service_name} in namespace {namespace}: {e}")
220
+ time.sleep(1)
221
+
222
+ return []
File without changes