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.
- kubetorch/__init__.py +59 -0
- kubetorch/cli.py +1939 -0
- kubetorch/cli_utils.py +967 -0
- kubetorch/config.py +453 -0
- kubetorch/constants.py +18 -0
- kubetorch/docs/Makefile +18 -0
- kubetorch/docs/__init__.py +0 -0
- kubetorch/docs/_ext/json_globaltoc.py +42 -0
- kubetorch/docs/api/cli.rst +10 -0
- kubetorch/docs/api/python/app.rst +21 -0
- kubetorch/docs/api/python/cls.rst +19 -0
- kubetorch/docs/api/python/compute.rst +25 -0
- kubetorch/docs/api/python/config.rst +11 -0
- kubetorch/docs/api/python/fn.rst +19 -0
- kubetorch/docs/api/python/image.rst +14 -0
- kubetorch/docs/api/python/secret.rst +18 -0
- kubetorch/docs/api/python/volumes.rst +13 -0
- kubetorch/docs/api/python.rst +101 -0
- kubetorch/docs/conf.py +69 -0
- kubetorch/docs/index.rst +20 -0
- kubetorch/docs/requirements.txt +5 -0
- kubetorch/globals.py +269 -0
- kubetorch/logger.py +59 -0
- kubetorch/resources/__init__.py +0 -0
- kubetorch/resources/callables/__init__.py +0 -0
- kubetorch/resources/callables/cls/__init__.py +0 -0
- kubetorch/resources/callables/cls/cls.py +159 -0
- kubetorch/resources/callables/fn/__init__.py +0 -0
- kubetorch/resources/callables/fn/fn.py +140 -0
- kubetorch/resources/callables/module.py +1315 -0
- kubetorch/resources/callables/utils.py +203 -0
- kubetorch/resources/compute/__init__.py +0 -0
- kubetorch/resources/compute/app.py +253 -0
- kubetorch/resources/compute/compute.py +2414 -0
- kubetorch/resources/compute/decorators.py +137 -0
- kubetorch/resources/compute/utils.py +1026 -0
- kubetorch/resources/compute/websocket.py +135 -0
- kubetorch/resources/images/__init__.py +1 -0
- kubetorch/resources/images/image.py +412 -0
- kubetorch/resources/images/images.py +64 -0
- kubetorch/resources/secrets/__init__.py +2 -0
- kubetorch/resources/secrets/kubernetes_secrets_client.py +377 -0
- kubetorch/resources/secrets/provider_secrets/__init__.py +0 -0
- kubetorch/resources/secrets/provider_secrets/anthropic_secret.py +12 -0
- kubetorch/resources/secrets/provider_secrets/aws_secret.py +16 -0
- kubetorch/resources/secrets/provider_secrets/azure_secret.py +14 -0
- kubetorch/resources/secrets/provider_secrets/cohere_secret.py +12 -0
- kubetorch/resources/secrets/provider_secrets/gcp_secret.py +16 -0
- kubetorch/resources/secrets/provider_secrets/github_secret.py +13 -0
- kubetorch/resources/secrets/provider_secrets/huggingface_secret.py +20 -0
- kubetorch/resources/secrets/provider_secrets/kubeconfig_secret.py +12 -0
- kubetorch/resources/secrets/provider_secrets/lambda_secret.py +13 -0
- kubetorch/resources/secrets/provider_secrets/langchain_secret.py +12 -0
- kubetorch/resources/secrets/provider_secrets/openai_secret.py +11 -0
- kubetorch/resources/secrets/provider_secrets/pinecone_secret.py +12 -0
- kubetorch/resources/secrets/provider_secrets/providers.py +92 -0
- kubetorch/resources/secrets/provider_secrets/ssh_secret.py +12 -0
- kubetorch/resources/secrets/provider_secrets/wandb_secret.py +11 -0
- kubetorch/resources/secrets/secret.py +224 -0
- kubetorch/resources/secrets/secret_factory.py +64 -0
- kubetorch/resources/secrets/utils.py +222 -0
- kubetorch/resources/volumes/__init__.py +0 -0
- kubetorch/resources/volumes/volume.py +340 -0
- kubetorch/servers/__init__.py +0 -0
- kubetorch/servers/http/__init__.py +0 -0
- kubetorch/servers/http/distributed_utils.py +2968 -0
- kubetorch/servers/http/http_client.py +802 -0
- kubetorch/servers/http/http_server.py +1622 -0
- kubetorch/servers/http/server_metrics.py +255 -0
- kubetorch/servers/http/utils.py +722 -0
- kubetorch/serving/__init__.py +0 -0
- kubetorch/serving/autoscaling.py +153 -0
- kubetorch/serving/base_service_manager.py +344 -0
- kubetorch/serving/constants.py +77 -0
- kubetorch/serving/deployment_service_manager.py +431 -0
- kubetorch/serving/knative_service_manager.py +487 -0
- kubetorch/serving/raycluster_service_manager.py +526 -0
- kubetorch/serving/service_manager.py +18 -0
- kubetorch/serving/templates/deployment_template.yaml +17 -0
- kubetorch/serving/templates/knative_service_template.yaml +19 -0
- kubetorch/serving/templates/kt_setup_template.sh.j2 +91 -0
- kubetorch/serving/templates/pod_template.yaml +198 -0
- kubetorch/serving/templates/raycluster_service_template.yaml +42 -0
- kubetorch/serving/templates/raycluster_template.yaml +35 -0
- kubetorch/serving/templates/service_template.yaml +21 -0
- kubetorch/serving/templates/workerset_template.yaml +36 -0
- kubetorch/serving/utils.py +344 -0
- kubetorch/utils.py +263 -0
- kubetorch-0.2.5.dist-info/METADATA +75 -0
- kubetorch-0.2.5.dist-info/RECORD +92 -0
- kubetorch-0.2.5.dist-info/WHEEL +4 -0
- 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
|