konduktor-nightly 0.1.0.dev20250909104711__py3-none-any.whl → 0.1.0.dev20250910100913__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.

Potentially problematic release.


This version of konduktor-nightly might be problematic. Click here for more details.

konduktor/__init__.py CHANGED
@@ -11,7 +11,7 @@ from konduktor.task import Task
11
11
  __all__ = ['launch', 'Resources', 'Task', 'Serving']
12
12
 
13
13
  # Replaced with the current commit when building the wheels.
14
- _KONDUKTOR_COMMIT_SHA = 'b772c32b1fa9e082d58d857dd655abc132c7bab4'
14
+ _KONDUKTOR_COMMIT_SHA = 'ca586c26e1db34e0b199c9fa57e84b39983db15e'
15
15
  os.makedirs(os.path.expanduser('~/.konduktor'), exist_ok=True)
16
16
 
17
17
 
@@ -45,5 +45,5 @@ def _get_git_commit():
45
45
 
46
46
 
47
47
  __commit__ = _get_git_commit()
48
- __version__ = '1.0.0.dev0.1.0.dev20250909104711'
48
+ __version__ = '1.0.0.dev0.1.0.dev20250910100913'
49
49
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
@@ -117,7 +117,12 @@ def create_jobset(
117
117
  )
118
118
  logger.info(
119
119
  f'task {colorama.Fore.CYAN}{colorama.Style.BRIGHT}'
120
- f'{task.name}{colorama.Style.RESET_ALL} created'
120
+ f'{colorama.Fore.CYAN}{colorama.Style.BRIGHT}{task.name}'
121
+ f'{colorama.Style.RESET_ALL} created in context '
122
+ f'{colorama.Fore.YELLOW}{colorama.Style.BRIGHT}{context}'
123
+ f'{colorama.Style.RESET_ALL}, namespace '
124
+ f'{colorama.Fore.GREEN}{colorama.Style.BRIGHT}{namespace}'
125
+ f'{colorama.Style.RESET_ALL}'
121
126
  )
122
127
  return jobset
123
128
  except kube_client.api_exception() as err:
konduktor/kube_client.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import logging
2
2
  import os
3
- from typing import Any, Callable, Optional
3
+ from typing import Any, Callable, List, Optional
4
4
 
5
5
  import kubernetes
6
6
  import urllib3
7
7
 
8
+ from konduktor import config
8
9
  from konduktor import logging as konduktor_logging
9
10
  from konduktor.utils import annotations, ux_utils
10
11
 
@@ -20,6 +21,10 @@ DEFAULT_IN_CLUSTER_REGION = 'in-cluster'
20
21
  # set to DEFAULT_IN_CLUSTER_REGION.
21
22
  IN_CLUSTER_CONTEXT_NAME_ENV_VAR = 'KONDUKTOR_IN_CLUSTER_CONTEXT_NAME'
22
23
 
24
+ # Tracks the most recently selected/loaded context name.
25
+ # None means no explicit context was resolved/loaded yet.
26
+ _ACTIVE_CONTEXT: Optional[str] = None
27
+
23
28
 
24
29
  def _decorate_methods(obj: Any, decorator: Callable, decoration_type: str):
25
30
  for attr_name in dir(obj):
@@ -54,12 +59,37 @@ def _api_logging_decorator(logger: str, level: int):
54
59
  def _load_config(context: Optional[str] = None):
55
60
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
56
61
 
62
+ # If no context explicitly provided, prefer the configured allowed context
63
+ # (first element) when present. This ensures the client defaults to the
64
+ # user-specified context instead of kubeconfig's current-context.
65
+ effective_context = context
66
+ allowed_contexts: List[str] = config.get_nested(
67
+ ('kubernetes', 'allowed_contexts'), []
68
+ )
69
+
70
+ is_allowed_selected = False
71
+ if effective_context is None and allowed_contexts:
72
+ effective_context = allowed_contexts[0]
73
+ is_allowed_selected = True
74
+ logger.info(
75
+ 'Detected kubernetes.allowed_contexts in config; using context: %s',
76
+ effective_context,
77
+ )
78
+
57
79
  def _load_config_from_kubeconfig(context: Optional[str] = None):
