infrahub-server 1.2.11__py3-none-any.whl → 1.3.0__py3-none-any.whl

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.
Files changed (211) hide show
  1. infrahub/actions/constants.py +130 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +243 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +393 -0
  6. infrahub/actions/tasks.py +119 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/branch/__init__.py +0 -0
  9. infrahub/branch/tasks.py +29 -0
  10. infrahub/branch/triggers.py +22 -0
  11. infrahub/cli/db.py +3 -4
  12. infrahub/computed_attribute/gather.py +3 -1
  13. infrahub/computed_attribute/tasks.py +23 -29
  14. infrahub/core/account.py +24 -47
  15. infrahub/core/attribute.py +13 -15
  16. infrahub/core/constants/__init__.py +10 -0
  17. infrahub/core/constants/database.py +1 -0
  18. infrahub/core/constants/infrahubkind.py +9 -0
  19. infrahub/core/constraint/node/runner.py +3 -1
  20. infrahub/core/convert_object_type/__init__.py +0 -0
  21. infrahub/core/convert_object_type/conversion.py +124 -0
  22. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  23. infrahub/core/diff/coordinator.py +8 -1
  24. infrahub/core/diff/query/all_conflicts.py +1 -5
  25. infrahub/core/diff/query/artifact.py +10 -20
  26. infrahub/core/diff/query/delete_query.py +8 -4
  27. infrahub/core/diff/query/diff_get.py +3 -6
  28. infrahub/core/diff/query/field_specifiers.py +1 -1
  29. infrahub/core/diff/query/field_summary.py +2 -4
  30. infrahub/core/diff/query/merge.py +72 -125
  31. infrahub/core/diff/query/save.py +83 -68
  32. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  33. infrahub/core/diff/query/time_range_query.py +0 -1
  34. infrahub/core/diff/repository/repository.py +4 -0
  35. infrahub/core/graph/__init__.py +1 -1
  36. infrahub/core/manager.py +14 -11
  37. infrahub/core/migrations/graph/__init__.py +6 -0
  38. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  39. infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
  40. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -6
  41. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  42. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  43. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  44. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  45. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  46. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
  47. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  48. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  49. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +662 -0
  50. infrahub/core/migrations/graph/m030_illegal_edges.py +82 -0
  51. infrahub/core/migrations/query/attribute_add.py +14 -11
  52. infrahub/core/migrations/query/attribute_rename.py +6 -11
  53. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  54. infrahub/core/migrations/query/node_duplicate.py +19 -21
  55. infrahub/core/migrations/query/relationship_duplicate.py +19 -18
  56. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  57. infrahub/core/migrations/schema/node_remove.py +19 -20
  58. infrahub/core/models.py +29 -2
  59. infrahub/core/node/__init__.py +131 -28
  60. infrahub/core/node/base.py +1 -1
  61. infrahub/core/node/create.py +211 -0
  62. infrahub/core/node/resource_manager/number_pool.py +31 -5
  63. infrahub/core/node/standard.py +6 -1
  64. infrahub/core/path.py +15 -1
  65. infrahub/core/protocols.py +57 -0
  66. infrahub/core/protocols_base.py +3 -0
  67. infrahub/core/query/__init__.py +2 -2
  68. infrahub/core/query/delete.py +3 -3
  69. infrahub/core/query/diff.py +19 -32
  70. infrahub/core/query/ipam.py +10 -20
  71. infrahub/core/query/node.py +29 -47
  72. infrahub/core/query/relationship.py +55 -34
  73. infrahub/core/query/resource_manager.py +1 -2
  74. infrahub/core/query/standard_node.py +19 -5
  75. infrahub/core/query/subquery.py +2 -4
  76. infrahub/core/relationship/constraints/count.py +10 -9
  77. infrahub/core/relationship/constraints/interface.py +2 -1
  78. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  79. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  80. infrahub/core/relationship/constraints/peer_relatives.py +72 -0
  81. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  82. infrahub/core/relationship/model.py +4 -1
  83. infrahub/core/schema/__init__.py +2 -1
  84. infrahub/core/schema/attribute_parameters.py +160 -0
  85. infrahub/core/schema/attribute_schema.py +130 -7
  86. infrahub/core/schema/basenode_schema.py +27 -3
  87. infrahub/core/schema/definitions/core/__init__.py +29 -1
  88. infrahub/core/schema/definitions/core/group.py +45 -0
  89. infrahub/core/schema/definitions/core/resource_pool.py +9 -0
  90. infrahub/core/schema/definitions/internal.py +43 -5
  91. infrahub/core/schema/generated/attribute_schema.py +16 -3
  92. infrahub/core/schema/generated/relationship_schema.py +11 -1
  93. infrahub/core/schema/manager.py +7 -2
  94. infrahub/core/schema/schema_branch.py +109 -12
  95. infrahub/core/validators/__init__.py +15 -2
  96. infrahub/core/validators/attribute/choices.py +1 -3
  97. infrahub/core/validators/attribute/enum.py +1 -3
  98. infrahub/core/validators/attribute/kind.py +1 -3
  99. infrahub/core/validators/attribute/length.py +13 -7
  100. infrahub/core/validators/attribute/min_max.py +118 -0
  101. infrahub/core/validators/attribute/number_pool.py +106 -0
  102. infrahub/core/validators/attribute/optional.py +1 -4
  103. infrahub/core/validators/attribute/regex.py +5 -6
  104. infrahub/core/validators/attribute/unique.py +1 -3
  105. infrahub/core/validators/determiner.py +18 -2
  106. infrahub/core/validators/enum.py +12 -0
  107. infrahub/core/validators/node/hierarchy.py +3 -6
  108. infrahub/core/validators/query.py +1 -3
  109. infrahub/core/validators/relationship/count.py +6 -12
  110. infrahub/core/validators/relationship/optional.py +2 -4
  111. infrahub/core/validators/relationship/peer.py +177 -12
  112. infrahub/core/validators/tasks.py +1 -1
  113. infrahub/core/validators/uniqueness/query.py +5 -9
  114. infrahub/database/__init__.py +12 -4
  115. infrahub/database/validation.py +100 -0
  116. infrahub/dependencies/builder/constraint/grouped/node_runner.py +4 -0
  117. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  118. infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
  119. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  120. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  121. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  122. infrahub/dependencies/registry.py +4 -0
  123. infrahub/events/group_action.py +1 -0
  124. infrahub/events/models.py +1 -1
  125. infrahub/git/base.py +5 -3
  126. infrahub/git/integrator.py +96 -5
  127. infrahub/git/tasks.py +1 -0
  128. infrahub/graphql/analyzer.py +139 -18
  129. infrahub/graphql/manager.py +4 -0
  130. infrahub/graphql/mutations/action.py +164 -0
  131. infrahub/graphql/mutations/convert_object_type.py +71 -0
  132. infrahub/graphql/mutations/main.py +25 -176
  133. infrahub/graphql/mutations/proposed_change.py +20 -17
  134. infrahub/graphql/mutations/relationship.py +32 -0
  135. infrahub/graphql/mutations/resource_manager.py +63 -7
  136. infrahub/graphql/queries/convert_object_type_mapping.py +34 -0
  137. infrahub/graphql/queries/resource_manager.py +7 -1
  138. infrahub/graphql/resolvers/many_relationship.py +1 -1
  139. infrahub/graphql/resolvers/resolver.py +2 -2
  140. infrahub/graphql/resolvers/single_relationship.py +1 -1
  141. infrahub/graphql/schema.py +6 -0
  142. infrahub/menu/menu.py +34 -2
  143. infrahub/message_bus/messages/__init__.py +0 -10
  144. infrahub/message_bus/operations/__init__.py +0 -8
  145. infrahub/message_bus/operations/refresh/registry.py +4 -7
  146. infrahub/patch/queries/delete_duplicated_edges.py +45 -39
  147. infrahub/pools/models.py +14 -0
  148. infrahub/pools/number.py +5 -3
  149. infrahub/pools/registration.py +22 -0
  150. infrahub/pools/tasks.py +126 -0
  151. infrahub/prefect_server/models.py +1 -19
  152. infrahub/proposed_change/models.py +68 -3
  153. infrahub/proposed_change/tasks.py +911 -34
  154. infrahub/schema/__init__.py +0 -0
  155. infrahub/schema/tasks.py +27 -0
  156. infrahub/schema/triggers.py +23 -0
  157. infrahub/task_manager/models.py +10 -6
  158. infrahub/trigger/catalogue.py +6 -0
  159. infrahub/trigger/models.py +23 -6
  160. infrahub/trigger/setup.py +26 -2
  161. infrahub/trigger/tasks.py +4 -2
  162. infrahub/types.py +6 -0
  163. infrahub/webhook/tasks.py +6 -9
  164. infrahub/workflows/catalogue.py +103 -1
  165. infrahub_sdk/client.py +43 -10
  166. infrahub_sdk/ctl/generator.py +4 -4
  167. infrahub_sdk/ctl/repository.py +1 -1
  168. infrahub_sdk/node/__init__.py +39 -0
  169. infrahub_sdk/node/attribute.py +122 -0
  170. infrahub_sdk/node/constants.py +21 -0
  171. infrahub_sdk/{node.py → node/node.py} +158 -803
  172. infrahub_sdk/node/parsers.py +15 -0
  173. infrahub_sdk/node/property.py +24 -0
  174. infrahub_sdk/node/related_node.py +266 -0
  175. infrahub_sdk/node/relationship.py +302 -0
  176. infrahub_sdk/protocols.py +112 -0
  177. infrahub_sdk/protocols_base.py +34 -2
  178. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  179. infrahub_sdk/query_groups.py +17 -5
  180. infrahub_sdk/schema/main.py +1 -0
  181. infrahub_sdk/schema/repository.py +16 -0
  182. infrahub_sdk/spec/object.py +1 -1
  183. infrahub_sdk/store.py +1 -1
  184. infrahub_sdk/testing/schemas/car_person.py +1 -0
  185. infrahub_sdk/utils.py +7 -20
  186. infrahub_sdk/yaml.py +6 -5
  187. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/METADATA +5 -5
  188. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/RECORD +197 -168
  189. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/WHEEL +1 -1
  190. infrahub_testcontainers/container.py +239 -65
  191. infrahub_testcontainers/docker-compose-cluster.test.yml +321 -0
  192. infrahub_testcontainers/docker-compose.test.yml +2 -1
  193. infrahub_testcontainers/helpers.py +23 -3
  194. infrahub_testcontainers/plugin.py +9 -0
  195. infrahub/message_bus/messages/check_generator_run.py +0 -26
  196. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  197. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  198. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  199. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  200. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  201. infrahub/message_bus/operations/check/__init__.py +0 -3
  202. infrahub/message_bus/operations/check/generator.py +0 -156
  203. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  204. infrahub/message_bus/operations/finalize/validator.py +0 -133
  205. infrahub/message_bus/operations/requests/__init__.py +0 -9
  206. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  207. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  208. infrahub/patch/queries/consolidate_duplicated_nodes.py +0 -109
  209. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  210. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/LICENSE.txt +0 -0
  211. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -2,17 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
