Skip to content

Commit 00eff23

Browse files
yt-msMidnighter
authored andcommitted
feat: add ContainerInstances to DeploymentNodes
1 parent 4b74da5 commit 00eff23

File tree

5 files changed

+112
-4
lines changed

5 files changed

+112
-4
lines changed

src/structurizr/model/container_instance.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
if TYPE_CHECKING:
3131
from .container import Container
32+
from .deployment_node import DeploymentNode
3233
from .model import Model
3334

3435

@@ -60,6 +61,7 @@ def hydrate(
6061
cls,
6162
container_instance_io: ContainerInstanceIO,
6263
model: "Model",
64+
parent: "DeploymentNode",
6365
) -> "ContainerInstance":
6466
"""Hydrate a new ContainerInstance instance from its IO.
6567
@@ -69,6 +71,7 @@ def hydrate(
6971
instance = cls(
7072
**cls.hydrate_arguments(container_instance_io),
7173
container=container,
74+
parent=parent,
7275
)
7376
model += instance
7477
return instance

src/structurizr/model/deployment_node.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919

2020
from pydantic import Field
2121

22+
from .container import Container
2223
from .container_instance import ContainerInstance, ContainerInstanceIO
2324
from .deployment_element import DeploymentElement, DeploymentElementIO
25+
from .infrastructure_node import InfrastructureNode, InfrastructureNodeIO
2426
from .software_system_instance import SoftwareSystemInstance, SoftwareSystemInstanceIO
2527

2628

@@ -63,6 +65,9 @@ class Config:
6365
software_system_instances: List[SoftwareSystemInstanceIO] = Field(
6466
default=(), alias="softwareSystemInstances"
6567
)
68+
infrastructure_nodes: List[InfrastructureNodeIO] = Field(
69+
default=(), alias="infrastructureNodes"
70+
)
6671

6772

6873
DeploymentNodeIO.update_forward_refs()
@@ -94,6 +99,7 @@ def __init__(
9499
children: Iterable["DeploymentNode"] = (),
95100
container_instances: Iterable[ContainerInstance] = (),
96101
software_system_instances: Iterable[SoftwareSystemInstance] = (),
102+
infrastructure_nodes: Iterable[InfrastructureNode] = (),
97103
**kwargs,
98104
) -> None:
99105
"""Initialize a deployment node."""
@@ -104,6 +110,7 @@ def __init__(
104110
self._children = set(children)
105111
self._container_instances = set(container_instances)
106112
self._software_system_instances = set(software_system_instances)
113+
self._infrastructure_nodes = set(infrastructure_nodes)
107114

108115
@property
109116
def children(self) -> Iterable["DeploymentNode"]:
@@ -120,12 +127,48 @@ def software_system_instances(self) -> Iterable[SoftwareSystemInstance]:
120127
"""Return read-only list of software system instances."""
121128
return list(self._software_system_instances)
122129

130+
@property
131+
def infrastructure_nodes(self) -> Iterable[InfrastructureNode]:
132+
"""Return read-only list of infrastructure nodes."""
133+
return list(self._infrastructure_nodes)
134+
123135
def add_deployment_node(self, **kwargs) -> "DeploymentNode":
124136
"""Add a new child deployment node to this node."""
125137
node = DeploymentNode(**kwargs)
126138
self += node
127139
return node
128140

141+
def add_container_instance(
142+
self, container: Container, *, replicate_relationships: bool
143+
) -> ContainerInstance:
144+
"""
145+
Create a new instance of the given container.
146+
147+
Args:
148+
container(Container): the Container to add an instance of.
149+
replicate_relationships: True if relationships should be replicated between
150+
the element instances in the same deployment
151+
environment, False otherwise.
152+
"""
153+
instance_id = (
154+
max(
155+
[
156+
c.instance_id
157+
for c in self.container_instances
158+
if c.container is container
159+
],
160+
default=0,
161+
)
162+
+ 1
163+
)
164+
instance = ContainerInstance(
165+
container=container, instance_id=instance_id, parent=self
166+
)
167+
self._container_instances.add(instance)
168+
model = self.model
169+
model += instance
170+
return instance
171+
129172
def __iadd__(self, node: "DeploymentNode") -> "DeploymentNode":
130173
"""Add a newly constructed chile deployment node to this node."""
131174
if node in self._children:
@@ -161,13 +204,17 @@ def hydrate(
161204
This will also automatically register with the model.
162205
"""
163206
node = cls(
164-
parent=parent,
165207
**cls.hydrate_arguments(deployment_node_io),
208+
parent=parent,
166209
)
167210
model += node
168211

169212
for child_io in deployment_node_io.children:
170213
child_node = DeploymentNode.hydrate(child_io, model=model, parent=node)
171214
node += child_node
172215

216+
for instance_io in deployment_node_io.container_instances:
217+
instance = ContainerInstance.hydrate(instance_io, model=model, parent=node)
218+
node._container_instances.add(instance)
219+
173220
return node

