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 +2 -2
- konduktor/backends/jobset_utils.py +46 -9
- konduktor/cli.py +284 -1
- konduktor/data/aws/s3.py +8 -0
- konduktor/data/gcp/gcs.py +8 -0
- konduktor/templates/pod.yaml.j2 +18 -0
- konduktor/utils/common_utils.py +4 -0
- konduktor/utils/kubernetes_utils.py +77 -9
- {konduktor_nightly-0.1.0.dev20250530104807.dist-info → konduktor_nightly-0.1.0.dev20250531104602.dist-info}/METADATA +1 -1
- {konduktor_nightly-0.1.0.dev20250530104807.dist-info → konduktor_nightly-0.1.0.dev20250531104602.dist-info}/RECORD +13 -13
- {konduktor_nightly-0.1.0.dev20250530104807.dist-info → konduktor_nightly-0.1.0.dev20250531104602.dist-info}/LICENSE +0 -0
- {konduktor_nightly-0.1.0.dev20250530104807.dist-info → konduktor_nightly-0.1.0.dev20250531104602.dist-info}/WHEEL +0 -0
- {konduktor_nightly-0.1.0.dev20250530104807.dist-info → konduktor_nightly-0.1.0.dev20250531104602.dist-info}/entry_points.txt +0 -0
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 = '
|
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.
|
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
|
-
|
222
|
-
|
223
|
-
]
|
224
|
-
|
225
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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=
|
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}')
|
konduktor/templates/pod.yaml.j2
CHANGED
@@ -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
|
konduktor/utils/common_utils.py
CHANGED
@@ -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
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
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(
|
616
|
+
config.merge_k8s_configs(metadata, custom_metadata)
|
600
617
|
|
601
618
|
secret = kubernetes.client.V1Secret(
|
602
|
-
metadata=kubernetes.client.V1ObjectMeta(**
|
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
|
-
|
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 {
|
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,4 +1,4 @@
|
|
1
|
-
konduktor/__init__.py,sha256=
|
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=
|
10
|
+
konduktor/backends/jobset_utils.py,sha256=diGpy-qpsQeVMFZVQsMgG3HxJxl2huxqbRU6FMA0QHY,21363
|
11
11
|
konduktor/check.py,sha256=JennyWoaqSKhdyfUldd266KwVXTPJpcYQa4EED4a_BA,7569
|
12
|
-
konduktor/cli.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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.
|
95
|
-
konduktor_nightly-0.1.0.
|
96
|
-
konduktor_nightly-0.1.0.
|
97
|
-
konduktor_nightly-0.1.0.
|
98
|
-
konduktor_nightly-0.1.0.
|
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,,
|
File without changes
|
File without changes
|