- from infrahub.core.constants import PathType
5
+ from infrahub import config
6
+ from infrahub.core.constants import PathType, RelationshipKind
6
7
  from infrahub.core.path import DataPath, GroupedDataPaths
7
8
  from infrahub.core.schema import GenericSchema
8
9
 
9
10
  from ..interface import ConstraintCheckerInterface
10
- from ..shared import (
11
- RelationshipSchemaValidatorQuery,
12
- )
11
+ from ..shared import RelationshipSchemaValidatorQuery
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from infrahub.core.branch import Branch
15
+ from infrahub.core.schema.relationship_schema import RelationshipSchema
16
16
  from infrahub.database import InfrahubDatabase
17
17
 
18
18
  from ..model import SchemaConstraintValidatorRequest
@@ -36,8 +36,7 @@ class RelationshipPeerUpdateValidatorQuery(RelationshipSchemaValidatorQuery):
36
36
  # ruff: noqa: E501
37
37
  query = """
38
38
  MATCH (n:%(node_kind)s)
39
- CALL {
40
- WITH n
39
+ CALL (n) {
41
40
  MATCH path = (root:Root)<-[rroot:IS_PART_OF]-(n)
42
41
  WHERE all(r in relationships(path) WHERE %(branch_filter)s)
43
42
  RETURN path as full_path, n as active_node
@@ -45,10 +44,8 @@ class RelationshipPeerUpdateValidatorQuery(RelationshipSchemaValidatorQuery):
45
44
  LIMIT 1
46
45
  }
47
46
  WITH full_path, active_node
48
- WITH full_path, active_node
49
47
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
50
- CALL {
51
- WITH active_node
48
+ CALL (active_node) {
52
49
  MATCH path = (active_node)-[rrel1:IS_RELATED]-(rel:Relationship { name: $relationship_id })-[rrel2:IS_RELATED]-(peer:Node)
53
50
  WHERE all(
54
51
  r in relationships(path)
@@ -68,8 +65,7 @@ class RelationshipPeerUpdateValidatorQuery(RelationshipSchemaValidatorQuery):
68
65
  collect([branch_level_sum, from_times, active_relationship_count, relationship_path, deepest_branch_name]) as enriched_paths,
69
66
  start_node,
70
67
  peer_node
71
- CALL {
72
- WITH enriched_paths, peer_node
68
+ CALL (enriched_paths, peer_node) {
73
69
  UNWIND enriched_paths as path_to_check
74
70
  RETURN path_to_check[3] as current_path, path_to_check[4] as branch_name, peer_node as current_peer
75
71
  ORDER BY
@@ -80,7 +76,6 @@ class RelationshipPeerUpdateValidatorQuery(RelationshipSchemaValidatorQuery):
80
76
  LIMIT 1
81
77
  }
82
78
  WITH start_node, current_peer, branch_name, current_path
83
- WITH start_node, current_peer, branch_name, current_path
84
79
  WHERE all(r in relationships(current_path) WHERE r.status = "active")
85
80
  AND NOT any(label IN LABELS(current_peer) WHERE label IN $allowed_peer_kinds)
86
81
  """ % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}
