Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
OpenDAS
dynamo
Commits
14e1d446
Unverified
Commit
14e1d446
authored
May 22, 2025
by
julienmancuso
Committed by
GitHub
May 22, 2025
Browse files
fix: add blocking mode for k8s connector in planner (#1176)
parent
3d4fe574
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
205 additions
and
5 deletions
+205
-5
components/planner/src/dynamo/planner/kube.py
components/planner/src/dynamo/planner/kube.py
+37
-0
components/planner/src/dynamo/planner/kubernetes_connector.py
...onents/planner/src/dynamo/planner/kubernetes_connector.py
+5
-1
components/planner/test/kube.py
components/planner/test/kube.py
+154
-0
components/planner/test/kubernetes_connector.py
components/planner/test/kubernetes_connector.py
+9
-4
No files found.
components/planner/src/dynamo/planner/kube.py
View file @
14e1d446
...
@@ -13,6 +13,7 @@
...
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# See the License for the specific language governing permissions and
# limitations under the License.
# limitations under the License.
import
asyncio
from
typing
import
Optional
from
typing
import
Optional
from
kubernetes
import
client
,
config
from
kubernetes
import
client
,
config
...
@@ -125,3 +126,39 @@ class KubernetesAPI:
...
@@ -125,3 +126,39 @@ class KubernetesAPI:
name
=
graph_deployment_name
,
name
=
graph_deployment_name
,
body
=
patch
,
body
=
patch
,
)
)
async
def
wait_for_graph_deployment_ready
(
self
,
graph_deployment_name
:
str
,
max_attempts
:
int
=
60
,
# default: 10 minutes total
delay_seconds
:
int
=
10
,
# default: check every 10 seconds
)
->
None
:
"""Wait for a graph deployment to be ready"""
for
attempt
in
range
(
max_attempts
):
await
asyncio
.
sleep
(
delay_seconds
)
graph_deployment
=
await
self
.
get_graph_deployment
(
graph_deployment_name
,
self
.
current_namespace
)
if
not
graph_deployment
:
raise
ValueError
(
f
"Graph deployment
{
graph_deployment_name
}
not found"
)
conditions
=
graph_deployment
.
get
(
"status"
,
{}).
get
(
"conditions"
,
[])
ready_condition
=
next
(
(
c
for
c
in
conditions
if
c
.
get
(
"type"
)
==
"Ready"
),
None
)
if
ready_condition
and
ready_condition
.
get
(
"status"
)
==
"True"
:
return
# Deployment is ready
print
(
f
"[Attempt
{
attempt
+
1
}
/
{
max_attempts
}
] "
f
"(status:
{
ready_condition
.
get
(
'status'
)
if
ready_condition
else
'N/A'
}
, "
f
"message:
{
ready_condition
.
get
(
'message'
)
if
ready_condition
else
'no condition found'
}
)"
)
# Raise after all attempts exhausted (without additional delay)
raise
TimeoutError
(
f
"Graph deployment '
{
graph_deployment_name
}
' "
f
"is not ready after
{
max_attempts
*
delay_seconds
}
seconds"
)
components/planner/src/dynamo/planner/kubernetes_connector.py
View file @
14e1d446
...
@@ -22,7 +22,7 @@ class KubernetesConnector(PlannerConnector):
...
@@ -22,7 +22,7 @@ class KubernetesConnector(PlannerConnector):
self
.
kube_api
=
KubernetesAPI
()
self
.
kube_api
=
KubernetesAPI
()
self
.
namespace
=
namespace
self
.
namespace
=
namespace
async
def
add_component
(
self
,
component_name
:
str
):
async
def
add_component
(
self
,
component_name
:
str
,
blocking
:
bool
=
True
):
"""Add a component by increasing its replica count by 1"""
"""Add a component by increasing its replica count by 1"""
deployment
=
await
self
.
kube_api
.
get_graph_deployment
(
deployment
=
await
self
.
kube_api
.
get_graph_deployment
(
component_name
,
self
.
namespace
component_name
,
self
.
namespace
...
@@ -38,6 +38,10 @@ class KubernetesConnector(PlannerConnector):
...
@@ -38,6 +38,10 @@ class KubernetesConnector(PlannerConnector):
component_name
,
component_name
,
current_replicas
+
1
,
current_replicas
+
1
,
)
)
if
blocking
:
await
self
.
kube_api
.
wait_for_graph_deployment_ready
(
self
.
_get_graph_deployment_name
(
deployment
)
)
async
def
remove_component
(
self
,
component_name
:
str
):
async
def
remove_component
(
self
,
component_name
:
str
):
"""Remove a component by decreasing its replica count by 1"""
"""Remove a component by decreasing its replica count by 1"""
...
...
components/planner/test/kube.py
0 → 100644
View file @
14e1d446
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from
typing
import
Any
,
Dict
from
unittest.mock
import
AsyncMock
,
MagicMock
,
patch
import
pytest
from
dynamo.planner.kube
import
KubernetesAPI
@
pytest
.
fixture
def
mock_config
():
with
patch
(
"dynamo.planner.kube.config"
)
as
mock
:
mock
.
load_incluster_config
=
MagicMock
()
yield
mock
@
pytest
.
fixture
def
mock_custom_api
():
with
patch
(
"dynamo.planner.kube.client.CustomObjectsApi"
)
as
mock
:
yield
mock
.
return_value
@
pytest
.
fixture
def
k8s_api
(
mock_custom_api
,
mock_config
):
return
KubernetesAPI
()
@
pytest
.
mark
.
asyncio
async
def
test_wait_for_graph_deployment_ready_success
(
k8s_api
,
mock_custom_api
):
# Mock the get_graph_deployment response
mock_deployment
:
Dict
[
str
,
Any
]
=
{
"status"
:
{
"conditions"
:
[
{
"type"
:
"Ready"
,
"status"
:
"True"
,
"message"
:
"Deployment is ready"
}
]
}
}
k8s_api
.
get_graph_deployment
=
AsyncMock
(
return_value
=
mock_deployment
)
# Test with minimal attempts and delay for faster testing
await
k8s_api
.
wait_for_graph_deployment_ready
(
"test-deployment"
,
max_attempts
=
2
,
delay_seconds
=
0.1
)
# Verify get_graph_deployment was called
k8s_api
.
get_graph_deployment
.
assert_called_once_with
(
"test-deployment"
,
k8s_api
.
current_namespace
)
@
pytest
.
mark
.
asyncio
async
def
test_wait_for_graph_deployment_ready_timeout
(
k8s_api
,
mock_custom_api
):
# Mock the get_graph_deployment response with not ready status
mock_deployment
:
Dict
[
str
,
Any
]
=
{
"status"
:
{
"conditions"
:
[
{
"type"
:
"Ready"
,
"status"
:
"False"
,
"message"
:
"Deployment is not ready"
,
}
]
}
}
k8s_api
.
get_graph_deployment
=
AsyncMock
(
return_value
=
mock_deployment
)
# Test with minimal attempts and delay for faster testing
with
pytest
.
raises
(
TimeoutError
)
as
exc_info
:
await
k8s_api
.
wait_for_graph_deployment_ready
(
"test-deployment"
,
max_attempts
=
2
,
delay_seconds
=
0.1
)
assert
"is not ready after"
in
str
(
exc_info
.
value
)
assert
k8s_api
.
get_graph_deployment
.
call_count
==
2
@
pytest
.
mark
.
asyncio
async
def
test_wait_for_graph_deployment_not_found
(
k8s_api
,
mock_custom_api
):
# Mock the get_graph_deployment response to return None
k8s_api
.
get_graph_deployment
=
AsyncMock
(
return_value
=
None
)
# Test with minimal attempts and delay for faster testing
with
pytest
.
raises
(
ValueError
)
as
exc_info
:
await
k8s_api
.
wait_for_graph_deployment_ready
(
"test-deployment"
,
max_attempts
=
2
,
delay_seconds
=
0.1
)
assert
"not found"
in
str
(
exc_info
.
value
)
assert
k8s_api
.
get_graph_deployment
.
call_count
==
1
@
pytest
.
mark
.
asyncio
async
def
test_wait_for_graph_deployment_no_conditions
(
k8s_api
,
mock_custom_api
):
# Mock the get_graph_deployment response with no conditions
mock_deployment
:
Dict
[
str
,
Any
]
=
{
"status"
:
{}}
k8s_api
.
get_graph_deployment
=
AsyncMock
(
return_value
=
mock_deployment
)
# Test with minimal attempts and delay for faster testing
with
pytest
.
raises
(
TimeoutError
)
as
exc_info
:
await
k8s_api
.
wait_for_graph_deployment_ready
(
"test-deployment"
,
max_attempts
=
2
,
delay_seconds
=
0.1
)
assert
"is not ready after"
in
str
(
exc_info
.
value
)
assert
k8s_api
.
get_graph_deployment
.
call_count
==
2
@
pytest
.
mark
.
asyncio
async
def
test_wait_for_graph_deployment_ready_on_second_attempt
(
k8s_api
,
mock_custom_api
):
# Mock the get_graph_deployment response to return not ready first, then ready
mock_deployment_not_ready
:
Dict
[
str
,
Any
]
=
{
"status"
:
{
"conditions"
:
[
{
"type"
:
"Ready"
,
"status"
:
"False"
,
"message"
:
"Deployment is not ready"
,
}
]
}
}
mock_deployment_ready
:
Dict
[
str
,
Any
]
=
{
"status"
:
{
"conditions"
:
[
{
"type"
:
"Ready"
,
"status"
:
"True"
,
"message"
:
"Deployment is ready"
}
]
}
}
k8s_api
.
get_graph_deployment
=
AsyncMock
(
side_effect
=
[
mock_deployment_not_ready
,
mock_deployment_ready
]
)
# Test with minimal attempts and delay for faster testing
await
k8s_api
.
wait_for_graph_deployment_ready
(
"test-deployment"
,
max_attempts
=
2
,
delay_seconds
=
0.1
)
assert
k8s_api
.
get_graph_deployment
.
call_count
==
2
components/planner/test/kubernetes_connector.py
View file @
14e1d446
...
@@ -25,6 +25,7 @@ def mock_kube_api():
...
@@ -25,6 +25,7 @@ def mock_kube_api():
mock_api
=
Mock
()
mock_api
=
Mock
()
mock_api
.
get_graph_deployment
=
AsyncMock
()
mock_api
.
get_graph_deployment
=
AsyncMock
()
mock_api
.
update_graph_replicas
=
AsyncMock
()
mock_api
.
update_graph_replicas
=
AsyncMock
()
mock_api
.
wait_for_graph_deployment_ready
=
AsyncMock
()
return
mock_api
return
mock_api
...
@@ -41,9 +42,7 @@ def kubernetes_connector(mock_kube_api_class, monkeypatch):
...
@@ -41,9 +42,7 @@ def kubernetes_connector(mock_kube_api_class, monkeypatch):
monkeypatch
.
setattr
(
monkeypatch
.
setattr
(
"dynamo.planner.kubernetes_connector.KubernetesAPI"
,
mock_kube_api_class
"dynamo.planner.kubernetes_connector.KubernetesAPI"
,
mock_kube_api_class
)
)
connector
=
KubernetesConnector
()
connector
=
KubernetesConnector
(
"default"
)
# Set the namespace attribute that's being accessed in the error
connector
.
namespace
=
"default"
return
connector
return
connector
...
@@ -56,15 +55,20 @@ async def test_add_component_increases_replicas(kubernetes_connector, mock_kube_
...
@@ -56,15 +55,20 @@ async def test_add_component_increases_replicas(kubernetes_connector, mock_kube_
"spec"
:
{
"services"
:
{
"test-component"
:
{
"replicas"
:
1
}}},
"spec"
:
{
"services"
:
{
"test-component"
:
{
"replicas"
:
1
}}},
}
}
mock_kube_api
.
get_graph_deployment
.
return_value
=
mock_deployment
mock_kube_api
.
get_graph_deployment
.
return_value
=
mock_deployment
mock_kube_api
.
update_graph_replicas
.
return_value
=
None
mock_kube_api
.
wait_for_graph_deployment_ready
.
return_value
=
None
# Act
# Act
await
kubernetes_connector
.
add_component
(
component_name
)
await
kubernetes_connector
.
add_component
(
component_name
)
# Assert
# Assert
mock_kube_api
.
get_graph_deployment
.
assert_called_once_with
(
component_name
)
mock_kube_api
.
get_graph_deployment
.
assert_called_once_with
(
component_name
,
kubernetes_connector
.
namespace
)
mock_kube_api
.
update_graph_replicas
.
assert_called_once_with
(
mock_kube_api
.
update_graph_replicas
.
assert_called_once_with
(
"test-graph"
,
component_name
,
2
"test-graph"
,
component_name
,
2
)
)
mock_kube_api
.
wait_for_graph_deployment_ready
.
assert_called_once_with
(
"test-graph"
)
@
pytest
.
mark
.
asyncio
@
pytest
.
mark
.
asyncio
...
@@ -86,6 +90,7 @@ async def test_add_component_with_no_replicas_specified(
...
@@ -86,6 +90,7 @@ async def test_add_component_with_no_replicas_specified(
mock_kube_api
.
update_graph_replicas
.
assert_called_once_with
(
mock_kube_api
.
update_graph_replicas
.
assert_called_once_with
(
"test-graph"
,
component_name
,
2
"test-graph"
,
component_name
,
2
)
)
mock_kube_api
.
wait_for_graph_deployment_ready
.
assert_called_once_with
(
"test-graph"
)
@
pytest
.
mark
.
asyncio
@
pytest
.
mark
.
asyncio
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment