Skip to content

Commit 372345e

Browse files
authored
Adds RayCluster.apply() (#778)
- Adds RayCluster.apply() implementation - Adds e2e tests for apply - Adds unit tests for apply
1 parent b5c13dc commit 372345e

File tree

10 files changed

+687
-38
lines changed

10 files changed

+687
-38
lines changed

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pytest -v src/codeflare_sdk
7676

7777
### Local e2e Testing
7878

79-
- Please follow the [e2e documentation](https://github.com/project-codeflare/codeflare-sdk/blob/main/docs/e2e.md)
79+
- Please follow the [e2e documentation](https://github.com/project-codeflare/codeflare-sdk/blob/main/docs/sphinx/user-docs/e2e.rst)
8080

8181
#### Code Coverage
8282

docs/sphinx/user-docs/ray-cluster-interaction.rst

+6
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ cluster.up()
6666

6767
| The ``cluster.up()`` function creates a Ray Cluster in the given namespace.
6868
69+
cluster.apply()
70+
------------
71+
72+
| The ``cluster.apply()`` function applies a Ray Cluster in the given namespace. If the cluster already exists, it is updated.
73+
| If it does not exist it is created.
74+
6975
cluster.down()
7076
--------------
7177

src/codeflare_sdk/common/kueue/test_kueue.py

+125-8
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
from ..utils.unit_test_support import (
15-
apply_template,
1615
get_local_queue,
17-
createClusterConfig,
16+
create_cluster_config,
1817
get_template_variables,
18+
apply_template,
1919
)
2020
from unittest.mock import patch
2121
from codeflare_sdk.ray.cluster.cluster import Cluster, ClusterConfiguration
2222
import yaml
2323
import os
2424
import filecmp
2525
from pathlib import Path
26-
from .kueue import list_local_queues
26+
from .kueue import list_local_queues, local_queue_exists, add_queue_label
2727

2828
parent = Path(__file__).resolve().parents[4] # project directory
2929
aw_dir = os.path.expanduser("~/.codeflare/resources/")
@@ -51,7 +51,7 @@ def test_cluster_creation_no_aw_local_queue(mocker):
5151
"kubernetes.client.CustomObjectsApi.list_namespaced_custom_object",
5252
return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"),
5353
)
54-
config = createClusterConfig()
54+
config = create_cluster_config()
5555
config.name = "unit-test-cluster-kueue"
5656
config.write_to_file = True
5757
config.local_queue = "local-queue-default"
@@ -67,7 +67,7 @@ def test_cluster_creation_no_aw_local_queue(mocker):
6767
assert cluster_kueue == expected_rc
6868

6969
# With resources loaded in memory, no Local Queue specified.
70-
config = createClusterConfig()
70+
config = create_cluster_config()
7171
config.name = "unit-test-cluster-kueue"
7272
config.write_to_file = False
7373
cluster = Cluster(config)
@@ -84,7 +84,7 @@ def test_aw_creation_local_queue(mocker):
8484
"kubernetes.client.CustomObjectsApi.list_namespaced_custom_object",
8585
return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"),
8686
)
87-
config = createClusterConfig()
87+
config = create_cluster_config()
8888
config.name = "unit-test-aw-kueue"
8989
config.appwrapper = True
9090
config.write_to_file = True
@@ -101,7 +101,7 @@ def test_aw_creation_local_queue(mocker):
101101
assert aw_kueue == expected_rc
102102

103103
# With resources loaded in memory, no Local Queue specified.
104-
config = createClusterConfig()
104+
config = create_cluster_config()
105105
config.name = "unit-test-aw-kueue"
106106
config.appwrapper = True
107107
config.write_to_file = False
@@ -120,7 +120,7 @@ def test_get_local_queue_exists_fail(mocker):
120120
"kubernetes.client.CustomObjectsApi.list_namespaced_custom_object",
121121
return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"),
122122
)
123-
config = createClusterConfig()
123+
config = create_cluster_config()
124124
config.name = "unit-test-aw-kueue"
125125
config.appwrapper = True
126126
config.write_to_file = True
@@ -175,6 +175,123 @@ def test_list_local_queues(mocker):
175175
assert lqs == []
176176

177177

