kblaunch 0.1.0__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.
kblaunch/__init__.py ADDED
File without changes
kblaunch/cli.py ADDED
@@ -0,0 +1,195 @@
1
+ import base64
2
+ import os
3
+
4
+ import typer
5
+ from kubejobs.jobs import KubernetesJob, KueueQueue
6
+ from kubernetes import client, config
7
+ from loguru import logger
8
+
9
+ app = typer.Typer()
10
+
11
+
12
+ def check_if_completed(job_name: str, namespace: str = "informatics") -> bool:
13
+ # Load the kube config
14
+ config.load_kube_config()
15
+
16
+ # Create an instance of the API class
17
+ api = client.BatchV1Api()
18
+
19
+ job_exists = False
20
+ is_completed = True
21
+
22
+ # Check if the job exists in the specified namespace
23
+ jobs = api.list_namespaced_job(namespace)
24
+ if job_name in {job.metadata.name for job in jobs.items}:
25
+ job_exists = True
26
+
27
+ if job_exists is True:
28
+ job = api.read_namespaced_job(job_name, namespace)
29
+ is_completed = False
30
+
31
+ # Check the status conditions
32
+ if job.status.conditions:
33
+ for condition in job.status.conditions:
34
+ if condition.type == "Complete" and condition.status == "True":
35
+ is_completed = True
36
+ elif condition.type == "Failed" and condition.status == "True":
37
+ logger.error(f"Job {job_name} has failed.")
38
+ else:
39
+ logger.info(f"Job {job_name} still running or status is unknown.")
40
+
41
+ if is_completed:
42
+ api_res = api.delete_namespaced_job(
43
+ name=job_name,
44
+ namespace=namespace,
45
+ body=client.V1DeleteOptions(propagation_policy="Foreground"),
46
+ )
47
+ logger.info(f"Job '{job_name}' deleted. Status: {api_res.status}")
48
+ return is_completed
49
+
50
+
51
+ def send_message_command(env_vars: dict) -> str:
52
+ if "SLACK_WEBHOOK" not in env_vars:
53
+ logger.debug("SLACK_WEBHOOK not found in env_vars.")
54
+ return ""
55
+ webhook = env_vars["SLACK_WEBHOOK"]
56
+ return (
57
+ """curl -X POST -H 'Content-type: application/json' --data '{"text":"Job started in '"$POD_NAME"'"}' """
58
+ + webhook
59
+ + " ; "
60
+ )
61
+
62
+
63
+ def get_env_vars(
64
+ local_env_vars: list[str],
65
+ secrets_env_vars: list[str],
66
+ load_dotenv: bool = False,
67
+ namespace: str = "informatics",
68
+ ) -> dict:
69
+ """Get environment variables from local environment and secrets."""
70
+
71
+ if load_dotenv:
72
+ from dotenv import load_dotenv
73
+
74
+ load_dotenv()
75
+
76
+ env_vars = {}
77
+ for var_name in local_env_vars:
78
+ value = os.getenv(var_name)
79
+ if value is not None:
80
+ env_vars[var_name] = value
81
+ else:
82
+ logger.warning(
83
+ f"Environment variable {var_name} not found in local environment"
84
+ )
85
+
86
+ for secret_name in secrets_env_vars:
87
+ v1 = client.CoreV1Api()
88
+ try:
89
+ secret = v1.read_namespaced_secret(name=secret_name, namespace=namespace)
90
+ for key, value in secret.data.items():
91
+ decoded_value = base64.b64decode(value).decode("utf-8")
92
+ if key in env_vars:
93
+ logger.warning(f"Key {key} already set in env_vars.")
94
+ env_vars[key] = decoded_value
95
+ except Exception as e:
96
+ logger.error(f"Error reading secret {secret_name}: {e}")
97
+ raise e
98
+
99
+ return env_vars
100
+
101
+
102
+ def export_env_vars(env_vars: dict) -> str:
103
+ """Export environment variables."""
104
+ cmd = ""
105
+ for key, value in env_vars.items():
106
+ cmd += f" export {key}='{value}' &&"
107
+ cmd = cmd.strip(" &&") + " ; "
108
+ return cmd
109
+
110
+
111
+ @app.command()
112
+ def launch(
113
+ email: str = typer.Option(..., help="User email"),
114
+ job_name: str = typer.Option(..., help="Name of the Kubernetes job"),
115
+ docker_image: str = typer.Option(
116
+ "nvcr.io/nvidia/cuda:12.0.0-devel-ubuntu22.04", help="Docker image"
117
+ ),
118
+ namespace: str = typer.Option("informatics", help="Kubernetes namespace"),
119
+ queue_name: str = typer.Option(KueueQueue.INFORMATICS, help="Kueue queue name"),
120
+ interactive: bool = typer.Option(False, help="Run in interactive mode"),
121
+ command: str = typer.Option(..., help="Command to run in the container"),
122
+ cpu_request: str = typer.Option("1", help="CPU request"),
123
+ ram_request: str = typer.Option("8Gi", help="RAM request"),
124
+ gpu_limit: int = typer.Option(1, help="GPU limit"),
125
+ gpu_product: str = typer.Option("NVIDIA-A100-80GB-PCIe", help="GPU product"),
126
+ secrets_env_vars: list[str] = typer.Option(
127
+ [], # Use empty list as default instead of None
128
+ help="List of secret environment variables to export to the container",
129
+ ),
130
+ local_env_vars: list[str] = typer.Option(
131
+ [], # Use empty list as default instead of None
132
+ help="List of local environment variables to export to the container",
133
+ ),
134
+ load_dotenv: bool = typer.Option(
135
+ True, help="Load environment variables from .env file"
136
+ ),
137
+ ):
138
+ """Launch a Kubernetes job with the specified configuration."""
139
+
140
+ is_completed = check_if_completed(job_name, namespace=namespace)
141
+
142
+ if is_completed is True:
143
+ logger.info(f"Job '{job_name}' is completed. Launching a new job.")
144
+
145
+ if interactive:
146
+ cmd = "while true; do sleep 60; done;"
147
+ else:
148
+ cmd = command
149
+ logger.info(f"Command: {cmd}")
150
+
151
+ # Get local environment variables
152
+ env_vars = get_env_vars(
153
+ local_env_vars=local_env_vars,
154
+ secrets_env_vars=secrets_env_vars,
155
+ load_dotenv=load_dotenv,
156
+ namespace=namespace,
157
+ )
158
+
159
+ logger.info(f"Creating job for: {cmd}")
160
+ job = KubernetesJob(
161
+ name=job_name,
162
+ cpu_request=cpu_request,
163
+ ram_request=ram_request,
164
+ image=docker_image,
165
+ gpu_type="nvidia.com/gpu",
166
+ gpu_limit=gpu_limit,
167
+ gpu_product=gpu_product,
168
+ backoff_limit=0,
169
+ command=["/bin/bash", "-c", "--"],
170
+ args=[export_env_vars(env_vars) + send_message_command(env_vars) + cmd],
171
+ user_email=email,
172
+ namespace=namespace,
173
+ kueue_queue_name=queue_name,
174
+ secret_env_vars=env_vars,
175
+ volume_mounts={
176
+ "nfs": {"mountPath": "/nfs", "server": "10.24.1.255", "path": "/"}
177
+ },
178
+ )
179
+
180
+ job_yaml = job.generate_yaml()
181
+ logger.info(job_yaml)
182
+
183
+ # Run the Job on the Kubernetes cluster
184
+ job.run()
185
+ else:
186
+ logger.info(f"Job '{job_name}' is still running.")
187
+
188
+
189
+ def cli():
190
+ """Entry point for the application"""
191
+ app()
192
+
193
+
194
+ if __name__ == "__main__":
195
+ cli()
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: kblaunch
3
+ Version: 0.1.0
4
+ Summary: A CLI tool for launching Kubernetes jobs in EIDF
5
+ Author-email: Gautier Dagan <gautier.dagan@ed.ac.uk>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: kubejobs>=0.4.5
10
+ Requires-Dist: kubernetes>=31.0.0
11
+ Requires-Dist: loguru>=0.7.3
12
+ Requires-Dist: python-dotenv>=1.0.1
13
+ Requires-Dist: typer>=0.15.1
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'test'
16
+ Requires-Dist: pytest-mock>=3.10.0; extra == 'test'
17
+ Requires-Dist: pytest>=7.0; extra == 'test'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # kblaunch
21
+
22
+ [![Test](https://github.com/gautierdag/kblaunch/actions/workflows/test.yaml/badge.svg)](https://github.com/gautierdag/kblaunch/actions/workflows/test.yaml)
23
+ ![Python Version](https://img.shields.io/badge/python-3.9+-blue)
24
+ ![Ruff](https://img.shields.io/badge/linter-ruff-blue)
25
+ [![PyPI Version](https://img.shields.io/pypi/v/kblaunch)](https://pypi.org/project/kblaunch/)
26
+
27
+ A CLI tool for launching Kubernetes jobs with environment variable and secret management.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install git+https://github.com/gautierdag/kblaunch.git
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Basic Usage
38
+
39
+ Launch a simple job:
40
+
41
+ ```bash
42
+ kblaunch \
43
+ --email your.email@ed.ac.uk \
44
+ --job-name myjob \
45
+ --command "python script.py"
46
+ ```
47
+
48
+ ### With Environment Variables
49
+
50
+ 1. From local environment:
51
+
52
+ ```bash
53
+ kblaunch \
54
+ --job-name myjob \
55
+ --command "python script.py" \
56
+ --local-env-vars PATH \
57
+ --local-env-vars PYTHONPATH
58
+ ```
59
+
60
+ 2. From Kubernetes secrets:
61
+
62
+ ```bash
63
+ kblaunch \
64
+ --job-name myjob \
65
+ --command "python script.py" \
66
+ --secrets-env-vars mysecret1 \
67
+ --secrets-env-vars mysecret2
68
+ ```
69
+
70
+ 3. From .env file:
71
+
72
+ ```bash
73
+ kblaunch \
74
+ --job-name myjob \
75
+ --command "python script.py" \
76
+ --load-dotenv
77
+ ```
78
+
79
+ ### GPU Jobs
80
+
81
+ Specify GPU requirements:
82
+
83
+ ```bash
84
+ kblaunch \
85
+ --job-name gpu-job \
86
+ --command "python train.py" \
87
+ --gpu-limit 2 \
88
+ --gpu-product "NVIDIA-A100-80GB-PCIe"
89
+ ```
90
+
91
+ ### Interactive Mode
92
+
93
+ Launch an interactive job:
94
+
95
+ ```bash
96
+ kblaunch \
97
+ --job-name interactive \
98
+ --interactive
99
+ ```
100
+
101
+ ## Options
102
+
103
+ - `--email`: User email [required]
104
+ - `--job-name`: Name of the Kubernetes job [required]
105
+ - `--docker-image`: Docker image (default: "nvcr.io/nvidia/cuda:12.0.0-devel-ubuntu22.04")
106
+ - `--namespace`: Kubernetes namespace (default: "informatics")
107
+ - `--queue-name`: Kueue queue name
108
+ - `--interactive`: Run in interactive mode (default: False)
109
+ - `--command`: Command to run in the container [required]
110
+ - `--cpu-request`: CPU request (default: "1")
111
+ - `--ram-request`: RAM request (default: "8Gi")
112
+ - `--gpu-limit`: GPU limit (default: 1)
113
+ - `--gpu-product`: GPU product (default: "NVIDIA-A100-80GB-PCIe")
114
+ - `--secrets-env-vars`: List of secret environment variables
115
+ - `--local-env-vars`: List of local environment variables
116
+ - `--load-dotenv`: Load environment variables from .env file (default: True)
117
+
118
+ ## Features
119
+
120
+ - Kubernetes job management
121
+ - Environment variable handling from multiple sources
122
+ - Kubernetes secrets integration
123
+ - GPU job support
124
+ - Interactive mode
125
+ - Automatic job cleanup
126
+ - Slack notifications (when configured)
@@ -0,0 +1,7 @@
1
+ kblaunch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ kblaunch/cli.py,sha256=pV_YD5YeQq_ifJhre9iT-BcvY5ns1ZzUJNiX8uCRfdQ,6452
3
+ kblaunch-0.1.0.dist-info/METADATA,sha256=bxOxNiD__aaFu_cNAktwN23Z3iKNeO7xdStmb1InTXw,3247
4
+ kblaunch-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ kblaunch-0.1.0.dist-info/entry_points.txt,sha256=XQxzjiVPA2ZKX8iIryH4_3VIrPHTREklu--gEnpWcB4,46
6
+ kblaunch-0.1.0.dist-info/licenses/LICENSE,sha256=Jw_Wqf5swgrgy2K0Pgat5DOoHDct-nz_AsIQ-yWI1HI,1070
7
+ kblaunch-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ kblaunch = kblaunch.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gautier Dagan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.