Skip to content

Commit a2ac674

Browse files
authored
close #307, close #265; check if any other installed element requires the given element (#348)
1 parent 8f567f6 commit a2ac674

3 files changed

Lines changed: 314 additions & 3 deletions

File tree

.github/workflows/build.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,20 @@ jobs:
7575
run: |
7676
VERSION=$(cat artifacts/version.txt)
7777
exordos bootstrap -i $VERSION -f -m core --admin-password $CORE_PASSWORD --cidr 10.20.0.0/22
78-
- name: sleep
79-
run: |
80-
sleep 30
8178
- name: check exordos core
8279
timeout-minutes: 10
8380
run: |
8481
# set -eux
8582
83+
for i in {1..10}; do
84+
if exordos ready_api; then
85+
echo "Exordos Core API is ready"
86+
break
87+
fi
88+
echo "Waiting for Exordos Core API... ($i/10)"
89+
sleep 5
90+
done
91+
8692
while [ $(exordos ee l -o json -f name=core | jq -r .[0]."Status") != "ACTIVE" ]; do
8793
sleep 2
8894
done

exordos_core/elements/dm/models.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,45 @@ def apply_element(self, element: "Element") -> "Manifest":
356356
self.apply_exports(element)
357357
return self
358358

359+
@staticmethod
360+
def _check_no_dependents(element: "Element") -> None:
361+
"""Check if any other installed element requires the given element.
362+
363+
Uses element_engine to get all installed elements and checks if any
364+
of them have the given element in their requirements.
365+
366+
Args:
367+
element: The element to check for dependents.
368+
369+
Raises:
370+
ValueError: If another installed element requires this element.
371+
"""
372+
373+
for other_element in element_engine.get_elements():
374+
# Skip the element itself
375+
if other_element.uuid == element.uuid:
376+
continue
377+
378+
if other_element.requirements:
379+
for req_element_name, req_spec in other_element.requirements.items():
380+
if req_element_name == element.name:
381+
# Check if element version is within the required range
382+
from_ver = req_spec.get("from_version")
383+
to_ver = req_spec.get("to_version")
384+
385+
version_matches = True
386+
if from_ver and element.version < from_ver:
387+
version_matches = False
388+
if to_ver and element.version > to_ver:
389+
version_matches = False
390+
391+
if version_matches:
392+
raise ValueError(
393+
f"Cannot uninstall element '{element.name}' version '{element.version}' "
394+
f"because it is required by element '{other_element.name}' version "
395+
f"'{other_element.version}'."
396+
)
397+
359398
def uninstall(self) -> "Manifest":
360399
element_engine.load_from_database()
361400

@@ -365,6 +404,8 @@ def uninstall(self) -> "Manifest":
365404
"version": ra_filters.EQ(self.version),
366405
}
367406
)
407+
for element in elements:
408+
self._check_no_dependents(element)
368409
for element in elements:
369410
element.delete()
370411
element_engine.remove_element(element)
@@ -1048,6 +1089,9 @@ def get_namespace(self, name: str) -> Namespace:
10481089
except KeyError:
10491090
raise exceptions.NamespaceNotFound(name=name)
10501091