@@ -130,3 +125,173 @@ class RelationshipPeerChecker(ConstraintCheckerInterface):
130
125
  await query.execute(db=self.db)
131
126
  grouped_data_paths_list.append(await query.get_paths())
132
127
  return grouped_data_paths_list
128
+
129
+
130
+ class RelationshipPeerParentValidatorQuery(RelationshipSchemaValidatorQuery):
131
+ name = "relationship_constraints_peer_parent_validator"
132
+
133
+ def __init__(
134
+ self,
135
+ relationship: RelationshipSchema,
136
+ parent_relationship: RelationshipSchema,
137
+ peer_parent_relationship: RelationshipSchema,
138
+ **kwargs: Any,
139
+ ):
140
+ super().__init__(**kwargs)
141
+
142
+ self.relationship = relationship
143
+ self.parent_relationship = parent_relationship
144
+ self.peer_parent_relationship = peer_parent_relationship
145
+
146
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
147
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string(), is_isolated=False)
148
+ self.params.update(branch_params)
149
+ self.params["peer_relationship_id"] = self.relationship.identifier
150
+ self.params["parent_relationship_id"] = self.parent_relationship.identifier
151
+ self.params["peer_parent_relationship_id"] = self.peer_parent_relationship.identifier
152
+
153
+ parent_arrows = self.parent_relationship.get_query_arrows()
154
+ parent_match = (
155
+ "MATCH (active_node)%(lstart)s[r1:IS_RELATED]%(lend)s"
156
+ "(rel:Relationship { name: $parent_relationship_id })%(rstart)s[r2:IS_RELATED]%(rend)s(parent:Node)"
157
+ ) % {
158
+ "lstart": parent_arrows.left.start,
159
+ "lend": parent_arrows.left.end,
160
+ "rstart": parent_arrows.right.start,
161
+ "rend": parent_arrows.right.end,
162
+ }
163
+
164
+ peer_parent_arrows = self.relationship.get_query_arrows()
165
+ peer_match = (
166
+ "MATCH (active_node)%(lstart)s[r1:IS_RELATED]%(lend)s"
167
+ "(r:Relationship {name: $peer_relationship_id })%(rstart)s[r2:IS_RELATED]%(rend)s(peer:Node)"
168
+ ) % {
169
+ "lstart": peer_parent_arrows.left.start,
170
+ "lend": peer_parent_arrows.left.end,
171
+ "rstart": peer_parent_arrows.right.start,
172
+ "rend": peer_parent_arrows.right.end,
173
+ }
174
+
175
+ peer_parent_arrows = self.peer_parent_relationship.get_query_arrows()
176
+ peer_parent_match = (
177
+ "MATCH (peer:Node)%(lstart)s[r1:IS_RELATED]%(lend)s"
178
+ "(r:Relationship {name: $peer_parent_relationship_id})%(rstart)s[r2:IS_RELATED]%(rend)s(peer_parent:Node)"
179
+ ) % {
180
+ "lstart": peer_parent_arrows.left.start,
181
+ "lend": peer_parent_arrows.left.end,
182
+ "rstart": peer_parent_arrows.right.start,
183
+ "rend": peer_parent_arrows.right.end,
184
+ }
185
+
186
+ query = """
187
+ MATCH (n:%(node_kind)s)
188
+ CALL (n) {
189
+ MATCH path = (root:Root)<-[r:IS_PART_OF]-(n)
190
+ WHERE %(branch_filter)s
191
+ RETURN n as active_node, r.status = "active" AS is_active
192
+ ORDER BY r.branch_level DESC, r.from DESC
193
+ LIMIT 1
194
+ }
195
+ WITH active_node, is_active
196
+ WHERE is_active = TRUE
197
+ %(parent_match)s
198
+ WHERE all(r in [r1, r2] WHERE %(branch_filter)s AND r.status = "active")
199
+ CALL (active_node) {
200
+ %(peer_match)s
201
+ WITH DISTINCT active_node, peer
202
+ %(peer_match)s
203
+ WHERE all(r in [r1, r2] WHERE %(branch_filter)s)
204
+ WITH peer, r1.status = "active" AND r2.status = "active" AS is_active
205
+ ORDER BY peer.uuid, r1.branch_level DESC, r2.branch_level DESC, r1.from DESC, r2.from DESC, is_active DESC
206
+ WITH peer, head(collect(is_active)) AS is_active
207
+ WHERE is_active = TRUE
208
+ RETURN peer
209
+ }
210
+ CALL (peer) {
211
+ %(peer_parent_match)s
212
+ WHERE all(r IN [r1, r2] WHERE %(branch_filter)s)
213
+ WITH peer_parent, r1, r2, r1.status = "active" AND r2.status = "active" AS is_active
214
+ WITH peer_parent, r1.branch AS branch_name, is_active
215
+ ORDER BY r1.branch_level DESC, r2.branch_level DESC, r1.from DESC, r2.from DESC, is_active DESC
216
+ LIMIT 1
217
+ WITH peer_parent, branch_name
218
+ WHERE is_active = TRUE
219
+ RETURN peer_parent, branch_name
220
+ }
221
+ WITH DISTINCT active_node, parent, peer, peer_parent, branch_name
222
+ WHERE parent.uuid <> peer_parent.uuid
223
+ """ % {
224
+ "branch_filter": branch_filter,
225
+ "node_kind": self.node_schema.kind,
226
+ "parent_match": parent_match,
227
+ "peer_match": peer_match,
228
+ "peer_parent_match": peer_parent_match,
229
+ }
230
+
231
+ self.add_to_query(query)
232
+ self.return_labels = ["active_node.uuid", "parent.uuid", "peer.uuid", "peer_parent.uuid", "branch_name"]
233
+
234
+ async def get_paths(self) -> GroupedDataPaths:
235
+ grouped_data_paths = GroupedDataPaths()
236
+
237
+ for result in self.results:
238
+ grouped_data_paths.add_data_path(
239
+ DataPath(
240
+ branch=str(result.get("branch_name")),
241
+ path_type=PathType.RELATIONSHIP_ONE,
242
+ node_id=str(result.get("peer.uuid")),
243
+ field_name=self.peer_parent_relationship.name,
244
+ peer_id=str(result.get("peer_parent.uuid")),
245
+ kind=self.relationship.peer,
246
+ )
247
+ )
248
+
249
+ return grouped_data_paths
250
+
251
+
252
+ class RelationshipPeerParentChecker(ConstraintCheckerInterface):
253
+ query_classes = [RelationshipPeerParentValidatorQuery]
254
+
255
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
256
+ self.db = db
257
+ self.branch = branch
258
+
259
+ @property
260
+ def name(self) -> str:
261
+ return "relationship.common_parent.update"
262
+
263
+ def supports(self, request: SchemaConstraintValidatorRequest) -> bool:
264
+ return request.constraint_name == self.name and config.SETTINGS.main.schema_strict_mode
265
+
266
+ async def check(self, request: SchemaConstraintValidatorRequest) -> list[GroupedDataPaths]:
267
+ grouped_data_paths_list: list[GroupedDataPaths] = []
268
+
269
+ if not request.schema_path.field_name:
270
+ return grouped_data_paths_list
271
+
272
+ relationship = request.node_schema.get_relationship(name=request.schema_path.field_name)
273
+ if not relationship.common_parent:
274
+ # Should not happen if schema validation was done properly
275
+ return grouped_data_paths_list
276
+
277
+ parent_relationship = next(
278
+ iter(request.node_schema.get_relationships_of_kind(relationship_kinds=[RelationshipKind.PARENT]))
279
+ )
280
+ peer_parent_relationship = request.schema_branch.get(name=relationship.peer, duplicate=False).get_relationship(
281
+ name=relationship.common_parent
282
+ )
283
+
284
+ for query_class in self.query_classes:
285
+ query = await query_class.init(
286
+ db=self.db,
287
+ branch=self.branch,
288
+ node_schema=request.node_schema,
289
+ schema_path=request.schema_path,
290
+ relationship=relationship,
291
+ parent_relationship=parent_relationship,
292
+ peer_parent_relationship=peer_parent_relationship,
293
+ )
294
+ await query.execute(db=self.db)
295
+ grouped_data_paths_list.append(await query.get_paths())
296
+
297
+ return grouped_data_paths_list
@@ -9,7 +9,7 @@ from prefect.logging import get_run_logger
9
9
 