src/structurizr/model/static_structure_element_instance.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(
4747
element: StaticStructureElement,
4848
instance_id: int,
4949
health_checks: Iterable["HTTPHealthCheck"] = (),
50+
parent: "DeploymentNode" = None,
5051
**kwargs
5152
) -> None:
5253
"""Initialize a StaticStructureElementInstance."""
@@ -56,6 +57,7 @@ def __init__(
5657
super().__init__(**kwargs)
5758
self.instance_id = instance_id
5859
self.health_checks = set(health_checks)
60+
self.parent = parent
5961

6062
@classmethod
6163
def hydrate_arguments(cls, instance_io: StaticStructureElementInstanceIO) -> dict:

tests/unit/model/test_container_instance.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def test_container_instance_hydration_retrieves_container_from_id(model_with_con
8787
"""Check that the container instance is retrieved from the model on hydration."""
8888
io = ContainerInstanceIO(container_id="19", instance_id=3, environment="Live")
8989

90-
instance = ContainerInstance.hydrate(io, model_with_container)
90+
instance = ContainerInstance.hydrate(io, model_with_container, parent=None)
9191

9292
assert instance.instance_id == 3
9393
assert instance.environment == "Live"
@@ -99,7 +99,7 @@ def test_container_instance_name_is_container_name(model_with_container):
9999
"""Ensure container instance takes its name from its container."""
100100
io = ContainerInstanceIO(container_id="19", instance_id=3, environment="Live")
101101

102-
instance = ContainerInstance.hydrate(io, model_with_container)
102+
instance = ContainerInstance.hydrate(io, model_with_container, parent=None)
103103

104104
assert instance.name == "Mock Container"
105105

@@ -114,7 +114,7 @@ def test_container_instance_hyrdates_http_health_checks(model_with_container):
114114
health_checks=[health_check_io],
115115
)
116116

117-
instance = ContainerInstance.hydrate(io, model_with_container)
117+
instance = ContainerInstance.hydrate(io, model_with_container, parent=None)
118118

119119
assert len(instance.health_checks) == 1
120120
assert list(instance.health_checks)[0].name == "name"

tests/unit/model/test_deployment_node.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def __init__(self):
2626
"""Initialize the mock, creating an empty node for tests."""
2727
self.empty_node = DeploymentNode(name="Empty")
2828
self.empty_node.set_model(self)
29+
self.mock_element = MockElement("element")
2930

3031
def __iadd__(self, node):
3132
"""Simulate the model assigning IDs to new elements."""
@@ -34,6 +35,20 @@ def __iadd__(self, node):
3435
node.set_model(self)
3536
return self
3637

38+
def get_element(self, id: str):
39+
"""Simulate getting an element by ID."""
40+
assert id == self.mock_element.id
41+
return self.mock_element
42+
43+
44+
class MockElement:
45+
"""Implement a mock element for testing."""
46+
47+
def __init__(self, name: str):
48+
"""Initialise the mock."""
49+
self.name = name
50+
self.id = name
51+
3752

3853
@pytest.fixture(scope="function")
3954
def model_with_node() -> MockModel:
@@ -108,3 +123,44 @@ def test_deployment_node_serialization_of_recursive_nodes(model_with_node):
108123
assert new_child.name == "child"
109124
assert new_child.parent is new_top_node
110125
assert new_child.model is model_with_node
126+
127+
128+
def test_deployment_node_add_container(model_with_node):
129+
"""Test adding a container to a node to create an instance."""
130+
node = model_with_node.empty_node
131+
container = MockElement("element")
132+
133+
instance = node.add_container_instance(container, replicate_relationships=False)
134+
135+
assert instance.container is container
136+
assert instance.model is model_with_node
137+
assert instance.parent is node
138+
assert instance in node.container_instances
139+
assert instance.instance_id == 1
140+
141+
142+
def test_deployment_node_serialising_container(model_with_node):
143+
"""Test serialisation and deserialisation includes container instances."""
144+
node = model_with_node.empty_node
145+
container = model_with_node.mock_element
146+
node.add_container_instance(container, replicate_relationships=False)
147+
148+
io = DeploymentNodeIO.from_orm(node)
149+
150+
assert len(io.container_instances) == 1
151+
assert io.container_instances[0].id == "id"
152+
153+
node2 = DeploymentNode.hydrate(io, model_with_node)
154+
155+
assert len(node2.container_instances) == 1
156+
instance = node2.container_instances[0]
157+
assert instance.instance_id == 1
158+
assert instance.container is container
159+
assert instance.model is model_with_node
160+
assert instance.parent is node2
161+
162+
163+
@pytest.mark.xfail(strict=True)
164+
def test_deployment_node_adding_container_replicating_relationships(model_with_node):
165+
"""Test replicating relationships when adding a container instance."""
166+
raise AssertionError() # Not implemented yet

0 commit comments

Comments
 (0)