konduktor-nightly 0.1.0.dev20250530104807__py3-none-any.whl → 0.1.0.dev20250531104602__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.
konduktor/__init__.py CHANGED
@@ -14,7 +14,7 @@ __all__ = [
14
14
  ]
15
15
 
16
16
  # Replaced with the current commit when building the wheels.
17
- _KONDUKTOR_COMMIT_SHA = '3424c3df69fe44e339d2575f7090470185d5f549'
17
+ _KONDUKTOR_COMMIT_SHA = 'cf8484c6676903ded1f8aa2758f8cac533329c05'
18
18
  os.makedirs(os.path.expanduser('~/.konduktor'), exist_ok=True)
19
19
 
20
20
 
@@ -48,5 +48,5 @@ def _get_git_commit():
48
48
 
49
49
 
50
50
  __commit__ = _get_git_commit()
51
- __version__ = '1.0.0.dev0.1.0.dev20250530104807'
51
+ __version__ = '1.0.0.dev0.1.0.dev20250531104602'
52
52
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
@@ -184,6 +184,32 @@ def create_pod_spec(task: 'konduktor.Task') -> Dict[str, Any]:
184
184
  f'Failed to set k8s secret {secret_name}: \n{result}'
185
185
  )
186
186
 
187
+ # Mount the user's secrets
188
+ git_ssh_secret_name = None
189
+ env_secret_envs = []
190
+
191
+ context = kubernetes_utils.get_current_kube_config_context_name()
192
+ namespace = kubernetes_utils.get_kube_config_context_namespace(context)
193
+ user_hash = common_utils.get_user_hash()
194
+ label_selector = f'konduktor/owner={user_hash}'
195
+ user_secrets = kubernetes_utils.list_secrets(
196
+ namespace, context, label_filter=label_selector
197
+ )
198
+
199
+ for secret in user_secrets:
200
+ kind = kubernetes_utils.get_secret_kind(secret)
201
+ if kind == 'git-ssh' and git_ssh_secret_name is None:
202
+ git_ssh_secret_name = secret.metadata.name
203
+ elif kind == 'env':
204
+ secret_name = secret.metadata.name
205
+ key = next(iter(secret.data))
206
+ env_secret_envs.append(
207
+ {
208
+ 'name': key,
209
+ 'valueFrom': {'secretKeyRef': {'name': secret_name, 'key': key}},
210
+ }
211
+ )
212
+
187
213
  with tempfile.NamedTemporaryFile() as temp:
188
214
  common_utils.fill_template(
189
215
  'pod.yaml.j2',
@@ -210,6 +236,9 @@ def create_pod_spec(task: 'konduktor.Task') -> Dict[str, Any]:
210
236
  # SSH
211
237
  'enable_ssh': enable_ssh,
212
238
  'secret_name': secret_name,
239
+ # Kinds of Secrets
240
+ # --kind git-ssh
241
+ 'git_ssh': git_ssh_secret_name,
213
242
  },
214
243
  temp.name,
215
244
  )
@@ -218,16 +247,24 @@ def create_pod_spec(task: 'konduktor.Task') -> Dict[str, Any]:
218
247
  kubernetes_utils.combine_pod_config_fields(temp.name, pod_config)
219
248
  pod_config = common_utils.read_yaml(temp.name)
220
249
 
221
- for env_var in pod_config['kubernetes']['pod_config']['spec']['containers'][0][
222
- 'env'
223
- ]:
224
- if env_var['name'] in task.envs:
225
- env_var['value'] = task.envs.pop(env_var['name'])
250
+ # Priority order: task.envs > secret envs > existing pod_config envs
251
+ existing_envs = pod_config['kubernetes']['pod_config']['spec']['containers'][0].get(
252
+ 'env', []
253
+ )
254
+ env_map = {env['name']: env for env in existing_envs}
255
+
256
+ # Inject secret envs
257
+ for env in env_secret_envs:
258
+ env_map[env['name']] = env
226
259
 
227
- for k, v in task.envs.items():
228
- pod_config['kubernetes']['pod_config']['spec']['containers'][0][
229
- 'env'
230
- ].append({'name': k, 'value': v})
260
+ # Inject task.envs
261
+ for k, v in task.envs.items():
262
+ env_map[k] = {'name': k, 'value': v}
263
+
264
+ # Replace the container's env section with the merged and prioritized map
265
+ pod_config['kubernetes']['pod_config']['spec']['containers'][0]['env'] = list(
266
+ env_map.values()
267
+ )
231
268
 
232
269
  # TODO(asaiacai): have some schema validations. see
233
270
  # https://github.com/skypilot-org/skypilot/pull/4466