1092+
def get_elements(self) -> tp.List["Element"]:
1093+
return [namespace.element for namespace in self._namespaces.values()]
1094+
10511095
def load_from_database(self) -> None:
10521096
self._namespaces = {}
10531097
self._resource_exports = {}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# Copyright 2025 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
import uuid as sys_uuid
18+
19+
import pytest
20+
21+
from exordos_core.elements.dm.models import Element
22+
from exordos_core.elements.dm.models import Manifest
23+
from exordos_core.elements.dm.models import element_engine
24+
25+
26+
class TestManifestCheckNoDependents:
27+
"""Tests for Manifest._check_no_dependents() method."""
28+
29+
@pytest.fixture(autouse=True)
30+
def setup_teardown(self):
31+
"""Fixture to ensure element_engine is cleaned up after each test."""
32+
yield
33+
# Teardown: cleanup any elements added during the test
34+
for element in element_engine.get_elements():
35+
element_engine.remove_element(element)
36+
37+
def test_check_passes_when_no_dependents(self, setup_teardown):
38+
"""Test that check passes when no other element requires the given element."""
39+
# Create dbaas element (the dependency)
40+
dbaas_element = Element(
41+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
42+
name="dbaas",
43+
version="0.2.2",
44+
link="$dbaas",
45+
)
46+
dbaas_element.requirements = {}
47+
48+
# Create stand element with no dependency on dbaas
49+
stand_element = Element(
50+
uuid=sys_uuid.UUID("33333333-3333-3333-3333-333333333333"),
51+
name="stand",
52+
version="1.0.0",
53+
link="$stand",
54+
)
55+
stand_element.requirements = {
56+
"core": {"from_version": "0.0.0", "to_version": "1.0.0"}
57+
}
58+
59+
# Setup element_engine
60+
element_engine.add_element(dbaas_element)
61+
element_engine.add_element(stand_element)
62+
63+
# Check should pass (no exception)
64+
Manifest._check_no_dependents(dbaas_element)
65+
66+
def test_check_detects_dependency_with_version_range(self, setup_teardown):
67+
"""Test that check detects dependency when element version is in required range."""
68+
# Create dbaas element (the dependency)
69+
dbaas_element = Element(
70+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
71+
name="dbaas",
72+
version="0.2.2",
73+
link="$dbaas",
74+
)
75+
dbaas_element.requirements = {}
76+
77+
# Create stand element that requires dbaas with version range
78+
stand_element = Element(
79+
uuid=sys_uuid.UUID("33333333-3333-3333-3333-333333333333"),
80+
name="stand",
81+
version="1.0.0",
82+
link="$stand",
83+
)
84+
stand_element.requirements = {
85+
"dbaas": {"from_version": "0.0.0", "to_version": "1.0.0"}
86+
}
87+
88+
# Setup element_engine
89+
element_engine.add_element(dbaas_element)
90+
element_engine.add_element(stand_element)
91+
92+
# Check should fail (0.2.2 is within range 0.0.0-1.0.0)
93+
with pytest.raises(ValueError) as exc_info:
94+
Manifest._check_no_dependents(dbaas_element)
95+
96+
assert "Cannot uninstall element 'dbaas' version '0.2.2'" in str(exc_info.value)
97+
assert "because it is required by element 'stand' version '1.0.0'" in str(
98+
exc_info.value
99+
)
100+
101+
def test_check_allows_version_below_range(self, setup_teardown):
102+
"""Test that check allows when element version is below required range."""
103+
# Create dbaas element version 0.2.2
104+
dbaas_element = Element(
105+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
106+
name="dbaas",
107+
version="0.2.2",
108+
link="$dbaas",
109+
)
110+
dbaas_element.requirements = {}
111+
112+
# Create stand element that requires version range starting at 0.3.0
113+
stand_element = Element(
114+
uuid=sys_uuid.UUID("33333333-3333-3333-3333-333333333333"),
115+
name="stand",
116+
version="1.0.0",
117+
link="$stand",
118+
)
119+
stand_element.requirements = {
120+
"dbaas": {"from_version": "0.3.0", "to_version": "1.0.0"}
121+
}
122+
123+
# Setup element_engine
124+
element_engine.add_element(dbaas_element)
125+
element_engine.add_element(stand_element)
126+
127+
# Check should pass (0.2.2 < 0.3.0)
128+
Manifest._check_no_dependents(dbaas_element)
129+
130+
def test_check_allows_version_above_range(self, setup_teardown):
131+
"""Test that check allows when element version is above required range."""
132+
# Create dbaas element version 0.2.2
133+
dbaas_element = Element(
134+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
135+
name="dbaas",
136+
version="0.2.2",
137+
link="$dbaas",
138+
)
139+
dbaas_element.requirements = {}
140+
141+
# Create stand element that requires version range ending at 0.2.0
142+
stand_element = Element(
143+
uuid=sys_uuid.UUID("33333333-3333-3333-3333-333333333333"),
144+
name="stand",
145+
version="1.0.0",
146+
link="$stand",
147+
)
148+
stand_element.requirements = {
149+
"dbaas": {"from_version": "0.0.0", "to_version": "0.2.0"}
150+
}
151+
152+
# Setup element_engine
153+
element_engine.add_element(dbaas_element)
154+
element_engine.add_element(stand_element)
155+
156+
# Check should pass (0.2.2 > 0.2.0)
157+
Manifest._check_no_dependents(dbaas_element)
158+
159+
def test_check_fails_with_only_from_version_when_in_range(self, setup_teardown):
160+
"""Test that check fails when version is within from_version range."""
161+
# Create dbaas element version 0.5.0
162+
dbaas_element = Element(
163+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
164+
name="dbaas",
165+
version="0.5.0",
166+
link="$dbaas",
167+
)
168+
dbaas_element.requirements = {}
169+
170+
# Create stand element that requires version >= 0.3.0 (no to_version)
171+
stand_element = Element(
172+
uuid=sys_uuid.UUID("33333333-3333-3333-3333-333333333333"),
173+
name="stand",
174+
version="1.0.0",
175+
link="$stand",
176+
)
177+
stand_element.requirements = {"dbaas": {"from_version": "0.3.0"}}
178+
179+
# Setup element_engine
180+
element_engine.add_element(dbaas_element)
181+
element_engine.add_element(stand_element)
182+
183+
# Check should fail (0.5.0 >= 0.3.0, so it's in range)
184+
with pytest.raises(ValueError) as exc_info:
185+
Manifest._check_no_dependents(dbaas_element)
186+
187+
assert "Cannot uninstall element 'dbaas' version '0.5.0'" in str(exc_info.value)
188+
189+
def test_check_fails_with_only_to_version_when_in_range(self, setup_teardown):
190+
"""Test that check fails when version is within to_version range."""
191+
# Create dbaas element version 0.1.5
192+
dbaas_element = Element(
193+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
194+
name="dbaas",
195+
version="0.1.5",
196+
link="$dbaas",
197+
)
198+
dbaas_element.requirements = {}
199+
200+
# Create stand element that requires version <= 0.2.0 (no from_version)
201+
stand_element = Element(
202+
uuid=sys_uuid.UUID("33333333-3333-3333-3333-333333333333"),
203+
name="stand",
204+
version="1.0.0",
205+
link="$stand",
206+
)
207+
stand_element.requirements = {"dbaas": {"to_version": "0.2.0"}}
208+
209+
# Setup element_engine
210+
element_engine.add_element(dbaas_element)
211+
element_engine.add_element(stand_element)
212+
213+
# Check should fail (0.1.5 <= 0.2.0, so it's in range)
214+
with pytest.raises(ValueError) as exc_info:
215+
Manifest._check_no_dependents(dbaas_element)
216+
217+
assert "Cannot uninstall element 'dbaas' version '0.1.5'" in str(exc_info.value)
218+
219+
def test_check_passes_with_only_from_version_when_below_range(self, setup_teardown):
220+
"""Test that check passes when version is below from_version."""
221+
# Create dbaas element version 0.2.0
222+
dbaas_element = Element(
223+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
224+
name="dbaas",
225+
version="0.2.0",
226+
link="$dbaas",
227+
)
228+
dbaas_element.requirements = {}
229+
230+
# Create stand element that requires version >= 0.3.0
231+
stand_element = Element(
232+
uuid=sys_uuid.UUID("33333333-3333-3333-3333-333333333333"),
233+
name="stand",
234+
version="1.0.0",
235+
link="$stand",
236+
)
237+
stand_element.requirements = {"dbaas": {"from_version": "0.3.0"}}
238+
239+
# Setup element_engine
240+
element_engine.add_element(dbaas_element)
241+
element_engine.add_element(stand_element)
242+
243+
# Check should pass (0.2.0 < 0.3.0, so it's below range)
244+
Manifest._check_no_dependents(dbaas_element)
245+
246+
def test_check_skips_same_element(self, setup_teardown):
247+
"""Test that check skips the element itself (same UUID)."""
248+
# Create dbaas element
249+
dbaas_element = Element(
250+
uuid=sys_uuid.UUID("11111111-1111-1111-1111-111111111111"),
251+
name="dbaas",
252+
version="0.2.2",
253+
link="$dbaas",
254+
)
255+
dbaas_element.requirements = {}
256+
257+
# Setup element_engine with only dbaas
258+
element_engine.add_element(dbaas_element)
259+
260+
# Check should pass (only element is itself)
261+
Manifest._check_no_dependents(dbaas_element)

0 commit comments

Comments
 (0)