178+
def test_local_queue_exists_found(mocker):
179+
# Mock Kubernetes client and list_namespaced_custom_object method
180+
mocker.patch("kubernetes.config.load_kube_config", return_value="ignore")
181+
mock_api_instance = mocker.Mock()
182+
mocker.patch("kubernetes.client.CustomObjectsApi", return_value=mock_api_instance)
183+
mocker.patch("codeflare_sdk.ray.cluster.cluster.config_check")
184+
185+
# Mock return value for list_namespaced_custom_object
186+
mock_api_instance.list_namespaced_custom_object.return_value = {
187+
"items": [
188+
{"metadata": {"name": "existing-queue"}},
189+
{"metadata": {"name": "another-queue"}},
190+
]
191+
}
192+
193+
# Call the function
194+
namespace = "test-namespace"
195+
local_queue_name = "existing-queue"
196+
result = local_queue_exists(namespace, local_queue_name)
197+
198+
# Assertions
199+
assert result is True
200+
mock_api_instance.list_namespaced_custom_object.assert_called_once_with(
201+
group="kueue.x-k8s.io",
202+
version="v1beta1",
203+
namespace=namespace,
204+
plural="localqueues",
205+
)
206+
207+
208+
def test_local_queue_exists_not_found(mocker):
209+
# Mock Kubernetes client and list_namespaced_custom_object method
210+
mocker.patch("kubernetes.config.load_kube_config", return_value="ignore")
211+
mock_api_instance = mocker.Mock()
212+
mocker.patch("kubernetes.client.CustomObjectsApi", return_value=mock_api_instance)
213+
mocker.patch("codeflare_sdk.ray.cluster.cluster.config_check")
214+
215+
# Mock return value for list_namespaced_custom_object
216+
mock_api_instance.list_namespaced_custom_object.return_value = {
217+
"items": [
218+
{"metadata": {"name": "another-queue"}},
219+
{"metadata": {"name": "different-queue"}},
220+
]
221+
}
222+
223+
# Call the function
224+
namespace = "test-namespace"
225+
local_queue_name = "non-existent-queue"
226+
result = local_queue_exists(namespace, local_queue_name)
227+
228+
# Assertions
229+
assert result is False
230+
mock_api_instance.list_namespaced_custom_object.assert_called_once_with(
231+
group="kueue.x-k8s.io",
232+
version="v1beta1",
233+
namespace=namespace,
234+
plural="localqueues",
235+
)
236+
237+
238+
import pytest
239+
from unittest import mock # If you're also using mocker from pytest-mock
240+
241+
242+
def test_add_queue_label_with_valid_local_queue(mocker):
243+
# Mock the kubernetes.client.CustomObjectsApi and its response
244+
mock_api_instance = mocker.patch("kubernetes.client.CustomObjectsApi")
245+
mock_api_instance.return_value.list_namespaced_custom_object.return_value = {
246+
"items": [
247+
{"metadata": {"name": "valid-queue"}},
248+
]
249+
}
250+
251+
# Mock other dependencies
252+
mocker.patch("codeflare_sdk.common.kueue.local_queue_exists", return_value=True)
253+
mocker.patch(
254+
"codeflare_sdk.common.kueue.get_default_kueue_name",
255+
return_value="default-queue",
256+
)
257+
258+
# Define input item and parameters
259+
item = {"metadata": {}}
260+
namespace = "test-namespace"
261+
local_queue = "valid-queue"
262+
263+
# Call the function
264+
add_queue_label(item, namespace, local_queue)
265+
266+
# Assert that the label is added to the item
267+
assert item["metadata"]["labels"] == {"kueue.x-k8s.io/queue-name": "valid-queue"}
268+
269+
270+
def test_add_queue_label_with_invalid_local_queue(mocker):
271+
# Mock the kubernetes.client.CustomObjectsApi and its response
272+
mock_api_instance = mocker.patch("kubernetes.client.CustomObjectsApi")
273+
mock_api_instance.return_value.list_namespaced_custom_object.return_value = {
274+
"items": [
275+
{"metadata": {"name": "valid-queue"}},
276+
]
277+
}
278+
279+
# Mock the local_queue_exists function to return False
280+
mocker.patch("codeflare_sdk.common.kueue.local_queue_exists", return_value=False)
281+
282+
# Define input item and parameters
283+
item = {"metadata": {}}
284+
namespace = "test-namespace"
285+
local_queue = "invalid-queue"
286+
287+
# Call the function and expect a ValueError
288+
with pytest.raises(
289+
ValueError,
290+
match="local_queue provided does not exist or is not in this namespace",
291+
):
292+
add_queue_label(item, namespace, local_queue)
293+
294+
178295
# Make sure to always keep this function last
179296
def test_cleanup():
180297
os.remove(f"{aw_dir}unit-test-cluster-kueue.yaml")

src/codeflare_sdk/common/utils/unit_test_support.py

+55-11
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,34 @@
2929
aw_dir = os.path.expanduser("~/.codeflare/resources/")
3030

