buildzr 0.0.18__tar.gz → 0.0.20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {buildzr-0.0.18 → buildzr-0.0.20}/PKG-INFO +1 -1
- buildzr-0.0.20/buildzr/__about__.py +1 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/dsl.py +109 -3
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_dsl.py +407 -0
- buildzr-0.0.18/buildzr/__about__.py +0 -1
- {buildzr-0.0.18 → buildzr-0.0.20}/.gitignore +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/CONTRIBUTING.md +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/LICENSE.md +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/README.md +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/color.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/explorer.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/expression.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/factory/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/factory/gen_id.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/interfaces/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/interfaces/interfaces.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/dsl/relations.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/encoders/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/encoders/encoder.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/models/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/models/generate.sh +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/models/models.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/sinks/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/sinks/interfaces.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/buildzr/sinks/json_sink.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/pyproject.toml +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/abstract_builder.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/__init__.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/component_view.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/container_view.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/container_view_sugar.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/groups.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/implied_relationships.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/nested_groups.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/simple.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/simple_dsl.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/system_context_view.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/samples/system_landscape_view.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_explorer.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_expression.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_typehints.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_views.py +0 -0
- {buildzr-0.0.18 → buildzr-0.0.20}/tests/test_workspaces.py +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
VERSION = "0.0.20"
|
@@ -129,14 +129,14 @@ class Workspace(DslWorkspaceElement):
|
|
129
129
|
|
130
130
|
_current_workspace.reset(self._token)
|
131
131
|
|
132
|
-
def _imply_relationships(
|
133
|
-
self,
|
132
|
+
def _imply_relationships( self,
|
134
133
|
) -> None:
|
135
134
|
|
136
135
|
"""
|
137
136
|
Process implied relationships:
|
138
137
|
If we have relationship s >> do >> a.b, then create s >> do >> a.
|
139
138
|
If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
|
139
|
+
If we have relationship s.ss >> do >> a, then create s >> do >> a.
|
140
140
|
And so on...
|
141
141
|
|
142
142
|
Relationships of `SoftwareSystemInstance`s and `ContainerInstance`s are
|
@@ -152,6 +152,7 @@ class Workspace(DslWorkspaceElement):
|
|
152
152
|
from buildzr.dsl.explorer import Explorer
|
153
153
|
|
154
154
|
explorer = Explorer(self)
|
155
|
+
# Take a snapshot of relationships to avoid processing newly created ones
|
155
156
|
relationships = list(explorer.walk_relationships())
|
156
157
|
for relationship in relationships:
|
157
158
|
source = relationship.source
|
@@ -162,6 +163,11 @@ class Workspace(DslWorkspaceElement):
|
|
162
163
|
isinstance(destination, (SoftwareSystemInstance, ContainerInstance)):
|
163
164
|
continue
|
164
165
|
|
166
|
+
# Skip relationships that are already implied (have linkedRelationshipId)
|
167
|
+
if relationship.model.linkedRelationshipId is not None:
|
168
|
+
continue
|
169
|
+
|
170
|
+
# Handle case: s >> a.b => s >> a (destination is child)
|
165
171
|
while destination_parent is not None and \
|
166
172
|
isinstance(source, DslElement) and \
|
167
173
|
not isinstance(source.model, buildzr.models.Workspace) and \
|
@@ -186,7 +192,38 @@ class Workspace(DslWorkspaceElement):
|
|
186
192
|
technology=relationship.model.technology,
|
187
193
|
)
|
188
194
|
r.model.linkedRelationshipId = relationship.model.id
|
189
|
-
|
195
|
+
destination_parent = destination_parent.parent
|
196
|
+
|
197
|
+
# Handle inverse case: s.ss >> a => s >> a (source is child)
|
198
|
+
source_parent = source.parent
|
199
|
+
while source_parent is not None and \
|
200
|
+
isinstance(destination, DslElement) and \
|
201
|
+
not isinstance(destination.model, buildzr.models.Workspace) and \
|
202
|
+
not isinstance(source_parent.model, buildzr.models.Workspace) and \
|
203
|
+
not isinstance(source_parent, DslWorkspaceElement):
|
204
|
+
|
205
|
+
if source_parent is destination.parent:
|
206
|
+
break
|
207
|
+
|
208
|
+
rels = source_parent.model.relationships
|
209
|
+
|
210
|
+
# The parent source relationship might be empty
|
211
|
+
# (i.e., []).
|
212
|
+
if rels is not None:
|
213
|
+
already_exists = any(
|
214
|
+
r.destinationId == destination.model.id and
|
215
|
+
r.description == relationship.model.description and
|
216
|
+
r.technology == relationship.model.technology
|
217
|
+
for r in rels
|
218
|
+
)
|
219
|
+
if not already_exists:
|
220
|
+
r = source_parent.uses(
|
221
|
+
destination,
|
222
|
+
description=relationship.model.description,
|
223
|
+
technology=relationship.model.technology,
|
224
|
+
)
|
225
|
+
r.model.linkedRelationshipId = relationship.model.id
|
226
|
+
source_parent = source_parent.parent
|
190
227
|
|
191
228
|
def person(self) -> TypedDynamicAttribute['Person']:
|
192
229
|
return TypedDynamicAttribute['Person'](self._dynamic_attrs)
|
@@ -371,6 +408,7 @@ class SoftwareSystem(DslElementRelationOverrides[
|
|
371
408
|
self.model.id = GenerateId.for_element()
|
372
409
|
self.model.name = name
|
373
410
|
self.model.description = description
|
411
|
+
self.model.relationships = []
|
374
412
|
self.model.tags = ','.join(self._tags)
|
375
413
|
self.model.properties = properties
|
376
414
|
|
@@ -804,6 +842,10 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
|
|
804
842
|
new relationship between those software system instances.
|
805
843
|
|
806
844
|
These implied relationships are used in `DeploymentView`.
|
845
|
+
|
846
|
+
Relationships are only created between instances that share at least
|
847
|
+
one common deployment group. If no deployment groups are specified,
|
848
|
+
instances are considered to be in the same default group.
|
807
849
|
"""
|
808
850
|
|
809
851
|
software_instances = [
|
@@ -839,12 +881,25 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
|
|
839
881
|
if not relationship.destinationId in other_softwares_ids:
|
840
882
|
continue
|
841
883
|
|
884
|
+
if software.model.id not in software_instance_map:
|
885
|
+
continue
|
886
|
+
|
887
|
+
if relationship.destinationId not in software_instance_map:
|
888
|
+
continue
|
889
|
+
|
842
890
|
this_software_instances = software_instance_map[software.model.id]
|
843
891
|
other_software_instances = software_instance_map[relationship.destinationId]
|
844
892
|
|
845
893
|
for this_software_instance in this_software_instances:
|
846
894
|
for other_software_instance in other_software_instances:
|
847
895
|
|
896
|
+
# Only create relationship if instances share a deployment group
|
897
|
+
if not self._instances_share_deployment_group(
|
898
|
+
this_software_instance,
|
899
|
+
other_software_instance
|
900
|
+
):
|
901
|
+
continue
|
902
|
+
|
848
903
|
already_exists = this_software_instance.model.relationships is not None and any(
|
849
904
|
r.sourceId == this_software_instance.model.id and
|
850
905
|
r.destinationId == other_software_instance.model.id and
|
@@ -862,6 +917,40 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
|
|
862
917
|
)
|
863
918
|
r.model.linkedRelationshipId = relationship.id
|
864
919
|
|
920
|
+
def _instances_share_deployment_group(
|
921
|
+
self,
|
922
|
+
instance1: Union['ContainerInstance', 'SoftwareSystemInstance'],
|
923
|
+
instance2: Union['ContainerInstance', 'SoftwareSystemInstance']
|
924
|
+
) -> bool:
|
925
|
+
"""
|
926
|
+
Check if two deployment instances share at least one common deployment group.
|
927
|
+
|
928
|
+
If either instance has no deployment groups specified, they are considered
|
929
|
+
to be in the "default" group and can relate to all other instances without
|
930
|
+
deployment groups.
|
931
|
+
|
932
|
+
Args:
|
933
|
+
instance1: First deployment instance
|
934
|
+
instance2: Second deployment instance
|
935
|
+
|
936
|
+
Returns:
|
937
|
+
True if instances share at least one deployment group or if both have
|
938
|
+
no deployment groups specified, False otherwise.
|
939
|
+
"""
|
940
|
+
groups1 = set(instance1.model.deploymentGroups or [])
|
941
|
+
groups2 = set(instance2.model.deploymentGroups or [])
|
942
|
+
|
943
|
+
# If both have no deployment groups, they can relate
|
944
|
+
if not groups1 and not groups2:
|
945
|
+
return True
|
946
|
+
|
947
|
+
# If one has groups and the other doesn't, they cannot relate
|
948
|
+
if (groups1 and not groups2) or (not groups1 and groups2):
|
949
|
+
return False
|
950
|
+
|
951
|
+
# Check if they share at least one common group
|
952
|
+
return bool(groups1.intersection(groups2))
|
953
|
+
|
865
954
|
def _imply_container_instance_relationships(self, workspace: Workspace) -> None:
|
866
955
|
|
867
956
|
"""
|
@@ -871,6 +960,10 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
|
|
871
960
|
between those container instances.
|
872
961
|
|
873
962
|
These implied relationships are used in `DeploymentView`.
|
963
|
+
|
964
|
+
Relationships are only created between instances that share at least
|
965
|
+
one common deployment group. If no deployment groups are specified,
|
966
|
+
instances are considered to be in the same default group.
|
874
967
|
"""
|
875
968
|
|
876
969
|
from buildzr.dsl.expression import Expression
|
@@ -907,12 +1000,25 @@ class DeploymentEnvironment(DslDeploymentEnvironment):
|
|
907
1000
|
if not relationship.destinationId in other_containers_ids:
|
908
1001
|
continue
|
909
1002
|
|
1003
|
+
if container.model.id not in container_instance_map:
|
1004
|
+
continue
|
1005
|
+
|
1006
|
+
if relationship.destinationId not in container_instance_map:
|
1007
|
+
continue
|
1008
|
+
|
910
1009
|
this_container_instances = container_instance_map[container.model.id]
|
911
1010
|
other_container_instances = container_instance_map[relationship.destinationId]
|
912
1011
|
|
913
1012
|
for this_container_instance in this_container_instances:
|
914
1013
|
for other_container_instance in other_container_instances:
|
915
1014
|
|
1015
|
+
# Only create relationship if instances share a deployment group
|
1016
|
+
if not self._instances_share_deployment_group(
|
1017
|
+
this_container_instance,
|
1018
|
+
other_container_instance
|
1019
|
+
):
|
1020
|
+
continue
|
1021
|
+
|
916
1022
|
already_exists = this_container_instance.model.relationships is not None and any(
|
917
1023
|
r.sourceId == this_container_instance.model.id and
|
918
1024
|
r.destinationId == other_container_instance.model.id and
|
@@ -399,6 +399,49 @@ def test_implied_relationship() -> Optional[None]:
|
|
399
399
|
os.remove('workspace.test.json')
|
400
400
|
os.remove('workspace2.test.json')
|
401
401
|
|
402
|
+
def test_inverse_implied_relationship() -> Optional[None]:
|
403
|
+
"""
|
404
|
+
Test that inverse implied relationships work correctly.
|
405
|
+
When a.container >> b (child to parent), it should imply a >> b.
|
406
|
+
|
407
|
+
See: https://docs.structurizr.com/java/implied-relationships
|
408
|
+
"""
|
409
|
+
|
410
|
+
with Workspace("w", implied_relationships=True) as w:
|
411
|
+
u = Person('User')
|
412
|
+
s = SoftwareSystem('System')
|
413
|
+
with s:
|
414
|
+
api = Container('API')
|
415
|
+
db = Container('Database')
|
416
|
+
api >> "Uses" >> db
|
417
|
+
|
418
|
+
# Create relationship from child to parent: s.api >> u
|
419
|
+
# This should imply s >> u
|
420
|
+
s.api >> "Notifies" >> u
|
421
|
+
|
422
|
+
# Invoke implied relationships via view
|
423
|
+
SystemContextView(
|
424
|
+
software_system_selector=s,
|
425
|
+
key='s_context',
|
426
|
+
description="System context view",
|
427
|
+
)
|
428
|
+
|
429
|
+
w.to_json('workspace.inverse.test.json')
|
430
|
+
|
431
|
+
# Check that System has an implied relationship to User
|
432
|
+
assert len(s.model.relationships) == 1
|
433
|
+
assert s.model.relationships[0].description == "Notifies"
|
434
|
+
assert s.model.relationships[0].sourceId == s.model.id
|
435
|
+
assert s.model.relationships[0].destinationId == u.model.id
|
436
|
+
assert s.model.relationships[0].linkedRelationshipId == s.api.model.relationships[1].id
|
437
|
+
|
438
|
+
# The implied relationship should appear in system context view
|
439
|
+
system_context_view_relationships = [x.id for x in w._m.views.systemContextViews[0].relationships]
|
440
|
+
assert s.model.relationships[0].id in system_context_view_relationships
|
441
|
+
|
442
|
+
import os
|
443
|
+
os.remove('workspace.inverse.test.json')
|
444
|
+
|
402
445
|
def test_tags_on_elements() -> Optional[None]:
|
403
446
|
|
404
447
|
u = Person('My User', tags={'admin'})
|
@@ -1105,6 +1148,7 @@ def test_json_sink_empty_views() -> Optional[None]:
|
|
1105
1148
|
|
1106
1149
|
import os
|
1107
1150
|
os.remove("test.json")
|
1151
|
+
|
1108
1152
|
def test_deployment_instance_relationships_with_implied_relationships() -> Optional[None]:
|
1109
1153
|
"""
|
1110
1154
|
Test that deployment instance relationships are created correctly when
|
@@ -1183,3 +1227,366 @@ def test_deployment_instance_relationships_with_implied_relationships() -> Optio
|
|
1183
1227
|
import os
|
1184
1228
|
os.remove('test_deployment1.json')
|
1185
1229
|
os.remove('test_deployment2.json')
|
1230
|
+
|
1231
|
+
def test_imply_relationships_before_deployment_environment_not_crashing() -> Optional[None]:
|
1232
|
+
|
1233
|
+
with Workspace('workspace') as w:
|
1234
|
+
|
1235
|
+
with SoftwareSystem("X") as x:
|
1236
|
+
|
1237
|
+
# Notice that we don't need to specify the tags "Application" and "Database"
|
1238
|
+
# for styling -- just pass the `wa` and `db` variables directly to the `StyleElements` class.
|
1239
|
+
wa = Container("Web Application", technology="Java and Spring boot")
|
1240
|
+
db = Container("Database Schema")
|
1241
|
+
|
1242
|
+
wa >> "Reads from and writes to" >> db
|
1243
|
+
|
1244
|
+
with DeploymentEnvironment("Live") as live:
|
1245
|
+
with DeploymentNode("Amazon Web Services") as aws:
|
1246
|
+
aws.add_tags("Amazon Web Services - Cloud")
|
1247
|
+
|
1248
|
+
with DeploymentNode("US-East-1") as region:
|
1249
|
+
region.add_tags("Amazon Web Services - Region")
|
1250
|
+
|
1251
|
+
dns = InfrastructureNode(
|
1252
|
+
"DNS Router",
|
1253
|
+
description="Routes incoming requests based upon domain name.",
|
1254
|
+
technology="Route 53",
|
1255
|
+
tags={"Amazon Web Services - Route 53"}
|
1256
|
+
)
|
1257
|
+
|
1258
|
+
lb = InfrastructureNode(
|
1259
|
+
"Load Balancer",
|
1260
|
+
description="Automatically distributes incoming application traffic.",
|
1261
|
+
technology="Elastic Load Balancer",
|
1262
|
+
tags={"Amazon Web Services - Elastic Load Balancer"}
|
1263
|
+
)
|
1264
|
+
|
1265
|
+
dns >> ("Fowards requests to", "HTTP") >> lb
|
1266
|
+
|
1267
|
+
with DeploymentNode("Amazon EC2", tags={"Amazon Web Services - EC2"}) as asg:
|
1268
|
+
with DeploymentNode("Amazon EC2 - Ubuntu Server", tags={"Amazon Web Services - EC2 Instance"}):
|
1269
|
+
lb >> "Forwards requests to" >> ContainerInstance(wa)
|
1270
|
+
|
1271
|
+
with DeploymentNode("Amazon RDS", tags={"Amazon Web Services - RDS Instance"}) as rds:
|
1272
|
+
with DeploymentNode("MySQL", tags={"Amazon Web Services - RDS MySQL instance"}):
|
1273
|
+
database_instance = ContainerInstance(db)
|
1274
|
+
|
1275
|
+
DeploymentView(
|
1276
|
+
environment=live,
|
1277
|
+
key='aws-deployment-view',
|
1278
|
+
software_system_selector=x,
|
1279
|
+
title="Amazon Web Services Deployment",
|
1280
|
+
description="Deployment view of the web application on AWS",
|
1281
|
+
auto_layout='lr',
|
1282
|
+
)
|
1283
|
+
|
1284
|
+
w.to_json('amazon_web_services.json', pretty=True)
|
1285
|
+
|
1286
|
+
def test_software_system_instance_relationships_with_missing_instances() -> Optional[None]:
|
1287
|
+
"""
|
1288
|
+
Test that _imply_software_system_instance_relationships doesn't crash when
|
1289
|
+
a software system has a relationship to another software system, but only
|
1290
|
+
one of them has instances deployed.
|
1291
|
+
|
1292
|
+
This reproduces the bug where:
|
1293
|
+
- E-Commerce System has instances deployed
|
1294
|
+
- Payment Provider has NO instances deployed
|
1295
|
+
- E-Commerce System -> Payment Provider relationship exists
|
1296
|
+
- Should not crash with KeyError when trying to look up Payment Provider instances
|
1297
|
+
"""
|
1298
|
+
|
1299
|
+
with Workspace('test-workspace') as w:
|
1300
|
+
# Create two software systems with a relationship
|
1301
|
+
ecommerce = SoftwareSystem('E-Commerce System')
|
1302
|
+
payment_provider = SoftwareSystem('Payment Provider')
|
1303
|
+
|
1304
|
+
ecommerce >> "Processes payments via" >> payment_provider
|
1305
|
+
|
1306
|
+
# Deploy only the E-Commerce System, NOT the Payment Provider
|
1307
|
+
with DeploymentEnvironment('Production') as prod:
|
1308
|
+
with DeploymentNode('AWS'):
|
1309
|
+
ecommerce_instance = SoftwareSystemInstance(ecommerce)
|
1310
|
+
|
1311
|
+
# This should not crash - the implication happens in DeploymentEnvironment.__exit__
|
1312
|
+
# Even though ecommerce has a relationship to payment_provider,
|
1313
|
+
# payment_provider has no instances deployed
|
1314
|
+
|
1315
|
+
# If we get here without a KeyError, the test passes
|
1316
|
+
assert ecommerce_instance.model.softwareSystemId == ecommerce.model.id
|
1317
|
+
|
1318
|
+
def test_container_instance_relationships_with_missing_instances() -> Optional[None]:
|
1319
|
+
"""
|
1320
|
+
Test that _imply_container_instance_relationships doesn't crash when
|
1321
|
+
a container has a relationship to another container, but only one of
|
1322
|
+
them has instances deployed.
|
1323
|
+
|
1324
|
+
This tests the same bug pattern but for containers instead of software systems.
|
1325
|
+
"""
|
1326
|
+
|
1327
|
+
with Workspace('test-workspace') as w:
|
1328
|
+
# Create software system with containers that have relationships
|
1329
|
+
with SoftwareSystem('E-Commerce System') as ecommerce:
|
1330
|
+
web_app = Container('Web Application')
|
1331
|
+
api = Container('API')
|
1332
|
+
database = Container('Database')
|
1333
|
+
external_service = Container('External Payment Service')
|
1334
|
+
|
1335
|
+
# Create relationships
|
1336
|
+
web_app >> "Calls" >> api
|
1337
|
+
api >> "Stores data in" >> database
|
1338
|
+
api >> "Processes payments via" >> external_service
|
1339
|
+
|
1340
|
+
# Deploy only some containers, NOT all of them
|
1341
|
+
with DeploymentEnvironment('Production') as prod:
|
1342
|
+
with DeploymentNode('AWS'):
|
1343
|
+
web_app_instance = ContainerInstance(web_app)
|
1344
|
+
api_instance = ContainerInstance(api)
|
1345
|
+
db_instance = ContainerInstance(database)
|
1346
|
+
# Note: external_service is NOT deployed
|
1347
|
+
|
1348
|
+
# This should not crash - even though api has a relationship to external_service,
|
1349
|
+
# external_service has no instances deployed
|
1350
|
+
|
1351
|
+
# If we get here without a KeyError, the test passes
|
1352
|
+
assert web_app_instance.model.containerId == web_app.model.id
|
1353
|
+
assert api_instance.model.containerId == api.model.id
|
1354
|
+
assert db_instance.model.containerId == database.model.id
|
1355
|
+
|
1356
|
+
# Verify that the deployed instances DO have implied relationships
|
1357
|
+
# web_app_instance should have relationship to api_instance
|
1358
|
+
web_to_api_rels = [
|
1359
|
+
r for r in (web_app_instance.model.relationships or [])
|
1360
|
+
if r.destinationId == api_instance.model.id
|
1361
|
+
]
|
1362
|
+
assert len(web_to_api_rels) == 1
|
1363
|
+
|
1364
|
+
# api_instance should have relationship to db_instance
|
1365
|
+
api_to_db_rels = [
|
1366
|
+
r for r in (api_instance.model.relationships or [])
|
1367
|
+
if r.destinationId == db_instance.model.id
|
1368
|
+
]
|
1369
|
+
assert len(api_to_db_rels) == 1
|
1370
|
+
|
1371
|
+
# But api_instance should NOT have a relationship to external_service
|
1372
|
+
# (because it wasn't deployed)
|
1373
|
+
all_api_destinations = [
|
1374
|
+
r.destinationId for r in (api_instance.model.relationships or [])
|
1375
|
+
]
|
1376
|
+
assert external_service.model.id not in all_api_destinations
|
1377
|
+
|
1378
|
+
def test_container_instance_relationships_respect_deployment_groups() -> Optional[None]:
|
1379
|
+
"""
|
1380
|
+
Test that container instance relationships only connect instances within
|
1381
|
+
the same deployment group.
|
1382
|
+
|
1383
|
+
When containers have relationships and are deployed with deployment groups,
|
1384
|
+
instance relationships should only be created between instances that share
|
1385
|
+
at least one common deployment group.
|
1386
|
+
|
1387
|
+
This matches the behavior in Structurizr DSL where deployment groups act
|
1388
|
+
as boundaries for relationship propagation.
|
1389
|
+
"""
|
1390
|
+
|
1391
|
+
with Workspace("w", scope=None) as w:
|
1392
|
+
with SoftwareSystem("Software System") as software_system:
|
1393
|
+
database = Container("Database")
|
1394
|
+
api = Container("Service API")
|
1395
|
+
api >> "Reads from and writes to" >> database
|
1396
|
+
|
1397
|
+
with DeploymentEnvironment("Production") as production:
|
1398
|
+
service_instance_1 = DeploymentGroup("Service Instance 1")
|
1399
|
+
service_instance_2 = DeploymentGroup("Service Instance 2")
|
1400
|
+
|
1401
|
+
with DeploymentNode("Server 1") as server_1:
|
1402
|
+
api_instance_1 = ContainerInstance(api, [service_instance_1])
|
1403
|
+
with DeploymentNode("Database Server"):
|
1404
|
+
db_instance_1 = ContainerInstance(database, [service_instance_1])
|
1405
|
+
|
1406
|
+
with DeploymentNode("Server 2") as server_2:
|
1407
|
+
api_instance_2 = ContainerInstance(api, [service_instance_2])
|
1408
|
+
with DeploymentNode("Database Server"):
|
1409
|
+
db_instance_2 = ContainerInstance(database, [service_instance_2])
|
1410
|
+
|
1411
|
+
# Verify deployment group assignments
|
1412
|
+
assert api_instance_1.model.deploymentGroups == ["Service Instance 1"]
|
1413
|
+
assert db_instance_1.model.deploymentGroups == ["Service Instance 1"]
|
1414
|
+
assert api_instance_2.model.deploymentGroups == ["Service Instance 2"]
|
1415
|
+
assert db_instance_2.model.deploymentGroups == ["Service Instance 2"]
|
1416
|
+
|
1417
|
+
# Check that api_instance_1 only has relationship to db_instance_1 (same group)
|
1418
|
+
api_1_relationships = [
|
1419
|
+
r for r in (api_instance_1.model.relationships or [])
|
1420
|
+
if r.description == "Reads from and writes to"
|
1421
|
+
]
|
1422
|
+
assert len(api_1_relationships) == 1, f"Expected 1 relationship, found {len(api_1_relationships)}"
|
1423
|
+
assert api_1_relationships[0].destinationId == db_instance_1.model.id
|
1424
|
+
assert api_1_relationships[0].destinationId != db_instance_2.model.id
|
1425
|
+
|
1426
|
+
# Check that api_instance_2 only has relationship to db_instance_2 (same group)
|
1427
|
+
api_2_relationships = [
|
1428
|
+
r for r in (api_instance_2.model.relationships or [])
|
1429
|
+
if r.description == "Reads from and writes to"
|
1430
|
+
]
|
1431
|
+
assert len(api_2_relationships) == 1, f"Expected 1 relationship, found {len(api_2_relationships)}"
|
1432
|
+
assert api_2_relationships[0].destinationId == db_instance_2.model.id
|
1433
|
+
assert api_2_relationships[0].destinationId != db_instance_1.model.id
|
1434
|
+
|
1435
|
+
# Verify no cross-group relationships exist
|
1436
|
+
all_api_1_destinations = [r.destinationId for r in (api_instance_1.model.relationships or [])]
|
1437
|
+
all_api_2_destinations = [r.destinationId for r in (api_instance_2.model.relationships or [])]
|
1438
|
+
|
1439
|
+
assert db_instance_2.model.id not in all_api_1_destinations, "api_instance_1 should not connect to db_instance_2"
|
1440
|
+
assert db_instance_1.model.id not in all_api_2_destinations, "api_instance_2 should not connect to db_instance_1"
|
1441
|
+
|
1442
|
+
def test_software_system_instance_relationships_respect_deployment_groups() -> Optional[None]:
|
1443
|
+
"""
|
1444
|
+
Test that software system instance relationships only connect instances
|
1445
|
+
within the same deployment group.
|
1446
|
+
|
1447
|
+
When software systems have relationships and are deployed with deployment
|
1448
|
+
groups, instance relationships should only be created between instances
|
1449
|
+
that share at least one common deployment group.
|
1450
|
+
"""
|
1451
|
+
|
1452
|
+
with Workspace("w", scope=None) as w:
|
1453
|
+
api_system = SoftwareSystem("API System")
|
1454
|
+
db_system = SoftwareSystem("Database System")
|
1455
|
+
|
1456
|
+
api_system >> "Connects to" >> db_system
|
1457
|
+
|
1458
|
+
with DeploymentEnvironment("Production") as production:
|
1459
|
+
region_1 = DeploymentGroup("Region 1")
|
1460
|
+
region_2 = DeploymentGroup("Region 2")
|
1461
|
+
|
1462
|
+
with DeploymentNode("Datacenter 1") as dc1:
|
1463
|
+
api_instance_1 = SoftwareSystemInstance(api_system, [region_1])
|
1464
|
+
db_instance_1 = SoftwareSystemInstance(db_system, [region_1])
|
1465
|
+
|
1466
|
+
with DeploymentNode("Datacenter 2") as dc2:
|
1467
|
+
api_instance_2 = SoftwareSystemInstance(api_system, [region_2])
|
1468
|
+
db_instance_2 = SoftwareSystemInstance(db_system, [region_2])
|
1469
|
+
|
1470
|
+
# Verify deployment group assignments
|
1471
|
+
assert api_instance_1.model.deploymentGroups == ["Region 1"]
|
1472
|
+
assert db_instance_1.model.deploymentGroups == ["Region 1"]
|
1473
|
+
assert api_instance_2.model.deploymentGroups == ["Region 2"]
|
1474
|
+
assert db_instance_2.model.deploymentGroups == ["Region 2"]
|
1475
|
+
|
1476
|
+
# Check that api_instance_1 only has relationship to db_instance_1 (same group)
|
1477
|
+
api_1_relationships = [
|
1478
|
+
r for r in (api_instance_1.model.relationships or [])
|
1479
|
+
if r.description == "Connects to"
|
1480
|
+
]
|
1481
|
+
assert len(api_1_relationships) == 1, f"Expected 1 relationship, found {len(api_1_relationships)}"
|
1482
|
+
assert api_1_relationships[0].destinationId == db_instance_1.model.id
|
1483
|
+
assert api_1_relationships[0].destinationId != db_instance_2.model.id
|
1484
|
+
|
1485
|
+
# Check that api_instance_2 only has relationship to db_instance_2 (same group)
|
1486
|
+
api_2_relationships = [
|
1487
|
+
r for r in (api_instance_2.model.relationships or [])
|
1488
|
+
if r.description == "Connects to"
|
1489
|
+
]
|
1490
|
+
assert len(api_2_relationships) == 1, f"Expected 1 relationship, found {len(api_2_relationships)}"
|
1491
|
+
assert api_2_relationships[0].destinationId == db_instance_2.model.id
|
1492
|
+
assert api_2_relationships[0].destinationId != db_instance_1.model.id
|
1493
|
+
|
1494
|
+
# Verify no cross-group relationships exist
|
1495
|
+
all_api_1_destinations = [r.destinationId for r in (api_instance_1.model.relationships or [])]
|
1496
|
+
all_api_2_destinations = [r.destinationId for r in (api_instance_2.model.relationships or [])]
|
1497
|
+
|
1498
|
+
assert db_instance_2.model.id not in all_api_1_destinations, "api_instance_1 should not connect to db_instance_2"
|
1499
|
+
assert db_instance_1.model.id not in all_api_2_destinations, "api_instance_2 should not connect to db_instance_1"
|
1500
|
+
|
1501
|
+
def test_container_instance_relationships_with_multiple_shared_deployment_groups() -> Optional[None]:
|
1502
|
+
"""
|
1503
|
+
Test that container instances with overlapping deployment groups can have
|
1504
|
+
relationships.
|
1505
|
+
|
1506
|
+
If two container instances share at least one deployment group, they should
|
1507
|
+
be able to have relationships even if they belong to other groups as well.
|
1508
|
+
"""
|
1509
|
+
|
1510
|
+
with Workspace("w", scope=None) as w:
|
1511
|
+
with SoftwareSystem("Software System") as software_system:
|
1512
|
+
frontend = Container("Frontend")
|
1513
|
+
backend = Container("Backend")
|
1514
|
+
frontend >> "Calls" >> backend
|
1515
|
+
|
1516
|
+
with DeploymentEnvironment("Production") as production:
|
1517
|
+
group_a = DeploymentGroup("Group A")
|
1518
|
+
group_b = DeploymentGroup("Group B")
|
1519
|
+
group_shared = DeploymentGroup("Shared Group")
|
1520
|
+
|
1521
|
+
with DeploymentNode("Server 1"):
|
1522
|
+
# Frontend in Group A and Shared Group
|
1523
|
+
frontend_instance = ContainerInstance(frontend, [group_a, group_shared])
|
1524
|
+
|
1525
|
+
with DeploymentNode("Server 2"):
|
1526
|
+
# Backend in Group B and Shared Group
|
1527
|
+
backend_instance = ContainerInstance(backend, [group_b, group_shared])
|
1528
|
+
|
1529
|
+
# Verify deployment group assignments
|
1530
|
+
assert set(frontend_instance.model.deploymentGroups) == {"Group A", "Shared Group"}
|
1531
|
+
assert set(backend_instance.model.deploymentGroups) == {"Group B", "Shared Group"}
|
1532
|
+
|
1533
|
+
# Frontend and backend share "Shared Group", so relationship should exist
|
1534
|
+
frontend_relationships = [
|
1535
|
+
r for r in (frontend_instance.model.relationships or [])
|
1536
|
+
if r.description == "Calls"
|
1537
|
+
]
|
1538
|
+
assert len(frontend_relationships) == 1, f"Expected 1 relationship, found {len(frontend_relationships)}"
|
1539
|
+
assert frontend_relationships[0].destinationId == backend_instance.model.id
|
1540
|
+
|
1541
|
+
def test_container_instance_relationships_with_no_deployment_groups() -> Optional[None]:
|
1542
|
+
"""
|
1543
|
+
Test that container instances without deployment groups can still have
|
1544
|
+
relationships (backward compatibility).
|
1545
|
+
|
1546
|
+
When no deployment groups are specified, all instances should be able to
|
1547
|
+
relate to each other as before.
|
1548
|
+
"""
|
1549
|
+
|
1550
|
+
with Workspace("w", scope=None) as w:
|
1551
|
+
with SoftwareSystem("Software System") as software_system:
|
1552
|
+
service_a = Container("Service A")
|
1553
|
+
service_b = Container("Service B")
|
1554
|
+
service_a >> "Communicates with" >> service_b
|
1555
|
+
|
1556
|
+
with DeploymentEnvironment("Production") as production:
|
1557
|
+
with DeploymentNode("Server 1"):
|
1558
|
+
# No deployment groups specified
|
1559
|
+
service_a_instance_1 = ContainerInstance(service_a)
|
1560
|
+
service_b_instance_1 = ContainerInstance(service_b)
|
1561
|
+
|
1562
|
+
with DeploymentNode("Server 2"):
|
1563
|
+
# No deployment groups specified
|
1564
|
+
service_a_instance_2 = ContainerInstance(service_a)
|
1565
|
+
service_b_instance_2 = ContainerInstance(service_b)
|
1566
|
+
|
1567
|
+
# When no deployment groups are specified, instances should be able to
|
1568
|
+
# relate to each other (all instances are in the "default" group)
|
1569
|
+
# This maintains backward compatibility
|
1570
|
+
|
1571
|
+
# Each service_a instance should relate to ALL service_b instances
|
1572
|
+
service_a_1_rels = [
|
1573
|
+
r for r in (service_a_instance_1.model.relationships or [])
|
1574
|
+
if r.description == "Communicates with"
|
1575
|
+
]
|
1576
|
+
service_a_2_rels = [
|
1577
|
+
r for r in (service_a_instance_2.model.relationships or [])
|
1578
|
+
if r.description == "Communicates with"
|
1579
|
+
]
|
1580
|
+
|
1581
|
+
# When no groups specified, all instances should connect to all other instances
|
1582
|
+
assert len(service_a_1_rels) == 2, f"Expected 2 relationships (to both service_b instances), found {len(service_a_1_rels)}"
|
1583
|
+
assert len(service_a_2_rels) == 2, f"Expected 2 relationships (to both service_b instances), found {len(service_a_2_rels)}"
|
1584
|
+
|
1585
|
+
# Verify destinations
|
1586
|
+
service_a_1_destinations = {r.destinationId for r in service_a_1_rels}
|
1587
|
+
service_a_2_destinations = {r.destinationId for r in service_a_2_rels}
|
1588
|
+
|
1589
|
+
assert service_b_instance_1.model.id in service_a_1_destinations
|
1590
|
+
assert service_b_instance_2.model.id in service_a_1_destinations
|
1591
|
+
assert service_b_instance_1.model.id in service_a_2_destinations
|
1592
|
+
assert service_b_instance_2.model.id in service_a_2_destinations
|
@@ -1 +0,0 @@
|
|
1
|
-
VERSION = "0.0.18"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|