konduktor/cli.py CHANGED
@@ -35,7 +35,9 @@ each other.
35
35
  """
36
36
 
37
37
  import os
38
+ import pathlib
38
39
  import shlex
40
+ from base64 import b64encode
39
41
  from typing import Any, Dict, List, Optional, Tuple
40
42
 
41
43
  import click
@@ -72,7 +74,7 @@ def _parse_env_var(env_var: str) -> Tuple[str, str]:
72
74
  ret = tuple(env_var.split('=', 1))
73
75
  if len(ret) != 2:
74
76
  raise click.UsageError(
75
- f'Invalid env var: {env_var}. Must be in the form of KEY=VAL ' 'or KEY.'
77
+ f'Invalid env var: {env_var}. Must be in the form of KEY=VALUE'
76
78
  )
77
79
  return ret[0], ret[1]
78
80
 
@@ -780,6 +782,287 @@ def check(clouds: Tuple[str]):
780
782
  konduktor_check.check(clouds=clouds_arg)
781
783
 
782
784
 
785
+ class KeyValueType(click.ParamType):
786
+ name = 'key=value'
787
+
788
+ def convert(self, value, param, ctx):
789
+ if '=' not in value:
790
+ self.fail(f'{value!r} is not a valid key=value pair', param, ctx)
791
+ key, val = value.split('=', 1)
792
+ return key, val
793
+
794
+
795
+ _SECRET_CREATE_OPTIONS = [
796
+ click.option(
797
+ '--inline',
798
+ type=KeyValueType(),
799
+ help='Key=value pair to store as an env secret (only valid with --kind env).',
800
+ ),
801
+ click.option(
802
+ '--from-file',
803
+ '--from_file',
804
+ type=click.Path(dir_okay=False),
805
+ help='Path to a single file to store as a secret.',
806
+ ),
807
+ click.option(
808
+ '--from-directory',
809
+ '--from_directory',
810
+ type=click.Path(file_okay=False),
811
+ help='Path to a directory to store as a multi-file secret.',
812
+ ),
813
+ click.option(
814
+ '--kind',
815
+ default='default',
816
+ type=click.Choice(['default', 'env', 'git-ssh']),
817
+ help='Type of secret being created. More kinds coming soon.',
818
+ ),
819
+ ]
820
+
821
+
822
+ @cli.group(cls=_NaturalOrderGroup)
823
+ def secret():
824
+ """Manage secrets used in Konduktor.
825
+
826
+ USAGE: konduktor secret COMMAND
827
+
828
+ \b
829
+ Use one of the following COMMANDS:
830
+ create [FLAGS] [NAME]
831
+ delete [NAME]
832
+ list [FLAGS]
833
+
834
+ \b
835
+ Examples:
836
+ konduktor secret create --kind git-ssh --from-file=~/.ssh/id_rsa my-ssh-name
837
+ konduktor secret create --kind env --inline FOO=bar my-env-name
838
+ konduktor delete my-ssh-name
839
+ konduktor list
840
+
841
+ \b
842
+ For details on COMMAND ARGS:
843
+ konduktor secret create -h
844
+ konduktor secret delete -h
845
+ konduktor secret list -h
846
+ """
847
+
848
+
849
+ @_add_click_options(_SECRET_CREATE_OPTIONS)
850
+ @secret.command()
851
+ @click.argument('name', required=True)
852
+ def create(kind, from_file, from_directory, inline, name):
853
+ """Create a new secret."""
854
+
855
+ if not kubernetes_utils.is_k8s_resource_name_valid(name):
856
+ raise click.BadParameter(
857
+ f'Invalid secret name: {name}. '
858
+ f'Name must consist of lower case alphanumeric characters or -, '
859
+ f'and must start and end with alphanumeric characters.',
860
+ )
861
+
862
+ basename = name
863
+ secret_name = f'{basename}-{common_utils.get_user_hash()}'
864
+
865
+ context = kubernetes_utils.get_current_kube_config_context_name()
866
+ namespace = kubernetes_utils.get_kube_config_context_namespace(context)
867
+
868
+ from_file = os.path.expanduser(from_file) if from_file else None
869
+ from_directory = os.path.expanduser(from_directory) if from_directory else None
870
+
871
+ sources = [bool(from_file), bool(from_directory), bool(inline)]
872
+
873
+ if sources.count(True) > 1:
874
+ raise click.UsageError(
875
+ 'Only one of --from-file, --from-directory, or --inline can be used.\n'
876
+ 'Examples:\n'
877
+ f' {colorama.Style.BRIGHT}konduktor secret create --kind git-ssh '
878
+ f'--from-file=~/.ssh/id_rsa my-ssh-name\n{colorama.Style.RESET_ALL}'
879
+ f' {colorama.Style.BRIGHT}konduktor secret create --kind env '
880
+ f'--inline FOO=bar my-env-name{colorama.Style.RESET_ALL}'
881
+ )
882
+
883
+ if sources.count(True) == 0:
884
+ raise click.UsageError(
885
+ 'You must specify one of --from-file, --from-directory, or --inline.\n'
886
+ 'Examples:\n'
887
+ f' {colorama.Style.BRIGHT}konduktor secret create --kind git-ssh '
888
+ f'--from-file=~/.ssh/id_rsa my-ssh-name\n{colorama.Style.RESET_ALL}'
889
+ f' {colorama.Style.BRIGHT}konduktor secret create --kind env '
890
+ f'--inline FOO=bar my-env-name{colorama.Style.RESET_ALL}'
891
+ )
892
+
893
+ if from_file and not os.path.isfile(from_file):
894
+ raise click.BadParameter(
895
+ f'--from-file {from_file} does not exist or is not a file'
896
+ )
897
+ if from_directory and not os.path.isdir(from_directory):
898
+ raise click.BadParameter(
899
+ f'--from-directory {from_directory} does not exist or is not a directory'
900
+ )
901
+
902
+ if kind == 'git-ssh' and not from_file:
903
+ raise click.UsageError(
904
+ '--kind git-ssh requires --from-file (not --from-directory or --inline). \n'
905
+ 'Example:\n'
906
+ f' {colorama.Style.BRIGHT}konduktor secret create --kind git-ssh '
907
+ f'--from-file=~/.ssh/id_rsa my-ssh-name{colorama.Style.RESET_ALL}'
908
+ )
909
+ if kind == 'env' and not inline:
910
+ raise click.UsageError(
911
+ '--kind env requires --inline (not --from-file or --from-directory). \n'
912
+ 'Example:\n'
913
+ f' {colorama.Style.BRIGHT}konduktor secret create --kind env '
914
+ f'--inline FOO=bar my-env-name{colorama.Style.RESET_ALL}'
915
+ )
916
+
917
+ data = {}
918
+ if from_directory:
919
+ click.echo(f'Creating secret from directory: {from_directory}')
920
+ base_path = pathlib.Path(from_directory)
921
+ for path in base_path.rglob('*'):
922
+ if path.is_file():
923
+ rel_path = path.relative_to(base_path)
924
+ with open(path, 'rb') as f:
925
+ data[str(rel_path)] = b64encode(f.read()).decode()
926
+ elif from_file:
927
+ click.echo(f'Creating secret from file: {from_file}')
928
+ key = os.path.basename(from_file)
929
+ if kind == 'git-ssh':
930
+ key = 'gitkey'
931
+ try:
932
+ with open(from_file, 'rb') as f:
933
+ data[key] = b64encode(f.read()).decode()
934
+ except OSError as e:
935
+ raise click.ClickException(f'Failed to read {kind} file {from_file}: {e}')
936
+ else:
937
+ click.echo('Creating secret from inline key=value pair')
938
+ key, value = inline
939
+ data = {key: b64encode(value.encode()).decode()}
940
+
941
+ secret_metadata = {
942
+ 'name': secret_name,
943
+ 'labels': {
944
+ 'parent': 'konduktor',
945
+ 'konduktor/owner': common_utils.get_user_hash(),
946
+ 'konduktor/basename': basename,
947
+ 'konduktor/secret-kind': kind or None,
948
+ },
949
+ }
950
+
951
+ # Limit --kind git-ssh secret to 1 max per user
952
+ # Overwrites if user trying to create more than 1
953
+ if kind == 'git-ssh':
954
+ user_hash = common_utils.get_user_hash()
955
+ label_selector = f'konduktor/owner={user_hash}'
956
+ existing = kubernetes_utils.list_secrets(
957
+ namespace, context, label_filter=label_selector
958
+ )
959
+ for s in existing:
960
+ labels = s.metadata.labels or {}
961
+ if labels.get('konduktor/secret-kind') == 'git-ssh':
962
+ old_name = s.metadata.name
963
+ click.echo(f'Found existing git-ssh secret: {old_name}, deleting it.')
964
+ kubernetes_utils.delete_secret(
965
+ secret_name=old_name, namespace=namespace, context=context
966
+ )
967
+ break
968
+
969
+ ok, err = kubernetes_utils.set_secret(
970
+ secret_name=secret_name,
971
+ namespace=namespace,
972
+ context=context,
973
+ data=data,
974
+ secret_metadata=secret_metadata,
975
+ )
976
+ if not ok:
977
+ raise click.ClickException(f'Failed to create secret: {err}')
978
+ click.secho(f'Secret {basename} created in namespace {namespace}.', fg='green')
979
+
980
+
981
+ @secret.command()
982
+ @click.argument('name', required=True)
983
+ def delete(name):
984
+ """Delete a secret by name."""
985
+
986
+ context = kubernetes_utils.get_current_kube_config_context_name()
987
+ namespace = kubernetes_utils.get_kube_config_context_namespace(context)
988
+ user_hash = common_utils.get_user_hash()
989
+
990
+ label_selector = f'konduktor/owner={user_hash}'
991
+ secrets = kubernetes_utils.list_secrets(
992
+ namespace, context, label_filter=label_selector
993
+ )
994
+
995
+ matches = [
996
+ s
997
+ for s in secrets
998
+ if s.metadata.labels and s.metadata.labels.get('konduktor/basename') == name
999
+ ]
1000
+
1001
+ if not matches:
1002
+ raise click.ClickException(
1003
+ f'No secret named "{name}" owned by you found in namespace {namespace}.'
1004
+ )
1005
+ elif len(matches) > 1:
1006
+ raise click.ClickException(f'Multiple secrets with basename "{name}" found.')
1007
+
1008
+ full_name = matches[0].metadata.name
1009
+
1010
+ ok, err = kubernetes_utils.delete_secret(full_name, namespace, context)
1011
+ if not ok:
1012
+ raise click.ClickException(f'Failed to delete secret: {err}')
1013
+ click.secho(f'Secret {name} deleted from namespace {namespace}.', fg='yellow')
1014
+
1015
+
1016
+ @secret.command(name='list')
1017
+ @click.option(
1018
+ '--all-users',
1019
+ '--all_users',
1020
+ '-u',
1021
+ is_flag=True,
1022
+ default=False,
1023
+ help='Show all secrets, including those not owned by the current user.',
1024
+ )
1025
+ def list_secrets(all_users: bool):
1026
+ """List secrets in the namespace.
1027
+ Defaults to only your secrets unless --all-users is set."""
1028
+
1029
+ context = kubernetes_utils.get_current_kube_config_context_name()
1030
+ namespace = kubernetes_utils.get_kube_config_context_namespace(context)
1031
+
1032
+ if not all_users:
1033
+ user_hash = common_utils.get_user_hash()
1034
+ username = common_utils.get_cleaned_username()
1035
+ label_selector = f'konduktor/owner={user_hash}'
1036
+ secrets = kubernetes_utils.list_secrets(
1037
+ namespace, context, label_filter=label_selector
1038
+ )
1039
+ else:
1040
+ secrets = kubernetes_utils.list_secrets(namespace, context)
1041
+
1042
+ if not secrets:
1043
+ if all_users:
1044
+ click.secho(f'No secrets found in {namespace}.', fg='yellow')
1045
+ else:
1046
+ click.secho(f'No secrets found for {username} in {namespace}.', fg='yellow')
1047
+ return
1048
+
1049
+ if all_users:
1050
+ click.secho(f'All secrets in {namespace} namespace:\n', bold=True)
1051
+ else:
1052
+ click.secho(f'Secrets in {namespace} namespace owned by you:\n', bold=True)
1053
+
1054
+ for s in secrets:
1055
+ labels = s.metadata.labels or {}
1056
+ basename = labels.get('konduktor/basename', s.metadata.name)
1057
+ kind = labels.get('konduktor/secret-kind', '(none)')
1058
+ owner = labels.get('konduktor/owner', '(none)')
1059
+
1060
+ if all_users:
1061
+ click.echo(f'{basename:30} kind={kind:10} owner={owner}')
1062
+ else:
1063
+ click.echo(f'{basename:30} kind={kind:10}')
1064
+
1065
+
783
1066
  def main():
784
1067
  return cli()
785
1068
 
konduktor/data/aws/s3.py CHANGED
@@ -1033,6 +1033,13 @@ class S3Store(storage_utils.AbstractStore):
1033
1033
  os.path.expanduser(os.path.join(credentials_dir, f))
1034
1034
  for f in _CREDENTIAL_FILES
1035
1035
  ]
1036
+
1037
+ secret_metadata = {
1038
+ 'labels': {
1039
+ 'konduktor/secret-kind': 'S3',
1040
+ },
1041
+ }
1042
+
1036
1043
  ok, result = kubernetes_utils.set_secret(
1037
1044
  secret_name=cls._AWS_SECRET_NAME,
1038
1045
  namespace=namespace,
@@ -1042,6 +1049,7 @@ class S3Store(storage_utils.AbstractStore):
1042
1049
  credentials_files
1043
1050
  )
1044
1051
  },
1052
+ secret_metadata=secret_metadata,
1045
1053
  )
1046
1054
  if not ok:
1047
1055
  logger.error(f'Failed to set AWS credentials in k8s secret: \n{result}')
konduktor/data/gcp/gcs.py CHANGED
@@ -887,6 +887,13 @@ class GcsStore(storage_utils.AbstractStore):
887
887
  os.path.expanduser(os.path.join(credentials_dir, f))
888
888
  for f in _CREDENTIAL_FILES
889
889
  ]
890
+
891
+ secret_metadata = {
892
+ 'labels': {
893
+ 'konduktor/secret-kind': 'GCS',
894
+ },
895
+ }
896
+
890
897
  ok, result = kubernetes_utils.set_secret(
891
898
  secret_name=cls._GCP_SECRET_NAME,
892
899
  namespace=namespace,
@@ -896,6 +903,7 @@ class GcsStore(storage_utils.AbstractStore):
896
903
  credentials_files
897
904
  )
898
905
  },
906
+ secret_metadata=secret_metadata,
899
907
  )
900
908
  if not ok:
901
909
  logger.error(f'Failed to set GCP credentials in k8s secret: \n{result}')
@@ -72,6 +72,10 @@ kubernetes:
72
72
  name: {{ secret_name }}
73
73
  key: PRIVKEY
74
74
  {% endif %}
75
+ {% if git_ssh %}
76
+ - name: GIT_SSH_COMMAND
77
+ value: "ssh -i /run/konduktor/git-ssh-secret/gitkey -o StrictHostKeyChecking=no"
78
+ {% endif %}
75
79
  # these are for compatibility with skypilot
76
80
  - name: SKYPILOT_NODE_IPS
77
81
  value: "{{ node_hostnames }}"
@@ -92,6 +96,10 @@ kubernetes:
92
96
  - name: {{ secret_type }}-secret
93
97
  mountPath: /run/konduktor/{{ secret_type }}-secret
94
98
  {% endfor %}
99
+ {% if git_ssh %}
100
+ - name: git-ssh-secret
101
+ mountPath: /run/konduktor/git-ssh-secret
102
+ {% endif %}
95
103
  command: ["bash", "-c"]
96
104
  args:
97
105
  - |
@@ -275,6 +283,9 @@ kubernetes:
275
283
  $(prefix_cmd) unzip /run/konduktor/s3-secret/awscredentials -d ~/.aws
276
284
  {% endif %}
277
285
  {% endfor %}
286
+ {% if git_ssh %}
287
+ $(prefix_cmd) echo "Unpacking GIT-SSH secret"
288
+ {% endif %}
278
289
  end_epoch=$(date +%s);
279
290
  $(prefix_cmd) echo "===== KONDUKTOR: Unpacking secrets credentials took $((end_epoch - start_epoch)) seconds ====="
280
291
 
@@ -337,6 +348,13 @@ kubernetes:
337
348
  secret:
338
349
  secretName: {{ secret_name }}
339
350
  {% endfor %}
351
+ {% if git_ssh %}
352
+ - name: git-ssh-secret
353
+ secret:
354
+ secretName: {{ git_ssh }}
355
+ defaultMode: 384
356
+ {% endif %}
357
+
340
358
 
341
359
  # TODO(asaiacai): should we add nodeSelectors here or leave to
342
360
  # kueue resource flavors. leaning towards defining
@@ -193,6 +193,10 @@ def get_user_hash(force_fresh_hash: bool = False) -> str:
193
193
  local client.
194
194
  """