58
80
  try:
59
81
  kubernetes.config.load_kube_config(context=context)
60
82
  except kubernetes.config.config_exception.ConfigException as e:
61
- # Check if exception was due to no current-context
62
- if 'Expected key current-context' in str(e):
83
+ # Improve error when a configured allowed context cannot be loaded
84
+ msg = str(e)
85
+ if is_allowed_selected and context is not None:
86
+ err_str = (
87
+ 'Configured Kubernetes context not usable: '
88
+ f'kubernetes.allowed_contexts[0] = {context!r}. '
89
+ 'Please ensure this context exists and is valid in your '
90
+ 'kubeconfig (typically at ~/.kube/config).'
91
+ )
92
+ elif 'Expected key current-context' in msg:
63
93
  err_str = (
64
94
  f'Failed to load Kubernetes configuration for {context!r}. '
65
95
  'Kubeconfig does not contain any valid context(s).\n'
@@ -73,7 +103,8 @@ def _load_config(context: Optional[str] = None):
73
103
  with ux_utils.print_exception_no_traceback():
74
104
  raise ValueError(err_str) from None
75
105
 
76
- if context == in_cluster_context_name() or context is None:
106
+ global _ACTIVE_CONTEXT
107
+ if effective_context == in_cluster_context_name() or effective_context is None:
77
108
  try:
78
109
  # Load in-cluster config if running in a pod and context is None.
79
110
  # Kubernetes set environment variables for service discovery do not
@@ -83,10 +114,22 @@ def _load_config(context: Optional[str] = None):
83
114
  os.environ['KUBERNETES_SERVICE_HOST'] = 'kubernetes.default.svc'
84
115
  os.environ['KUBERNETES_SERVICE_PORT'] = '443'
85
116
  kubernetes.config.load_incluster_config()
117
+ _ACTIVE_CONTEXT = in_cluster_context_name()
86
118
  except kubernetes.config.config_exception.ConfigException:
87
- _load_config_from_kubeconfig()
119
+ # If allowed_contexts was specified, do not fall back silently.
120
+ if is_allowed_selected:
121
+ _load_config_from_kubeconfig(effective_context)
122
+ else:
123
+ _load_config_from_kubeconfig()
124
+ # Best effort: set active context to current-context from kubeconfig
125
+ try:
126
+ _, current_ctx = kubernetes.config.list_kube_config_contexts()
127
+ _ACTIVE_CONTEXT = current_ctx.get('name') if current_ctx else None
128
+ except kubernetes.config.config_exception.ConfigException:
129
+ _ACTIVE_CONTEXT = None
88
130
  else:
89
- _load_config_from_kubeconfig(context)
131
+ _load_config_from_kubeconfig(effective_context)
132
+ _ACTIVE_CONTEXT = effective_context
90
133
 
91
134
 
92
135
  @_api_logging_decorator('urllib3', logging.ERROR)
@@ -183,3 +226,12 @@ def in_cluster_context_name() -> Optional[str]:
183
226
  context name.
184
227
  """
185
228
  return os.environ.get(IN_CLUSTER_CONTEXT_NAME_ENV_VAR) or DEFAULT_IN_CLUSTER_REGION
229
+
230
+
231
+ def get_active_context() -> Optional[str]:
232
+ """Returns the last context selected by the client loader.
233
+
234
+ This reflects the effective context used by the most recent client init.
235
+ May be None if no client has been initialized yet.
236
+ """
237
+ return _ACTIVE_CONTEXT
@@ -264,11 +264,31 @@ AUTOSCALER_TO_LABEL_FORMATTER = {
264
264
 
265
265
  @functools.lru_cache()
266
266
  def get_current_kube_config_context_name() -> Optional[str]:
267
- """Get the current kubernetes context from the kubeconfig file
267
+ """Get the active Kubernetes context name.
268
+
269
+ Precedence:
270
+ 1) The first entry in `kubernetes.allowed_contexts` (if configured).
271
+ 2) kubeconfig's current-context (fallback when not configured).
268
272
 
269
273
  Returns:
270
- str | None: The current kubernetes context if it exists, None otherwise
274
+ str | None: The selected context if it exists, None otherwise.
271
275
  """
276
+ # 1) Prefer a user-configured allowed context if provided.
277
+ try:
278
+ allowed_contexts: Optional[List[str]] = config.get_nested(
279
+ ('kubernetes', 'allowed_contexts'), None
280
+ )
281
+ if allowed_contexts:
282
+ context = allowed_contexts[0]
283
+ logger.info(
284
+ 'Detected kubernetes.allowed_contexts in config; using context: %s',
285
+ context,
286
+ )
287
+ return context
288
+ except Exception: # fallback safely if config loading fails unexpectedly
289
+ pass
290
+
291
+ # 2) Fall back to kubeconfig's current context
272
292
  k8s = kubernetes
273
293
  try:
274
294
  _, current_context = k8s.config.list_kube_config_contexts()
@@ -336,13 +356,24 @@ def get_kube_config_context_namespace(context_name: Optional[str] = None) -> str
336
356
  """
337
357
  k8s = kubernetes
338
358
  ns_path = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'
339
- # If using in-cluster context, get the namespace from the service account
340
- # namespace file. Uses the same logic as adaptors.kubernetes._load_config()
341
- # to stay consistent with in-cluster config loading.
359
+
360
+ # If no explicit context provided, prefer configured allowed context first.
361
+ if context_name is None:
362
+ try:
363
+ allowed_contexts: Optional[List[str]] = config.get_nested(
364
+ ('kubernetes', 'allowed_contexts'), None
365
+ )
366
+ if allowed_contexts:
367
+ context_name = allowed_contexts[0]
368
+ except Exception:
369
+ pass
370
+
371
+ # If using in-cluster context, get the namespace from the SA namespace file.
342
372
  if context_name == kube_client.in_cluster_context_name() or context_name is None:
343
373
  if os.path.exists(ns_path):
344
374
  with open(ns_path, encoding='utf-8') as f:
345
375
  return f.read().strip()
376
+
346
377
  # If not in-cluster, get the namespace from kubeconfig
347
378
  try:
348
379
  contexts, current_context = k8s.config.list_kube_config_contexts()
@@ -170,6 +170,29 @@ def create_table(field_names: List[str], **kwargs) -> prettytable.PrettyTable:
170
170
  return table
171
171
 
172
172
 
173
+ def _get_kr8s_api():
174
+ """Return a kr8s API configured for the selected context.
175
+
176
+ Honors kubernetes.allowed_contexts via
177
+ get_current_kube_config_context_name(). Fails with a clear error if a
178
+ configured context cannot be initialized.
179
+ """
180
+ ctx = kubernetes_utils.get_current_kube_config_context_name()
181
+ try:
182
+ # kr8s exposes a factory function `api(...)` that accepts context.
183
+ api = kr8s.api(context=ctx) if ctx else kr8s.api()
184
+ if ctx:
185
+ logger.debug('Initialized kr8s API for context: %s', ctx)
186
+ return api
187
+ except Exception as e: # defensive: surface a clear error if context fails
188
+ if ctx:
189
+ raise ValueError(
190
+ 'Failed to initialize kr8s client for context '
191
+ f'{ctx!r}. Ensure the context exists and your kubeconfig is valid.'
192
+ ) from e
193
+ raise
194
+
195
+
173
196
  def run_with_log(
174
197
  cmd: Union[List[str], str],
175
198
  log_path: str,
@@ -311,7 +334,8 @@ def tail_loki_logs_ws(
311
334
  # until we reach the end of the log and then invoke tail again.
312
335
  # Also include checks that the job is running/ever ran.
313
336
  raise ValueError('num_logs must be less than or equal to 5000')
314
- loki_svc = kr8s.objects.Service.get('loki', namespace='loki')
337
+ api = _get_kr8s_api()
338
+ loki_svc = kr8s.objects.Service.get('loki', namespace='loki', api=api)
315
339
  with kr8s.portforward.PortForward(
316
340
  loki_svc, LOKI_REMOTE_PORT, local_port='auto'
317
341
  ) as port:
@@ -345,8 +369,9 @@ def tail_vicky_logs(
345
369
  context = kubernetes_utils.get_current_kube_config_context_name()
346
370
  namespace = kubernetes_utils.get_kube_config_context_namespace(context)
347
371
  query: Dict[str, Any] = {}
372
+ api = _get_kr8s_api()
348
373
  vicky_svc = kr8s.objects.Service.get(
349
- 'vls-victoria-logs-single-server', namespace='victoria-logs'
374
+ 'vls-victoria-logs-single-server', namespace='victoria-logs', api=api
350
375
  )
351
376
 
352
377
  if num_logs == -1:
@@ -11,6 +11,7 @@ import kr8s
11
11
  import websockets
12
12
 
13
13
  from konduktor import logging
14
+ from konduktor.utils import kubernetes_utils
14
15
 
15
16
  logger = logging.get_logger(__name__)
16
17
 
@@ -59,7 +60,18 @@ def tail_loki_logs_ws(
59
60
  # until we reach the end of the log and then invoke tail again.
60
61
  # Also include checks that the job is running/ever ran.
61
62
  raise ValueError('num_logs must be less than or equal to 5000')
62
- loki_svc = kr8s.objects.Service.get('loki', namespace='loki')
63
+ # Initialize kr8s API honoring allowed_contexts if configured.
64
+ ctx = kubernetes_utils.get_current_kube_config_context_name()
65
+ try:
66
+ api = kr8s.api(context=ctx) if ctx else kr8s.api()
67
+ except Exception as e:
68
+ if ctx:
69
+ raise ValueError(
70
+ 'Failed to initialize kr8s client for context '
71
+ f'{ctx!r}. Ensure the context exists and your kubeconfig is valid.'
72
+ ) from e
73
+ raise
74
+ loki_svc = kr8s.objects.Service.get('loki', namespace='loki', api=api)
63
75
  with kr8s.portforward.PortForward(
64
76
  loki_svc, LOKI_REMOTE_PORT, local_port='auto'
65
77
  ) as port:
@@ -504,6 +504,13 @@ def get_config_schema():
504
504
  'anyOf': [{'required': ['name']}, {'required': ['namespace']}]
505
505
  },
506
506
  },
507
+ 'allowed_contexts': {
508
+ 'type': 'array',
509
+ 'items': {
510
+ 'type': 'string',
511
+ },
512
+ 'maxItems': 1,
513
+ },
507
514
  'provision_timeout': {
508
515
  'type': 'integer',
509
516
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: konduktor-nightly
3
- Version: 0.1.0.dev20250909104711
3
+ Version: 0.1.0.dev20250910100913
4
4
  Summary: GPU Cluster Health Management
5
5
  Author: Andrew Aikawa
6
6
  Author-email: asai@berkeley.edu
@@ -1,4 +1,4 @@
1
- konduktor/__init__.py,sha256=A6f9Nl9Tn3RVpQ0DeqpvSiTxg3o0-JrDxUy63NQmZMY,1574
1
+ konduktor/__init__.py,sha256=z3Tti97BuZnXObngTeM9xmNSkiQivqwK9dIZ0FIJHf0,1574
2
2
  konduktor/adaptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  konduktor/adaptors/aws.py,sha256=s47Ra-GaqCQibzVfmD0pmwEWHif1EGO5opMbwkLxTCU,8244
4
4
  konduktor/adaptors/common.py,sha256=ZIqzjx77PIHUwpjfAQ1uX8B2aX78YMuGj4Bppd-MdyM,4183
@@ -10,7 +10,7 @@ konduktor/backends/constants.py,sha256=nt9G9AmFCOMwO4GuKgRQSzJJuKapOmaROp4_Y0tMF
10
10
  konduktor/backends/deployment.py,sha256=fswN9hX_7NwcEogYmo1xn3WgWF8XCcGDvV5yx54_CA0,5860
11
11
  konduktor/backends/deployment_utils.py,sha256=qcuoLPeMvEVqgD_h71hQZXAp4ZCdXsFeSBxhtXW6pAA,39846
12
12
  konduktor/backends/jobset.py,sha256=drt8Gc0iYQx18JWXBU6XfhUvC2xCKd8szSJ2JC4O20Q,8640
13
- konduktor/backends/jobset_utils.py,sha256=7j-HCu7UqrFbmmoz9um3an8NNETRPo0y3lp14Mii6HQ,26070
13
+ konduktor/backends/jobset_utils.py,sha256=UsFBIta9_IGDe8xvTMwQv6nr0k13QhLeF8Hm6KaN5do,26378
14
14
  konduktor/backends/pod_utils.py,sha256=KP_PAgsdNHFgt4Od-5gAtpifAKIL7DMBg7NJ44uqikg,14885
15
15
  konduktor/check.py,sha256=JennyWoaqSKhdyfUldd266KwVXTPJpcYQa4EED4a_BA,7569
16
16
  konduktor/cli.py,sha256=OwVdT4ibAQoAJO79YzlThQv_VKlHpsD-CHRwOzehGQ8,57613
@@ -66,7 +66,7 @@ konduktor/data/registry.py,sha256=CUbMsN_Q17Pf4wRHkqZrycErEjTP7cLEdgcfwVGcEpc,69
66
66
  konduktor/data/storage.py,sha256=o2So-bY9glvgbGdoN7AQNYmNnvGf1AUDPpImtadRL90,35213
67
67
  konduktor/data/storage_utils.py,sha256=n4GivkN0KMqmyOTDznF0Z-hzsJvm7KCEh5i5HgFAT-4,20806
68
68
  konduktor/execution.py,sha256=d0EP79iSrW2uFsoqn0YV_4kgIupPIqpMOParXx0y3kg,18519
69
- konduktor/kube_client.py,sha256=WELs9jClRW9r-imNJF3gJi3Z7ygkFDnYDmMXu5nJhEM,6213
69
+ konduktor/kube_client.py,sha256=HtM3d-_GigHnfGINRANchApR9_OigqczBgeYJ6Dj4j0,8504
70
70
  konduktor/logging.py,sha256=xtcCdnecmC3rqMTyunK-klQRINojI7NI4Apag78i9jM,3221
71
71
  konduktor/manifests/apoxy-setup.yaml,sha256=HDZu7Evm_siIpK1E4tNZ9WVTnFV2LhBXwjJlFOYSTcU,3319
72
72
  konduktor/manifests/apoxy-setup2.yaml,sha256=BhXsgcVrLBruLXnF7xlj0Ej6YVJFYMABJIpYtwakQMo,731
@@ -92,16 +92,16 @@ konduktor/utils/constants.py,sha256=1DneiTR21lvKUcWdBGwC4I4fD4uPjbjLUilEnJS7rzA,
92
92
  konduktor/utils/env_options.py,sha256=T41Slzf4Mzl-n45CGXXqdy2fCrYhPNZQ7RP5vmnN4xc,2258
93
93
  konduktor/utils/exceptions.py,sha256=5IFnN5bIUSBJv4KRRrCepk5jyY9EG5vWWQqbjCmP3NU,6682
94
94
  konduktor/utils/kubernetes_enums.py,sha256=SabUueF6Bpzbpa57gyH5VB65xla2N9l8CZmAeYTfGmM,176
95
- konduktor/utils/kubernetes_utils.py,sha256=7RThCOiyaALRqbwHZ40qMnBsbAgt669k0NHkxtfx7Bs,26205
96
- konduktor/utils/log_utils.py,sha256=K96vSLcG-5DCtLO37fxFsCodR507envw4ZARNzJboTU,17231
97
- konduktor/utils/loki_utils.py,sha256=h2ZvZQr1nE_wXXsKsGMjhG2s2MXknNd4icydTR_ruKU,3539
95
+ konduktor/utils/kubernetes_utils.py,sha256=XleYxzG64hciZb-CjzBDjX8BOMhFATIIHZlXD2bqN0Q,27186
96
+ konduktor/utils/log_utils.py,sha256=VUyTtN819BJnSwm33-73-h8aaD51Y5Gawt6ek2kU1tk,18181
97
+ konduktor/utils/loki_utils.py,sha256=eOGiD7dZNuwzmyXKiifyqz00EVh2nwcUPFSiPkac9y0,4050
98
98
  konduktor/utils/rich_utils.py,sha256=ycADW6Ij3wX3uT8ou7T8qxX519RxlkJivsLvUahQaJo,3583
99
- konduktor/utils/schemas.py,sha256=twDGauRd2pPg8CeIbNeoB_kgbtXPDBKy363gd5-UVa0,18842
99
+ konduktor/utils/schemas.py,sha256=X2q-Nuk71EfMQXl4QFOtFWlQgd8tC_jFAo5dScmlEQc,19067
100
100
  konduktor/utils/subprocess_utils.py,sha256=WoFkoFhGecPR8-rF8WJxbIe-YtV94LXz9UG64SDhCY4,9448
101
101
  konduktor/utils/ux_utils.py,sha256=LSH4b5lckD157qDF4keThxtkGdxNrAfGKmH1ewhZkm4,8646
102
102
  konduktor/utils/validator.py,sha256=gCB5v9Up9bCWD_92fS5ChfRRXj_m56Ky9uzd_77wXGI,16927
103
- konduktor_nightly-0.1.0.dev20250909104711.dist-info/LICENSE,sha256=MuuqTZbHvmqXR_aNKAXzggdV45ANd3wQ5YI7tnpZhm0,6586
104
- konduktor_nightly-0.1.0.dev20250909104711.dist-info/METADATA,sha256=YjlJ5WuU4dp1-Zs9yA2Okl-yCUpf0Rtbkv8hERQZOp0,4247
105
- konduktor_nightly-0.1.0.dev20250909104711.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
106
- konduktor_nightly-0.1.0.dev20250909104711.dist-info/entry_points.txt,sha256=k3nG5wDFIJhNqsZWrHk4d0irIB2Ns9s47cjRWYsTCT8,48
107
- konduktor_nightly-0.1.0.dev20250909104711.dist-info/RECORD,,
103
+ konduktor_nightly-0.1.0.dev20250910100913.dist-info/LICENSE,sha256=MuuqTZbHvmqXR_aNKAXzggdV45ANd3wQ5YI7tnpZhm0,6586
104
+ konduktor_nightly-0.1.0.dev20250910100913.dist-info/METADATA,sha256=sPXDtwbnZ04QEPu5LKgMv0jgEM3l-r57S0B4U03pXCo,4247
105
+ konduktor_nightly-0.1.0.dev20250910100913.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
106
+ konduktor_nightly-0.1.0.dev20250910100913.dist-info/entry_points.txt,sha256=k3nG5wDFIJhNqsZWrHk4d0irIB2Ns9s47cjRWYsTCT8,48
107
+ konduktor_nightly-0.1.0.dev20250910100913.dist-info/RECORD,,