3131

32-
def createClusterConfig():
32+
def create_cluster_config(num_workers=2, write_to_file=False):
3333
config = ClusterConfiguration(
3434
name="unit-test-cluster",
3535
namespace="ns",
36-
num_workers=2,
36+
num_workers=num_workers,
3737
worker_cpu_requests=3,
3838
worker_cpu_limits=4,
3939
worker_memory_requests=5,
4040
worker_memory_limits=6,
4141
appwrapper=True,
42-
write_to_file=False,
42+
write_to_file=write_to_file,
4343
)
4444
return config
4545

4646

47-
def createClusterWithConfig(mocker):
48-
mocker.patch("kubernetes.config.load_kube_config", return_value="ignore")
49-
mocker.patch(
50-
"kubernetes.client.CustomObjectsApi.get_cluster_custom_object",
51-
return_value={"spec": {"domain": "apps.cluster.awsroute.org"}},
52-
)
53-
cluster = Cluster(createClusterConfig())
47+
def create_cluster(mocker, num_workers=2, write_to_file=False):
48+
cluster = Cluster(create_cluster_config(num_workers, write_to_file))
5449
return cluster
5550

5651

57-
def createClusterWrongType():
52+
def patch_cluster_with_dynamic_client(mocker, cluster, dynamic_client=None):
53+
mocker.patch.object(cluster, "get_dynamic_client", return_value=dynamic_client)
54+
mocker.patch.object(cluster, "down", return_value=None)
55+
mocker.patch.object(cluster, "config_check", return_value=None)
56+
# mocker.patch.object(cluster, "_throw_for_no_raycluster", return_value=None)
57+
58+
59+
def create_cluster_wrong_type():
5860
config = ClusterConfiguration(
5961
name="unit-test-cluster",
6062
namespace="ns",
@@ -412,6 +414,48 @@ def mocked_ingress(port, cluster_name="unit-test-cluster", annotations: dict = N
412414
return mock_ingress
413415

414416

417+
# Global dictionary to maintain state in the mock
418+
cluster_state = {}
419+
420+
421+
# The mock side_effect function for server_side_apply
422+
def mock_server_side_apply(resource, body=None, name=None, namespace=None, **kwargs):
423+
# Simulate the behavior of server_side_apply:
424+
# Update a mock state that represents the cluster's current configuration.
425+
# Stores the state in a global dictionary for simplicity.
426+
427+
global cluster_state
428+
429+
if not resource or not body or not name or not namespace:
430+
raise ValueError("Missing required parameters for server_side_apply")
431+
432+
# Extract worker count from the body if it exists
433+
try:
434+
worker_count = (
435+
body["spec"]["workerGroupSpecs"][0]["replicas"]
436+
if "spec" in body and "workerGroupSpecs" in body["spec"]
437+
else None
438+
)
439+
except KeyError:
440+
worker_count = None
441+
442+
# Apply changes to the cluster_state mock
443+
cluster_state[name] = {
444+
"namespace": namespace,
445+
"worker_count": worker_count,
446+
"body": body,
447+
}
448+
449+
# Return a response that mimics the behavior of a successful apply
450+
return {
451+
"status": "success",
452+
"applied": True,
453+
"name": name,
454+
"namespace": namespace,
455+
"worker_count": worker_count,
456+
}
457+
458+
415459
@patch.dict("os.environ", {"NB_PREFIX": "test-prefix"})
416460
def create_cluster_all_config_params(mocker, cluster_name, is_appwrapper) -> Cluster:
417461
mocker.patch(

src/codeflare_sdk/common/widgets/test_widgets.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import codeflare_sdk.common.widgets.widgets as cf_widgets
1616
import pandas as pd
1717
from unittest.mock import MagicMock, patch
18-
from ..utils.unit_test_support import get_local_queue, createClusterConfig
18+
from ..utils.unit_test_support import get_local_queue, create_cluster_config
1919
from codeflare_sdk.ray.cluster.cluster import Cluster
2020
from codeflare_sdk.ray.cluster.status import (
2121
RayCluster,
@@ -38,7 +38,7 @@ def test_cluster_up_down_buttons(mocker):
3838
"kubernetes.client.CustomObjectsApi.list_namespaced_custom_object",
3939
return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"),
4040
)
41-
cluster = Cluster(createClusterConfig())
41+
cluster = Cluster(create_cluster_config())
4242

4343
with patch("ipywidgets.Button") as MockButton, patch(
4444
"ipywidgets.Checkbox"

0 commit comments

Comments
 (0)