195
195
 
196
+ override = os.environ.get('KONDUKTOR_TEST_USER_HASH')
197
+ if override:
198
+ return override
199
+
196
200
  def _is_valid_user_hash(user_hash: Optional[str]) -> bool:
197
201
  if user_hash is None:
198
202
  return False
@@ -33,6 +33,8 @@ DEFAULT_NAMESPACE = 'default'
33
33
 
34
34
  DEFAULT_SERVICE_ACCOUNT_NAME = 'konduktor-service-account'
35
35
 
36
+ DNS_SUBDOMAIN_REGEX = r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
37
+
36
38
  MEMORY_SIZE_UNITS = {
37
39
  'B': 1,
38
40
  'K': 2**10,
@@ -579,6 +581,7 @@ def set_secret(
579
581
  namespace: str,
580
582
  context: Optional[str],
581
583
  data: Dict[str, str],
584
+ secret_metadata: Optional[Dict[str, Any]] = None,
582
585
  ) -> Tuple[bool, Optional[str]]:
583
586
  """
584
587
  Create/update a secret in a namespace. Values are encoded to base64.
@@ -588,26 +591,46 @@ def set_secret(
588
591
  ```
589
592
  """
590
593
  with _K8s_CLIENT_LOCK:
591
- secret_exists, response = check_secret_exists(
592
- secret_name=secret_name,
593
- namespace=namespace,
594
- context=context,
594
+ user_hash = common_utils.get_user_hash()
595
+
596
+ full_name = (
597
+ secret_metadata.get('name')
598
+ if secret_metadata and 'name' in secret_metadata
599
+ else secret_name
595
600
  )
601
+ assert isinstance(full_name, str), 'Secret name must be a string'
602
+
603
+ metadata: Dict[str, Any] = {
604
+ 'name': full_name,
605
+ 'labels': {
606
+ 'parent': 'konduktor',
607
+ 'konduktor/owner': user_hash,
608
+ 'konduktor/basename': secret_name,
609
+ },
610
+ }
611
+
612
+ if secret_metadata:
613
+ metadata['labels'].update(secret_metadata.get('labels', {}))
596
614
 
597
- secret_metadata = {'name': secret_name, 'labels': {'parent': 'konduktor'}}
598
615
  custom_metadata = config.get_nested(('kubernetes', 'custom_metadata'), {})
599
- config.merge_k8s_configs(secret_metadata, custom_metadata)
616
+ config.merge_k8s_configs(metadata, custom_metadata)
600
617
 
601
618
  secret = kubernetes.client.V1Secret(
602
- metadata=kubernetes.client.V1ObjectMeta(**secret_metadata),
619
+ metadata=kubernetes.client.V1ObjectMeta(**metadata),
603
620
  type='Opaque',
604
621
  data=data,
605
622
  )
606
623
 
624
+ secret_exists, _ = check_secret_exists(
625
+ secret_name=full_name,
626
+ namespace=namespace,
627
+ context=context,
628
+ )
629
+
607
630
  try:
608
631
  if secret_exists:
609
632
  kube_client.core_api(context).patch_namespaced_secret(
610
- secret_name, namespace, secret
633
+ full_name, namespace, secret
611
634
  )
612
635
  else:
613
636
  kube_client.core_api(context).create_namespaced_secret(
@@ -617,12 +640,50 @@ def set_secret(
617
640
  return False, str(e)
618
641
  else:
619
642
  logger.debug(
620
- f'Secret {secret_name} in namespace {namespace} '
643
+ f'Secret {full_name} in namespace {namespace} '
621
644
  f'in context {context} created/updated'
622
645
  )
623
646
  return True, None
624
647
 
625
648
 
649
+ def list_secrets(
650
+ namespace: str,
651
+ context: Optional[str],
652
+ label_filter: Optional[str] = None,
653
+ ) -> List[kubernetes.client.V1Secret]:
654
+ """List all secrets in a namespace, optionally filtering by label."""
655
+ secrets = kube_client.core_api(context).list_namespaced_secret(namespace).items
656
+ if label_filter:
657
+ key, val = label_filter.split('=', 1)
658
+ return [
659
+ s
660
+ for s in secrets
661
+ if s.metadata.labels and s.metadata.labels.get(key) == val
662
+ ]
663
+ return secrets
664
+
665
+
666
+ def delete_secret(
667
+ name: str,
668
+ namespace: str,
669
+ context: Optional[str],
670
+ ) -> Tuple[bool, Optional[str]]:
671
+ """Deletes a secret by name in the given namespace/context."""
672
+ try:
673
+ kube_client.core_api(context).delete_namespaced_secret(name, namespace)
674
+ logger.debug(f'Secret {name} deleted from namespace {namespace}')
675
+ return True, None
676
+ except kube_client.api_exception() as e:
677
+ return False, str(e)
678
+
679
+
680
+ def get_secret_kind(secret: kubernetes.client.V1Secret) -> Optional[str]:
681
+ """Get the konduktor-specific kind of a secret, if labeled."""
682
+ if secret.metadata.labels:
683
+ return secret.metadata.labels.get('konduktor/secret-kind')
684
+ return None
685
+
686
+
626
687
  def get_autoscaler_type() -> Optional[kubernetes_enums.KubernetesAutoscalerType]:
627
688
  """Returns the autoscaler type by reading from config"""
628
689
  autoscaler_type = config.get_nested(('kubernetes', 'autoscaler'), None)
@@ -661,3 +722,10 @@ def is_label_valid(label_key: str, label_value: str) -> Tuple[bool, Optional[str
661
722
  if not key_valid or not value_valid:
662
723
  return False, error_msg
663
724
  return True, None
725
+
726
+
727
+ def is_k8s_resource_name_valid(name):
728
+ """Returns whether or not a k8s name is valid (must consist of
729
+ lower case alphanumeric characters or -, and must start and end
730
+ with alphanumeric characters)"""
731
+ return re.match(DNS_SUBDOMAIN_REGEX, name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: konduktor-nightly
3
- Version: 0.1.0.dev20250530104807
3
+ Version: 0.1.0.dev20250531104602
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=xvEeJ9mWysi91Zif_yvb9nLV-KoVK8jtnTCnsswcSsA,1540
1
+ konduktor/__init__.py,sha256=J0xYwCSHyRmCY7OMSztuG9RmIeNVp0_DIGVQd6MZgMU,1540
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
@@ -7,9 +7,9 @@ konduktor/authentication.py,sha256=_mVy3eqoKohicHostFiGwG1-2ybxP-l7ouofQ0LRlCY,4
7
7
  konduktor/backends/__init__.py,sha256=1Q6sqqdeMYarpTX_U-QVywJYf7idiUTRsyP-E4BQSOw,129
8
8
  konduktor/backends/backend.py,sha256=qh0bp94lzoTYZkzyQv2-CVrB5l91FkG2vclXg24UFC0,2910
9
9
  konduktor/backends/jobset.py,sha256=UdhwAuZODLMbLY51Y2zOBsh6wg4Pb84oHVvUKzx3Z2w,8434
10
- konduktor/backends/jobset_utils.py,sha256=4vMYOhTENfBL9khzFuj69-Vy4g0sBkUpXX-1bfPnVys,20054
10
+ konduktor/backends/jobset_utils.py,sha256=diGpy-qpsQeVMFZVQsMgG3HxJxl2huxqbRU6FMA0QHY,21363
11
11
  konduktor/check.py,sha256=JennyWoaqSKhdyfUldd266KwVXTPJpcYQa4EED4a_BA,7569
12
- konduktor/cli.py,sha256=Fl1dwNB5T-kDQAlAoOJetzl6RYt9FYUlowKjbNhVjkQ,23412
12
+ konduktor/cli.py,sha256=qiTFut28crvcXOoSvnN3NcTb-xPmtFB336zdt9Q2bxU,33370
13
13
  konduktor/config.py,sha256=J50JxC6MsXMnlrJPXdDUMr38C89xvOO7mR8KJ6fyils,15520
14
14
  konduktor/constants.py,sha256=T3AeXXxuQHINW_bAWyztvDeS8r4g8kXBGIwIq13cys0,1814
15
15
  konduktor/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -51,12 +51,12 @@ konduktor/dashboard/frontend/server.js,sha256=jcp6_Ww9YJD3uKY07jR3KMlAM6n1QZdxZn
51
51
  konduktor/dashboard/frontend/tailwind.config.js,sha256=fCnc48wvioIDOe5ldQ_6RE7F76cP7aU7pDrxBPJx-Fk,366
52
52
  konduktor/data/__init__.py,sha256=KMR2i3E9YcIpiIuCxtRdS7BQ1w2vUAbbve7agziJrLo,213
53
53
  konduktor/data/aws/__init__.py,sha256=_6zWfNNAK1QGgyKqg_yPYWcXlnffchyvIMErYa6tw_U,331
54
- konduktor/data/aws/s3.py,sha256=T4FnCxilNp35bsgmE7j5O3j15FVbgWRdUH8YFXCiwSw,48335
54
+ konduktor/data/aws/s3.py,sha256=lNgI02wacyXudyrIfXPKrscH4o153Wa_o5qpQR-jjLQ,48506
55
55
  konduktor/data/constants.py,sha256=yXVEoTI2we1xOjVSU-bjRCQCLpVvpEvJ0GedXvSwEfw,127
56
56
  konduktor/data/data_utils.py,sha256=IG1jgb_La997wi90xCvxYYsHQRlmm8Aooq04ZSf8EDI,9670
57
57
  konduktor/data/gcp/__init__.py,sha256=rlQxACBC_Vu36mdgPyJgUy4mGc_6Nt_a96JAuaPz2pQ,489
58
58
  konduktor/data/gcp/constants.py,sha256=dMfOiFccM8O6rUi9kClJcbvw1K1VnS1JzzQk3apq8ho,1483
59
- konduktor/data/gcp/gcs.py,sha256=nqhCvQuGpHFPoxT5SKgxL25KtZuSg377Nh1bICiQwlc,42057
59
+ konduktor/data/gcp/gcs.py,sha256=Zc1LXrjoeNU9EDK229evrKxjVqsKIUicbtYlugA_TiY,42229
60
60
  konduktor/data/gcp/utils.py,sha256=FJQcMXZqtMIzjZ98b3lTTc0UbdPUKTDLsOsfJaaH5-s,214
61
61
  konduktor/data/registry.py,sha256=CUbMsN_Q17Pf4wRHkqZrycErEjTP7cLEdgcfwVGcEpc,696
62
62
  konduktor/data/storage.py,sha256=o2So-bY9glvgbGdoN7AQNYmNnvGf1AUDPpImtadRL90,35213
@@ -71,19 +71,19 @@ konduktor/manifests/pod_cleanup_controller.yaml,sha256=hziL1Ka1kCAEL9R7Tjvpb80iw
71
71
  konduktor/resource.py,sha256=w2PdIrmQaJWA-GLSmVBcg4lxwuxvPulz35_YSKa5o24,19254
72
72
  konduktor/task.py,sha256=ofwd8WIhfD6C3ThLcv6X3GUzQHyZ6ddjUagE-umF4K0,35207
73
73
  konduktor/templates/jobset.yaml.j2,sha256=onYiHtXAgk-XBtji994hPu_g0hxnLzvmfxwjbdKdeZc,960
74
- konduktor/templates/pod.yaml.j2,sha256=XPG87LYieHlMxJSDVlWlU2c1eroKYYP5m3bz3daJDgY,15508
74
+ konduktor/templates/pod.yaml.j2,sha256=JvDruGpBbRHSklqNEeKSvPH0Y1uldvcylqLUaIcguuQ,16086
75
75
  konduktor/usage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  konduktor/usage/constants.py,sha256=gCL8afIHZhO0dcxbJGpESE9sCC1cBSbeRnQ8GwNOY4M,612
77
77
  konduktor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  konduktor/utils/accelerator_registry.py,sha256=1tpVIaM0UZ3w1desPhVEwNCUruamhP-igDZrcfaoRWI,574
79
79
  konduktor/utils/annotations.py,sha256=oy2-BLydkFt3KWkXDuaGY84d6b7iISuy4eAT9uXk0Fc,2225
80
80
  konduktor/utils/base64_utils.py,sha256=mF-Tw98mFRG70YE4w6s9feuQSCYZHOb8YatBZwMugyI,3130
81
- konduktor/utils/common_utils.py,sha256=F5x7k4AdBB44u8PYRkaugORnZKnK3JLqGn1jHOKgUYo,14960
81
+ konduktor/utils/common_utils.py,sha256=4yG5Kjvu1hu6x2nKNaaCUKQNrheUaG61Qe913MFPry8,15060
82
82
  konduktor/utils/constants.py,sha256=1DneiTR21lvKUcWdBGwC4I4fD4uPjbjLUilEnJS7rzA,216
83
83
  konduktor/utils/env_options.py,sha256=T41Slzf4Mzl-n45CGXXqdy2fCrYhPNZQ7RP5vmnN4xc,2258
84
84
  konduktor/utils/exceptions.py,sha256=5IFnN5bIUSBJv4KRRrCepk5jyY9EG5vWWQqbjCmP3NU,6682
85
85
  konduktor/utils/kubernetes_enums.py,sha256=SabUueF6Bpzbpa57gyH5VB65xla2N9l8CZmAeYTfGmM,176
86
- konduktor/utils/kubernetes_utils.py,sha256=1MZHwU4vy-exA4TA5_oTiV-zm1A2ayfeA0T_75DMFM8,23937
86
+ konduktor/utils/kubernetes_utils.py,sha256=VG7qatUFyWHY-PCQ8fYWh2kn2TMwfg84cn-VkXdCwI8,26077
87
87
  konduktor/utils/log_utils.py,sha256=oFCKkYKCS_e_GRw_-0F7WsiIZNqJL1RZ4cD5-zh59Q4,9765
88
88
  konduktor/utils/loki_utils.py,sha256=h2ZvZQr1nE_wXXsKsGMjhG2s2MXknNd4icydTR_ruKU,3539
89
89
  konduktor/utils/rich_utils.py,sha256=ycADW6Ij3wX3uT8ou7T8qxX519RxlkJivsLvUahQaJo,3583
@@ -91,8 +91,8 @@ konduktor/utils/schemas.py,sha256=2fHsTi3t9q3LXqOPrcpkmPsMbaoJBnuJstd6ULmDiUo,16
91
91
  konduktor/utils/subprocess_utils.py,sha256=WoFkoFhGecPR8-rF8WJxbIe-YtV94LXz9UG64SDhCY4,9448
92
92
  konduktor/utils/ux_utils.py,sha256=czCwiS1bDqgeKtzAJctczpLwFZzAse7WuozdvzEFYJ4,7437
93
93
  konduktor/utils/validator.py,sha256=tgBghVyedyzGx84-U2Qfoh_cJBE3oUk9gclMW90ORks,691
94
- konduktor_nightly-0.1.0.dev20250530104807.dist-info/LICENSE,sha256=MuuqTZbHvmqXR_aNKAXzggdV45ANd3wQ5YI7tnpZhm0,6586
95
- konduktor_nightly-0.1.0.dev20250530104807.dist-info/METADATA,sha256=rJvh5iMrpeeqvyDT7ZfGFVmgNCBzg8Aczb2f4W4LQXA,4289
96
- konduktor_nightly-0.1.0.dev20250530104807.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
97
- konduktor_nightly-0.1.0.dev20250530104807.dist-info/entry_points.txt,sha256=k3nG5wDFIJhNqsZWrHk4d0irIB2Ns9s47cjRWYsTCT8,48
98
- konduktor_nightly-0.1.0.dev20250530104807.dist-info/RECORD,,
94
+ konduktor_nightly-0.1.0.dev20250531104602.dist-info/LICENSE,sha256=MuuqTZbHvmqXR_aNKAXzggdV45ANd3wQ5YI7tnpZhm0,6586
95
+ konduktor_nightly-0.1.0.dev20250531104602.dist-info/METADATA,sha256=cN-qh6GPr5-9ZKsVP52lvNC6HqaLK-cW1XlWbiG9TpQ,4289
96
+ konduktor_nightly-0.1.0.dev20250531104602.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
97
+ konduktor_nightly-0.1.0.dev20250531104602.dist-info/entry_points.txt,sha256=k3nG5wDFIJhNqsZWrHk4d0irIB2Ns9s47cjRWYsTCT8,48
98
+ konduktor_nightly-0.1.0.dev20250531104602.dist-info/RECORD,,