Skip to content

Commit e171648

Browse files
committed
task(RHOAIENG-33283): Add runtime env e2e
1 parent a07c632 commit e171648

File tree

4 files changed

+86
-16
lines changed

4 files changed

+86
-16
lines changed

src/codeflare_sdk/ray/rayjobs/rayjob.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import logging
2020
import warnings
21-
from typing import Dict, Any, Optional, Tuple
21+
from typing import Dict, Any, Optional, Tuple, Union
2222

2323
from ray.runtime_env import RuntimeEnv
2424
from codeflare_sdk.common.kueue.kueue import get_default_kueue_name
@@ -63,7 +63,7 @@ def __init__(
6363
cluster_name: Optional[str] = None,
6464
cluster_config: Optional[ManagedClusterConfig] = None,
6565
namespace: Optional[str] = None,
66-
runtime_env: Optional[RuntimeEnv] = None,
66+
runtime_env: Optional[Union[RuntimeEnv, Dict[str, Any]]] = None,
6767
ttl_seconds_after_finished: int = 0,
6868
active_deadline_seconds: Optional[int] = None,
6969
local_queue: Optional[str] = None,
@@ -77,7 +77,10 @@ def __init__(
7777
cluster_name: The name of an existing Ray cluster (optional if cluster_config provided)
7878
cluster_config: Configuration for creating a new cluster (optional if cluster_name provided)
7979
namespace: The Kubernetes namespace (auto-detected if not specified)
80-
runtime_env: Ray runtime environment configuration as RuntimeEnv object (optional)
80+
runtime_env: Ray runtime environment configuration. Can be:
81+
- RuntimeEnv object from ray.runtime_env
82+
- Dict with keys like 'working_dir', 'pip', 'env_vars', etc.
83+
Example: {"working_dir": "./my-scripts", "pip": ["requests"]}
8184
ttl_seconds_after_finished: Seconds to wait before cleanup after job finishes (default: 0)
8285
active_deadline_seconds: Maximum time the job can run before being terminated (optional)
8386
local_queue: The Kueue LocalQueue to submit the job to (optional)
@@ -109,7 +112,13 @@ def __init__(
109112

110113
self.name = job_name
111114
self.entrypoint = entrypoint
112-
self.runtime_env = runtime_env
115+
116+
# Convert dict to RuntimeEnv if needed for user convenience
117+
if isinstance(runtime_env, dict):
118+
self.runtime_env = RuntimeEnv(**runtime_env)
119+
else:
120+
self.runtime_env = runtime_env
121+
113122
self.ttl_seconds_after_finished = ttl_seconds_after_finished
114123
self.active_deadline_seconds = active_deadline_seconds
115124
self.local_queue = local_queue
@@ -232,6 +241,9 @@ def _build_rayjob_cr(self) -> Dict[str, Any]:
232241
},
233242
}
234243

244+
# Extract files once and use for both runtime_env and submitter pod
245+
files = extract_all_local_files(self)
246+
235247
labels = {}
236248
# If cluster_config is provided, use the local_queue from the cluster_config
237249
if self._cluster_config is not None:
@@ -262,9 +274,6 @@ def _build_rayjob_cr(self) -> Dict[str, Any]:
262274
if self.active_deadline_seconds:
263275
rayjob_cr["spec"]["activeDeadlineSeconds"] = self.active_deadline_seconds
264276

265-
# Extract files once and use for both runtime_env and submitter pod
266-
files = extract_all_local_files(self)
267-
268277
# Add runtime environment (can be inferred even if not explicitly specified)
269278
processed_runtime_env = process_runtime_env(self, files)
270279
if processed_runtime_env:
@@ -326,8 +335,19 @@ def _build_submitter_pod_template(
326335

327336
# Build ConfigMap items for each file
328337
config_map_items = []
338+
entrypoint_path = files.get(
339+
"__entrypoint_path__"
340+
) # Metadata for single file case
341+
329342
for file_name in files.keys():
330-
config_map_items.append({"key": file_name, "path": file_name})
343+
if file_name == "__entrypoint_path__":
344+
continue # Skip metadata key
345+
346+
# For single file case, use the preserved path structure
347+
if entrypoint_path:
348+
config_map_items.append({"key": file_name, "path": entrypoint_path})
349+
else:
350+
config_map_items.append({"key": file_name, "path": file_name})
331351

332352
# Check if we need to unzip working_dir
333353
has_working_dir_zip = "working_dir.zip" in files

src/codeflare_sdk/ray/rayjobs/runtime_env.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
logger = logging.getLogger(__name__)
2323

2424
# Regex pattern for finding Python files in entrypoint commands
25-
PYTHON_FILE_PATTERN = r"(?:python\s+)?([./\w/]+\.py)"
25+
# Matches paths like: test.py, ./test.py, dir/test.py, my-dir/test.py
26+
PYTHON_FILE_PATTERN = r"(?:python\s+)?([./\w/-]+\.py)"
2627

2728
# Path where working_dir will be unzipped on submitter pod
2829
UNZIP_PATH = "/tmp/rayjob-working-dir"
@@ -126,11 +127,15 @@ def _extract_single_entrypoint_file(job: RayJob) -> Optional[Dict[str, str]]:
126127
"""
127128
Extract single Python file from entrypoint if no working_dir specified.
128129
130+
Returns a dict with metadata about the file path structure so we can
131+
preserve it when mounting via ConfigMap.
132+
129133
Args:
130134
job: RayJob instance
131135
132136
Returns:
133-
Dict of {filename: content} or None
137+
Dict with special format: {"__entrypoint_path__": path, "filename": content}
138+
This allows us to preserve directory structure when mounting
134139
"""
135140
if not job.entrypoint:
136141
return None
@@ -145,9 +150,15 @@ def _extract_single_entrypoint_file(job: RayJob) -> Optional[Dict[str, str]]:
145150
with open(file_path, "r") as f:
146151
content = f.read()
147152

153+
# Use basename as key (ConfigMap keys can't have slashes)
154+
# But store the full path for later use in ConfigMap item.path
148155
filename = os.path.basename(file_path)
156+
relative_path = file_path.lstrip("./")
157+
149158
logger.info(f"Extracted single entrypoint file: {file_path}")
150-
return {filename: content}
159+
160+
# Return special format with metadata
161+
return {"__entrypoint_path__": relative_path, filename: content}
151162

152163
except (IOError, OSError) as e:
153164
logger.warning(f"Could not read entrypoint file {file_path}: {e}")
@@ -348,10 +359,13 @@ def create_file_configmap(
348359
# Use a basic config builder for ConfigMap creation
349360
config_builder = ManagedClusterConfig()
350361

362+
# Filter out metadata keys (like __entrypoint_path__) from ConfigMap data
363+
configmap_files = {k: v for k, v in files.items() if not k.startswith("__")}
364+
351365
# Validate and build ConfigMap spec
352-
config_builder.validate_configmap_size(files)
366+
config_builder.validate_configmap_size(configmap_files)
353367
configmap_spec = config_builder.build_file_configmap_spec(
354-
job_name=job.name, namespace=job.namespace, files=files
368+
job_name=job.name, namespace=job.namespace, files=configmap_files
355369
)
356370

357371
# Create ConfigMap with owner reference

src/codeflare_sdk/ray/rayjobs/test/test_rayjob.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,38 @@ def test_rayjob_with_runtime_env(auto_mock_setup):
491491
assert rayjob_cr["spec"]["runtimeEnvYAML"] == "pip:\n- numpy\n- pandas\n"
492492

493493

494+
def test_rayjob_with_runtime_env_dict(auto_mock_setup):
495+
"""
496+
Test RayJob with runtime environment as dict (user convenience).
497+
Users can pass a dict instead of having to import RuntimeEnv.
498+
"""
499+
# User can pass dict instead of RuntimeEnv object
500+
runtime_env_dict = {
501+
"pip": ["numpy", "pandas"],
502+
"env_vars": {"TEST_VAR": "test_value"},
503+
}
504+
505+
rayjob = RayJob(
506+
job_name="test-job",
507+
entrypoint="python test.py",
508+
cluster_name="test-cluster",
509+
runtime_env=runtime_env_dict,
510+
namespace="test-namespace",
511+
)
512+
513+
# Should be converted to RuntimeEnv internally
514+
assert isinstance(rayjob.runtime_env, RuntimeEnv)
515+
assert rayjob.runtime_env["env_vars"] == {"TEST_VAR": "test_value"}
516+
517+
# Verify it generates proper YAML output
518+
rayjob_cr = rayjob._build_rayjob_cr()
519+
assert "runtimeEnvYAML" in rayjob_cr["spec"]
520+
runtime_yaml = rayjob_cr["spec"]["runtimeEnvYAML"]
521+
assert "pip:" in runtime_yaml or "pip_packages:" in runtime_yaml
522+
assert "env_vars:" in runtime_yaml
523+
assert "TEST_VAR" in runtime_yaml
524+
525+
494526
def test_rayjob_with_active_deadline_and_ttl(auto_mock_setup):
495527
"""
496528
Test RayJob with both active deadline and TTL settings.

src/codeflare_sdk/ray/rayjobs/test/test_runtime_env.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,13 @@ def track_create_cm(*args, **kwargs):
431431
mock_create_cm.assert_called_once()
432432

433433
# Verify create_file_configmap was called with: (job, files, rayjob_result)
434-
mock_create_cm.assert_called_with(
435-
rayjob, {"test.py": "print('test')"}, submit_result
436-
)
434+
# Files dict includes metadata key __entrypoint_path__ for single file case
435+
call_args = mock_create_cm.call_args[0]
436+
assert call_args[0] == rayjob
437+
assert call_args[2] == submit_result
438+
# Check that the actual file content is present
439+
assert "test.py" in call_args[1]
440+
assert call_args[1]["test.py"] == "print('test')"
437441

438442

439443
def test_rayjob_submit_with_files_new_cluster(auto_mock_setup, tmp_path):

0 commit comments

Comments
 (0)