10
10
  from infrahub.core.branch import Branch # noqa: TC001
11
11
  from infrahub.core.path import SchemaPath # noqa: TC001
12
- from infrahub.core.schema import GenericSchema, NodeSchema # noqa: TC001
12
+ from infrahub.core.schema import GenericSchema, NodeSchema
13
13
  from infrahub.core.validators.aggregated_checker import AggregatedConstraintChecker
14
14
  from infrahub.core.validators.model import (
15
15
  SchemaConstraintValidatorRequest,
@@ -158,12 +158,11 @@ class NodeUniqueAttributeConstraintQuery(Query):
158
158
  # ruff: noqa: E501
159
159
  query = """
160
160
  // get attributes for node and its relationships
161
- CALL {
161
+ CALL () {
162
162
  %(select_subqueries_str)s
163
163
  }
164
- CALL {
164
+ CALL (potential_path) {
165
165
  WITH potential_path
166
- WITH potential_path // workaround for neo4j not allowing WHERE in a WITH of a subquery
167
166
  // only the branches and times we care about
168
167
  WHERE all(
169
168
  r IN relationships(potential_path) WHERE (
@@ -183,8 +182,7 @@ class NodeUniqueAttributeConstraintQuery(Query):
183
182
  start_node,
184
183
  rel_identifier,
185
184
  potential_attr
186
- CALL {
187
- WITH enriched_paths
185
+ CALL (enriched_paths) {
188
186
  UNWIND enriched_paths as path_to_check
189
187
  RETURN path_to_check[0] as current_path, path_to_check[4] as latest_value
190
188
  ORDER BY
@@ -194,16 +192,14 @@ class NodeUniqueAttributeConstraintQuery(Query):
194
192
  path_to_check[3] DESC
195
193
  LIMIT 1
196
194
  }
197
- CALL {
195
+ CALL (current_path) {
198
196
  // only active paths
199
197
  WITH current_path
200
- WITH current_path // workaround for neo4j not allowing WHERE in a WITH of a subquery
201
198
  WHERE all(r IN relationships(current_path) WHERE r.status = "active")
202
199
  RETURN current_path as active_path
203
200
  }
204
- CALL {
201
+ CALL (active_path) {
205
202
  // get deepest branch name
206
- WITH active_path
207
203
  UNWIND %(branch_name_and_level)s as branch_name_and_level
208
204
  RETURN branch_name_and_level[0] as branch_name
209
205
  ORDER BY branch_name_and_level[1] DESC
@@ -39,7 +39,7 @@ if TYPE_CHECKING:
39
39
  from types import TracebackType
40
40
 
41
41
  from infrahub.core.branch import Branch
42
- from infrahub.core.schema import MainSchemaTypes, NodeSchema
42
+ from infrahub.core.schema import GenericSchema, MainSchemaTypes, NodeSchema
43
43
  from infrahub.core.schema.schema_branch import SchemaBranch
44
44
 
45
45
  validated_database = {}
@@ -91,6 +91,15 @@ class DatabaseSchemaManager:
91
91
 
92
92
  raise ValueError("The selected node is not of type NodeSchema")
93
93
 
94
+ def get_generic_schema(
95
+ self, name: str, branch: Branch | str | None = None, duplicate: bool = True
96
+ ) -> GenericSchema:
97
+ schema = self.get(name=name, branch=branch, duplicate=duplicate)
98
+ if schema.is_generic_schema:
99
+ return schema
100
+
101
+ raise ValueError("The selected node is not of type GenericSchema")
102
+
94
103
  def set(self, name: str, schema: MainSchemaTypes, branch: str | None = None) -> int:
95
104
  branch_name = get_branch_name(branch=branch)
96
105
  if branch_name not in self._db._schemas:
@@ -284,7 +293,7 @@ class InfrahubDatabase:
284
293
  exc_type: type[BaseException] | None,
285
294
  exc_value: BaseException | None,
286
295
  traceback: TracebackType | None,
287
- ):
296
+ ) -> None:
288
297
  if self._mode == InfrahubDatabaseMode.SESSION:
289
298
  return await self._session.close()
290
299
 
@@ -330,7 +339,7 @@ class InfrahubDatabase:
330
339
  CONNECTION_POOL_USAGE.labels(self._driver._pool.address).set(float(connpool_usage))
331
340
 
332
341
  if config.SETTINGS.database.max_concurrent_queries:
333
- while connpool_usage > config.SETTINGS.database.max_concurrent_queries: # noqa: ASYNC110
342
+ while connpool_usage > config.SETTINGS.database.max_concurrent_queries:
334
343
  await asyncio.sleep(config.SETTINGS.database.max_concurrent_queries_delay)
335
344
  connpool_usage = self._driver._pool.in_use_connection_count(self._driver._pool.address)
336
345
 
@@ -489,7 +498,6 @@ async def get_db(retry: int = 0) -> AsyncDriver:
489
498
  trusted_certificates=trusted_certificates,
490
499
  notifications_disabled_categories=[
491
500
  NotificationDisabledCategory.UNRECOGNIZED,
492
- NotificationDisabledCategory.DEPRECATION, # TODO: Remove me with 1.3
493
501
  ],
494
502
  notifications_min_severity=NotificationMinimumSeverity.WARNING,
495
503
  )
@@ -0,0 +1,100 @@
1
+ from infrahub.database import InfrahubDatabase
2
+
3
+
4
+ async def verify_no_duplicate_relationships(db: InfrahubDatabase) -> None:
5
+ """
6
+ Verify that no duplicate active relationships exist at the database level
7
+ A duplicate is defined as
8
+ - connecting the same two nodes
9
+ - having the same identifier
10
+ - having the same direction (inbound, outbound, bidirectional)
11
+ - having the same branch
12
+ A more thorough check that no duplicates exist at any point in time is possible, but more complex
13
+ """
14
+ query = """
15
+ MATCH (a:Node)-[e1:IS_RELATED {status: "active"}]-(rel:Relationship)-[e2:IS_RELATED {branch: e1.branch, status: "active"}]-(b:Node)
16
+ WHERE a.uuid <> b.uuid
17
+ AND e1.to IS NULL
18
+ AND e2.to IS NULL
19
+ WITH a, rel.name AS rel_name, b, e1.branch AS branch, CASE
20
+ WHEN startNode(e1) = a AND startNode(e2) = rel THEN "out"
21
+ WHEN startNode(e1) = rel AND startNode(e2) = b THEN "in"
22
+ ELSE "bidir"
23
+ END AS direction, COUNT(*) AS num_duplicates
24
+ WHERE num_duplicates > 1
25
+ RETURN a.uuid AS node_id1, b.uuid AS node_id2, rel_name, branch, direction, num_duplicates
26
+ """
27
+ results = await db.execute_query(query=query)
28
+ for result in results:
29
+ node_id1 = result.get("node_id1")
30
+ node_id2 = result.get("node_id2")
31
+ rel_name = result.get("rel_name")
32
+ branch = result.get("branch")
33
+ direction = result.get("direction")
34
+ num_duplicates = result.get("num_duplicates")
35
+ raise ValueError(
36
+ f"{num_duplicates} duplicate relationships ({branch=},{direction=}) between nodes '{node_id1}' and '{node_id2}'"
37
+ f" with relationship name '{rel_name}'"
38
+ )
39
+
40
+
41
+ async def verify_no_edges_added_after_node_delete(db: InfrahubDatabase) -> None:
42
+ """
43
+ Verify that no edges are added to a Node after it is deleted on a given branch
44
+ """
45
+ query = """
46
+ // ------------
47
+ // find deleted nodes
48
+ // ------------
49
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
50
+ WHERE e.status = "deleted" OR e.to IS NOT NULL
51
+ WITH DISTINCT n, e.branch AS delete_branch, e.branch_level AS delete_branch_level, CASE
52
+ WHEN e.status = "deleted" THEN e.from
53
+ ELSE e.to
54
+ END AS delete_time
55
+ // ------------
56
+ // find the edges added to the deleted node after the delete time
57
+ // ------------
58
+ MATCH (n)-[added_e]-(peer)
59
+ WHERE added_e.from > delete_time
60
+ AND type(added_e) <> "IS_PART_OF"
61
+ // if the node was deleted on a branch (delete_branch_level > 1), and then updated on main/global (added_e.branch_level = 1), we can ignore it
62
+ AND added_e.branch_level >= delete_branch_level
63
+ AND (added_e.branch = delete_branch OR delete_branch_level = 1)
64
+ WITH DISTINCT n, delete_branch, delete_time, added_e, peer AS added_peer
65
+ // ------------
66
+ // get the branched_from for the branch on which the node was deleted
67
+ // ------------
68
+ CALL (added_e) {
69
+ MATCH (b:Branch {name: added_e.branch})
70
+ RETURN b.branched_from AS added_e_branched_from
71
+ }
72
+ // ------------
73
+ // account for the following situations, given that the edge update time is after the node delete time
74
+ // - deleted on main/global, updated on branch
75
+ // - illegal if the delete is before branch.branched_from
76
+ // - deleted on branch, updated on branch
77
+ // - illegal
78
+ // ------------
79
+ WITH n, delete_branch, delete_time, added_e, added_peer
80
+ WHERE delete_branch = added_e.branch
81
+ OR delete_time < added_e_branched_from
82
+ RETURN n.uuid AS n_uuid, delete_branch, delete_time, added_e, added_peer
83
+ """
84
+ results = await db.execute_query(query=query)
85
+ error_messages = []
86
+ for result in results:
87
+ n_uuid = result.get("n_uuid")
88
+ delete_branch = result.get("delete_branch")
89
+ delete_time = result.get("delete_time")
90
+ added_e = result.get("added_e")
91
+ added_e_branch = added_e.get("branch")
92
+ added_e_from = added_e.get("from")
93
+ added_peer = result.get("added_peer")
94
+ message = (
95
+ f"Node {n_uuid} was deleted on {delete_branch} at {delete_time} but has an {added_e.type} edge added on"
96
+ f" branch {added_e_branch} at {added_e_from} to {added_peer.element_id}"
97
+ )
98
+ error_messages.append(message)
99
+ if error_messages:
100
+ raise ValueError(error_messages)
@@ -4,6 +4,8 @@ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilder
4
4
  from ..node.grouped_uniqueness import NodeGroupedUniquenessConstraintDependency
5
5
  from ..relationship_manager.count import RelationshipCountConstraintDependency
6
6
  from ..relationship_manager.peer_kind import RelationshipPeerKindConstraintDependency
7
+ from ..relationship_manager.peer_parent import RelationshipPeerParentConstraintDependency
8
+ from ..relationship_manager.peer_relatives import RelationshipPeerRelativesConstraintDependency
7
9
  from ..relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
8
10
 
9
11
 
@@ -18,5 +20,7 @@ class NodeConstraintRunnerDependency(DependencyBuilder[NodeConstraintRunner]):
18
20
  RelationshipPeerKindConstraintDependency.build(context=context),
19
21
  RelationshipCountConstraintDependency.build(context=context),
20
22
  RelationshipProfilesKindConstraintDependency.build(context=context),
23
+ RelationshipPeerParentConstraintDependency.build(context=context),
24
+ RelationshipPeerRelativesConstraintDependency.build(context=context),
21
25
  ],
22
26
  )
@@ -0,0 +1,8 @@
1
+ from infrahub.core.relationship.constraints.peer_parent import RelationshipPeerParentConstraint
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class RelationshipPeerParentConstraintDependency(DependencyBuilder[RelationshipPeerParentConstraint]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> RelationshipPeerParentConstraint:
8
+ return RelationshipPeerParentConstraint(db=context.db, branch=context.branch)
@@ -0,0 +1,8 @@
1
+ from infrahub.core.relationship.constraints.peer_relatives import RelationshipPeerRelativesConstraint
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class RelationshipPeerRelativesConstraintDependency(DependencyBuilder[RelationshipPeerRelativesConstraint]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> RelationshipPeerRelativesConstraint:
8
+ return RelationshipPeerRelativesConstraint(db=context.db, branch=context.branch)
@@ -14,6 +14,7 @@ from .node_attribute import SchemaNodeAttributeAddConstraintDependency
14
14
  from .node_relationship import SchemaNodeRelationshipAddConstraintDependency
15
15
  from .relationship_count import SchemaRelationshipCountConstraintDependency
16
16
  from .relationship_optional import SchemaRelationshipOptionalConstraintDependency
17
+ from .relationship_peer import SchemaRelationshipPeerParentConstraintDependency
17
18
  from .uniqueness import SchemaUniquenessConstraintDependency
18
19
 
19
20
 
@@ -36,6 +37,7 @@ class AggregatedSchemaConstraintsDependency(DependencyBuilder[AggregatedConstrai
36
37
  SchemaAttributeKindConstraintDependency.build(context=context),
37
38
  SchemaNodeAttributeAddConstraintDependency.build(context=context),
38
39
  SchemaNodeRelationshipAddConstraintDependency.build(context=context),
40
+ SchemaRelationshipPeerParentConstraintDependency.build(context=context),
39
41
  ],
40
42
  db=context.db,
41
43
  branch=context.branch,
@@ -0,0 +1,8 @@
1
+ from infrahub.core.validators.relationship.peer import RelationshipPeerParentChecker
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class SchemaRelationshipPeerParentConstraintDependency(DependencyBuilder[RelationshipPeerParentChecker]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> RelationshipPeerParentChecker:
8
+ return RelationshipPeerParentChecker(db=context.db, branch=context.branch)
@@ -6,5 +6,5 @@ from .parent_node_adder import DiffParentNodeAdderDependency
6
6
 
7
7
  class DiffDeserializerDependency(DependencyBuilder[EnrichedDiffDeserializer]):
8
8
  @classmethod
9
- def build(cls, context: DependencyBuilderContext) -> EnrichedDiffDeserializer: # noqa: ARG003
9
+ def build(cls, context: DependencyBuilderContext) -> EnrichedDiffDeserializer:
10
10
  return EnrichedDiffDeserializer(parent_adder=DiffParentNodeAdderDependency.build(context=context))
@@ -3,6 +3,8 @@ from .builder.constraint.node.grouped_uniqueness import NodeGroupedUniquenessCon
3
3
  from .builder.constraint.node.uniqueness import NodeAttributeUniquenessConstraintDependency
4
4
  from .builder.constraint.relationship_manager.count import RelationshipCountConstraintDependency
5
5
  from .builder.constraint.relationship_manager.peer_kind import RelationshipPeerKindConstraintDependency
6
+ from .builder.constraint.relationship_manager.peer_parent import RelationshipPeerParentConstraintDependency
7
+ from .builder.constraint.relationship_manager.peer_relatives import RelationshipPeerRelativesConstraintDependency
6
8
  from .builder.constraint.relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
7
9
  from .builder.constraint.schema.aggregated import AggregatedSchemaConstraintsDependency
8
10
  from .builder.constraint.schema.attribute_regex import SchemaAttributeRegexConstraintDependency
@@ -37,6 +39,8 @@ def build_component_registry() -> ComponentDependencyRegistry:
37
39
  component_registry.track_dependency(RelationshipCountConstraintDependency)
38
40
  component_registry.track_dependency(RelationshipProfilesKindConstraintDependency)
39
41
  component_registry.track_dependency(RelationshipPeerKindConstraintDependency)
42
+ component_registry.track_dependency(RelationshipPeerParentConstraintDependency)
43
+ component_registry.track_dependency(RelationshipPeerRelativesConstraintDependency)
40
44
  component_registry.track_dependency(NodeConstraintRunnerDependency)
41
45
  component_registry.track_dependency(NodeDeleteValidatorDependency)
42
46
  component_registry.track_dependency(IpamKindsGetterDependency)
@@ -89,6 +89,7 @@ class GroupMutatedEvent(InfrahubEvent):
89
89
  "infrahub.node.id": self.node_id,
90
90
  "infrahub.node.action": self.action.value,
91
91
  "infrahub.node.root_id": self.node_id,
92
+ "infrahub.branch.name": self.meta.context.branch.name,
92
93
  }
93
94
 
94
95
 
infrahub/events/models.py CHANGED
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, PrivateAttr, model_validator
8
8
 
9
9
  from infrahub import __version__
10
10
  from infrahub.auth import AccountSession, AuthType
11
- from infrahub.context import InfrahubContext # noqa: TC001
11
+ from infrahub.context import InfrahubContext
12
12
  from infrahub.core.branch import Branch # noqa: TC001
13
13
  from infrahub.message_bus import InfrahubMessage, Meta
14
14
  from infrahub.worker import WORKER_IDENTITY
infrahub/git/base.py CHANGED
@@ -162,6 +162,11 @@ class InfrahubRepositoryBase(BaseModel, ABC):
162
162
  infrahub_branch_name: str | None = Field(None, description="Infrahub branch on which to sync the remote repository")
163
163
  model_config = ConfigDict(arbitrary_types_allowed=True, ignored_types=(Flow, Task))
164
164
 
165
+ def get_client(self) -> InfrahubClient:
166
+ if self.client is None:
167
+ raise ValueError("Client is not set")
168
+ return self.client
169
+
165
170
  @property
166
171
  def sdk(self) -> InfrahubClient:
167
172
  if self.client:
@@ -445,9 +450,6 @@ class InfrahubRepositoryBase(BaseModel, ABC):
445
450
 
446
451
  return [Worktree.init(response) for response in responses]
447
452
 
448
- def get_client(self) -> InfrahubClient:
449
- return self.sdk
450
-
451
453
  def get_location(self) -> str:
452
454
  if self.location:
453
455
  return self.location