infrahub-server 1.3.0b3__py3-none-any.whl → 1.3.0b6__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 (48) hide show
  1. infrahub/actions/constants.py +36 -79
  2. infrahub/actions/schema.py +2 -0
  3. infrahub/core/constraint/node/runner.py +3 -1
  4. infrahub/core/convert_object_type/conversion.py +2 -0
  5. infrahub/core/diff/query/delete_query.py +8 -4
  6. infrahub/core/diff/repository/repository.py +4 -0
  7. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  8. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  9. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  10. infrahub/core/node/__init__.py +65 -36
  11. infrahub/core/path.py +14 -0
  12. infrahub/core/relationship/constraints/count.py +10 -9
  13. infrahub/core/relationship/constraints/interface.py +2 -1
  14. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  15. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  16. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  17. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  18. infrahub/core/schema/definitions/internal.py +8 -1
  19. infrahub/core/schema/generated/relationship_schema.py +6 -1
  20. infrahub/core/schema/schema_branch.py +36 -8
  21. infrahub/core/validators/__init__.py +2 -1
  22. infrahub/core/validators/relationship/peer.py +174 -4
  23. infrahub/database/__init__.py +0 -1
  24. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  25. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  26. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  27. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  28. infrahub/dependencies/registry.py +2 -0
  29. infrahub/git/integrator.py +0 -8
  30. infrahub/git/tasks.py +1 -0
  31. infrahub/graphql/mutations/convert_object_type.py +16 -7
  32. infrahub/graphql/mutations/relationship.py +32 -0
  33. infrahub/graphql/queries/convert_object_type_mapping.py +3 -5
  34. infrahub/message_bus/operations/refresh/registry.py +3 -6
  35. infrahub/pools/models.py +14 -0
  36. infrahub/pools/tasks.py +71 -1
  37. infrahub_sdk/ctl/generator.py +4 -4
  38. infrahub_sdk/ctl/repository.py +1 -1
  39. infrahub_sdk/node/node.py +146 -92
  40. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  41. infrahub_sdk/query_groups.py +4 -3
  42. infrahub_sdk/utils.py +7 -20
  43. infrahub_sdk/yaml.py +6 -5
  44. {infrahub_server-1.3.0b3.dist-info → infrahub_server-1.3.0b6.dist-info}/METADATA +2 -2
  45. {infrahub_server-1.3.0b3.dist-info → infrahub_server-1.3.0b6.dist-info}/RECORD +48 -44
  46. {infrahub_server-1.3.0b3.dist-info → infrahub_server-1.3.0b6.dist-info}/LICENSE.txt +0 -0
  47. {infrahub_server-1.3.0b3.dist-info → infrahub_server-1.3.0b6.dist-info}/WHEEL +0 -0
  48. {infrahub_server-1.3.0b3.dist-info → infrahub_server-1.3.0b6.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import Enum
4
+ from typing import Self
4
5
 
5
6
  from infrahub.core.constants import InfrahubKind
6
7
  from infrahub.core.schema.dropdown import DropdownChoice
@@ -9,165 +10,121 @@ from infrahub.utils import InfrahubStringEnum
9
10
 
10
11
  class NodeAction(InfrahubStringEnum):
11
12
  CREATED = "created"
12
- DELETED = "deleted"
13
13
  UPDATED = "updated"
14
14
 
15
15
 
16
- class BranchScope(Enum):
16
+ class DropdownEnum(Enum):
17
+ @classmethod
18
+ def available_types(cls) -> list[DropdownChoice]:
19
+ return [cls.__members__[member].value for member in list(cls.__members__)]
20
+
21
+ @classmethod
22
+ def from_value(cls, value: str) -> Self:
23
+ for member in cls.__members__:
24
+ if value == cls.__members__[member].value.name:
25
+ return cls.__members__[member]
26
+
27
+ raise NotImplementedError(f"The defined value {value} doesn't match a value of {cls.__class__.__name__}")
28
+
29
+
30
+ class BranchScope(DropdownEnum):
17
31
  ALL_BRANCHES = DropdownChoice(
18
32
  name="all_branches",
19
33
  label="All Branches",
20
34
  description="All branches",
21
- color="#fef08a",
35
+ color="#4cd964",
22
36
  )
23
37
  DEFAULT_BRANCH = DropdownChoice(
24
38
  name="default_branch",
25
39
  label="Default Branch",
26
40
  description="Only the default branch",
27
- color="#86efac",
41
+ color="#5ac8fa",
28
42
  )
29
43
  OTHER_BRANCHES = DropdownChoice(
30
44
  name="other_branches",
31
45
  label="Other Branches",
32
46
  description="All branches except the default branch",
33
- color="#e5e7eb",
47
+ color="#ff2d55",
34
48
  )
35
49
 
36
- @classmethod
37
- def available_types(cls) -> list[DropdownChoice]:
38
- return [cls.__members__[member].value for member in list(cls.__members__)]
39
50
 
40
- @classmethod
41
- def from_value(cls, value: str) -> BranchScope:
42
- for member in cls.__members__:
43
- if value == cls.__members__[member].value.name:
44
- return cls.__members__[member]
45
-
46
- raise NotImplementedError(f"The defined value {value} doesn't match a branch scope")
47
-
48
-
49
- class MemberAction(Enum):
51
+ class MemberAction(DropdownEnum):
50
52
  ADD_MEMBER = DropdownChoice(
51
53
  name="add_member",
52
54
  label="Add member",
53
55
  description="Add impacted member to the selected group",
54
- color="#86efac",
56
+ color="#4cd964",
55
57
  )
56
58
  REMOVE_MEMBER = DropdownChoice(
57
59
  name="remove_member",
58
60
  label="Remove member",
59
61
  description="Remove impacted member from the selected group",
60
- color="#fef08a",
62
+ color="#ff2d55",
61
63
  )
62
64
 
63
- @classmethod
64
- def available_types(cls) -> list[DropdownChoice]:
65
- return [cls.__members__[member].value for member in list(cls.__members__)]
66
-
67
- @classmethod
68
- def from_value(cls, value: str) -> MemberAction:
69
- for member in cls.__members__:
70
- if value == cls.__members__[member].value.name:
71
- return cls.__members__[member]
72
-
73
- raise NotImplementedError(f"The defined value {value} doesn't match a member action")
74
-
75
65
 
76
- class MemberUpdate(Enum):
66
+ class MemberUpdate(DropdownEnum):
77
67
  ADDED = DropdownChoice(
78
68
  name="added",
79
69
  label="Added",
80
70
  description="Trigger when members are added to this group",
81
- color="#86efac",
71
+ color="#4cd964",
82
72
  )
83
73
  REMOVED = DropdownChoice(
84
74
  name="removed",
85
75
  label="Removed",
86
76
  description="Trigger when members are removed from this group",
87
- color="#fef08a",
77
+ color="#ff2d55",
88
78
  )
89
79
 
90
- @classmethod
91
- def available_types(cls) -> list[DropdownChoice]:
92
- return [cls.__members__[member].value for member in list(cls.__members__)]
93
-
94
- @classmethod
95
- def from_value(cls, value: str) -> MemberUpdate:
96
- for member in cls.__members__:
97
- if value == cls.__members__[member].value.name:
98
- return cls.__members__[member]
99
80
 
100
- raise NotImplementedError(f"The defined value {value} doesn't match a MemberUpdate")
101
-
102
-
103
- class RelationshipMatch(Enum):
81
+ class RelationshipMatch(DropdownEnum):
104
82
  ADDED = DropdownChoice(
105
83
  name="added",
106
84
  label="Added",
107
85
  description="Check if the selected relationship was added",
108
- color="#86efac",
86
+ color="#4cd964",
109
87
  )
110
88
  REMOVED = DropdownChoice(
111
89
  name="removed",
112
90
  label="Removed",
113
91
  description="Check if the selected relationship was removed",
114
- color="#fef08a",
92
+ color="#ff2d55",
115
93
  )
116
94
  UPDATED = DropdownChoice(
117
95
  name="updated",
118
96
  label="Updated",
119
97
  description="Check if the selected relationship was updated, added or removed.",
120
- color="#e5e7eb",
98
+ color="#5ac8fa",
121
99
  )
122
100
 
123
- @classmethod
124
- def available_types(cls) -> list[DropdownChoice]:
125
- return [cls.__members__[member].value for member in list(cls.__members__)]
126
-
127
- @classmethod
128
- def from_value(cls, value: str) -> RelationshipMatch:
129
- for member in cls.__members__:
130
- if value == cls.__members__[member].value.name:
131
- return cls.__members__[member]
132
-
133
- raise NotImplementedError(f"The defined value {value} doesn't match a RelationshipMatch")
134
101
 
135
-
136
- class ValueMatch(Enum):
102
+ class ValueMatch(DropdownEnum):
137
103
  VALUE = DropdownChoice(
138
104
  name="value",
139
105
  label="Value",
140
106
  description="Match against the current value",
141
- color="#fef08a",
107
+ color="#4cd964",
142
108
  )
143
109
  VALUE_PREVIOUS = DropdownChoice(
144
110
  name="value_previous",
145
111
  label="Value Previous",
146
112
  description="Match against the previous value",
147
- color="#86efac",
113
+ color="#ff2d55",
148
114
  )
149
115
  VALUE_FULL = DropdownChoice(
150
116
  name="value_full",
151
117
  label="Full value match",
152
118
  description="Match against both the current and previous values",
153
- color="#e5e7eb",
119
+ color="#5ac8fa",
154
120
  )
155
121
 
156
- @classmethod
157
- def available_types(cls) -> list[DropdownChoice]:
158
- return [cls.__members__[member].value for member in list(cls.__members__)]
159
-
160
- @classmethod
161
- def from_value(cls, value: str) -> ValueMatch:
162
- for member in cls.__members__:
163
- if value == cls.__members__[member].value.name:
164
- return cls.__members__[member]
165
-
166
- raise NotImplementedError(f"The defined value {value} doesn't match a ValueMatch")
167
-
168
122
 
169
123
  NODES_THAT_TRIGGER_ACTION_RULES_SETUP = [
124
+ InfrahubKind.GENERATORACTION,
170
125
  InfrahubKind.GROUPACTION,
171
126
  InfrahubKind.GROUPTRIGGERRULE,
172
127
  InfrahubKind.NODETRIGGERRULE,
128
+ InfrahubKind.NODETRIGGERATTRIBUTEMATCH,
129
+ InfrahubKind.NODETRIGGERRELATIONSHIPMATCH,
173
130
  ]
@@ -271,6 +271,7 @@ core_node_trigger_attribute_match = NodeSchema(
271
271
  branch=BranchSupportType.AGNOSTIC,
272
272
  generate_profile=False,
273
273
  inherit_from=["CoreNodeTriggerMatch"],
274
+ display_labels=["attribute_name__value"],
274
275
  attributes=[
275
276
  Attr(
276
277
  name="attribute_name",
@@ -319,6 +320,7 @@ core_node_trigger_relationship_match = NodeSchema(
319
320
  branch=BranchSupportType.AGNOSTIC,
320
321
  generate_profile=False,
321
322
  inherit_from=["CoreNodeTriggerMatch"],
323
+ display_labels=["relationship_name__value", "modification_type__value"],
322
324
  attributes=[
323
325
  Attr(
324
326
  name="relationship_name",
@@ -38,4 +38,6 @@ class NodeConstraintRunner:
38
38
  relationship_manager: RelationshipManager = getattr(node, relationship_name)
39
39
  await relationship_manager.fetch_relationship_ids(db=db, force_refresh=True)
40
40
  for relationship_constraint in self.relationship_manager_constraints:
41
- await relationship_constraint.check(relm=relationship_manager, node_schema=node.get_schema())
41
+ await relationship_constraint.check(
42
+ relm=relationship_manager, node_schema=node.get_schema(), node=node
43
+ )
@@ -101,6 +101,8 @@ async def convert_object_type(
101
101
  deleted_node_out_rels_peer_ids = await get_out_rels_peers_ids(node=node, db=dbt)
102
102
  deleted_node_unidir_rels_peer_ids = await get_unidirectional_rels_peers_ids(node=node, db=dbt, branch=branch)
103
103
 
104
+ # Delete the node, so we delete relationships with peers as well, which might temporarily break cardinality constraints
105
+ # but they should be restored when creating the new node.
104
106
  deleted_nodes = await NodeManager.delete(db=dbt, branch=branch, nodes=[node], cascade_delete=False)
105
107
  if len(deleted_nodes) != 1:
106
108
  raise ValueError(f"Deleted {len(deleted_nodes)} nodes instead of 1")
@@ -9,17 +9,21 @@ class EnrichedDiffDeleteQuery(Query):
9
9
  type = QueryType.WRITE
10
10
  insert_return = False
11
11
 
12
- def __init__(self, enriched_diff_root_uuids: list[str], **kwargs: Any) -> None:
12
+ def __init__(self, enriched_diff_root_uuids: list[str] | None = None, **kwargs: Any) -> None:
13
13
  super().__init__(**kwargs)
14
14
  self.enriched_diff_root_uuids = enriched_diff_root_uuids
15
15
 
16
16
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
17
- self.params = {"diff_root_uuids": self.enriched_diff_root_uuids}
17
+ diff_filter = ""
18
+ if self.enriched_diff_root_uuids:
19
+ self.params = {"diff_root_uuids": self.enriched_diff_root_uuids}
20
+ diff_filter = "WHERE d_root.uuid IN $diff_root_uuids"
21
+
18
22
  query = """
19
23
  MATCH (d_root:DiffRoot)
20
- WHERE d_root.uuid IN $diff_root_uuids
24
+ %(diff_filter)s
21
25
  OPTIONAL MATCH (d_root)-[*]->(diff_thing)
22
26
  DETACH DELETE diff_thing
23
27
  DETACH DELETE d_root
24
- """
28
+ """ % {"diff_filter": diff_filter}
25
29
  self.add_to_query(query=query)
@@ -377,6 +377,10 @@ class DiffRepository:
377
377
  await query.execute(db=self.db)
378
378
  return query.get_summary()
379
379
 
380
+ async def delete_all_diff_roots(self) -> None:
381
+ query = await EnrichedDiffDeleteQuery.init(db=self.db)
382
+ await query.execute(db=self.db)
383
+
380
384
  async def delete_diff_roots(self, diff_root_uuids: list[str]) -> None:
381
385
  query = await EnrichedDiffDeleteQuery.init(db=self.db, enriched_diff_root_uuids=diff_root_uuids)
382
386
  await query.execute(db=self.db)
@@ -31,6 +31,5 @@ class Migration015(ArbitraryMigration):
31
31
  component_registry = get_component_registry()
32
32
  diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
33
33
 
34
- diff_roots = await diff_repo.get_roots_metadata()
35
- await diff_repo.delete_diff_roots(diff_root_uuids=[d.uuid for d in diff_roots])
34
+ await diff_repo.delete_all_diff_roots()
36
35
  return MigrationResult()
@@ -31,6 +31,5 @@ class Migration016(ArbitraryMigration):
31
31
  component_registry = get_component_registry()
32
32
  diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
33
33
 
34
- diff_roots = await diff_repo.get_roots_metadata()
35
- await diff_repo.delete_diff_roots(diff_root_uuids=[d.uuid for d in diff_roots])
34
+ await diff_repo.delete_all_diff_roots()
36
35
  return MigrationResult()
@@ -33,6 +33,5 @@ class Migration028(ArbitraryMigration):
33
33
  component_registry = get_component_registry()
34
34
  diff_repo = await component_registry.get_component(DiffRepository, db=db, branch=default_branch)
35
35
 
36
- diff_roots = await diff_repo.get_roots_metadata()
37
- await diff_repo.delete_diff_roots(diff_root_uuids=[d.uuid for d in diff_roots])
36
+ await diff_repo.delete_all_diff_roots()
38
37
  return MigrationResult()
@@ -7,6 +7,7 @@ from infrahub_sdk.template import Jinja2Template
7
7
  from infrahub_sdk.utils import is_valid_uuid
8
8
  from infrahub_sdk.uuidt import UUIDT
9
9
 
10
+ from infrahub import lock
10
11
  from infrahub.core import registry
11
12
  from infrahub.core.changelog.models import NodeChangelog
12
13
  from infrahub.core.constants import (
@@ -34,6 +35,7 @@ from infrahub.core.schema import (
34
35
  from infrahub.core.schema.attribute_parameters import NumberPoolParameters
35
36
  from infrahub.core.timestamp import Timestamp
36
37
  from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
38
+ from infrahub.pools.models import NumberPoolLockDefinition
37
39
  from infrahub.types import ATTRIBUTE_TYPES
38
40
 
39
41
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
@@ -271,7 +273,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
271
273
  )
272
274
  except NodeNotFoundError:
273
275
  if number_pool_parameters:
274
- number_pool = await self._create_number_pool(
276
+ number_pool = await self._fetch_or_create_number_pool(
275
277
  db=db, attribute=attribute, number_pool_parameters=number_pool_parameters
276
278
  )
277
279
 
@@ -306,35 +308,49 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
306
308
  )
307
309
  )
308
310
 
309
- async def _create_number_pool(
311
+ async def _fetch_or_create_number_pool(
310
312
  self, db: InfrahubDatabase, attribute: BaseAttribute, number_pool_parameters: NumberPoolParameters
311
313
  ) -> CoreNumberPool:
312
- schema = db.schema.get_node_schema(name="CoreNumberPool", duplicate=False)
313
-
314
- pool_node = self._schema.kind
315
- schema_attribute = self._schema.get_attribute(attribute.schema.name)
316
- if schema_attribute.inherited:
317
- for generic_name in self._schema.inherit_from:
318
- generic_node = db.schema.get_generic_schema(name=generic_name, duplicate=False)
319
- if attribute.schema.name in generic_node.attribute_names:
320
- pool_node = generic_node.kind
321
- break
322
-
323
- number_pool = await Node.init(db=db, schema=schema, branch=self._branch)
324
- await number_pool.new(
325
- db=db,
326
- id=number_pool_parameters.number_pool_id,
327
- name=f"{pool_node}.{attribute.schema.name} [{number_pool_parameters.number_pool_id}]",
328
- node=pool_node,
329
- node_attribute=attribute.schema.name,
330
- start_range=number_pool_parameters.start_range,
331
- end_range=number_pool_parameters.end_range,
332
- pool_type=NumberPoolType.SCHEMA.value,
333
- )
334
- await number_pool.save(db=db)
314
+ number_pool_from_db: CoreNumberPool | None = None
315
+ lock_definition = NumberPoolLockDefinition(pool_id=str(number_pool_parameters.number_pool_id))
316
+ async with lock.registry.get(
317
+ name=lock_definition.lock_name, namespace=lock_definition.namespace_name, local=False
318
+ ):
319
+ try:
320
+ number_pool_from_db = await registry.manager.get_one_by_id_or_default_filter(
321
+ db=db, id=str(number_pool_parameters.number_pool_id), kind=CoreNumberPool
322
+ )
323
+ except NodeNotFoundError:
324
+ schema = db.schema.get_node_schema(name="CoreNumberPool", duplicate=False)
325
+
326
+ pool_node = self._schema.kind
327
+ schema_attribute = self._schema.get_attribute(attribute.schema.name)
328
+ if schema_attribute.inherited:
329
+ for generic_name in self._schema.inherit_from:
330
+ generic_node = db.schema.get_generic_schema(name=generic_name, duplicate=False)
331
+ if attribute.schema.name in generic_node.attribute_names:
332
+ pool_node = generic_node.kind
333
+ break
334
+
335
+ number_pool = await Node.init(db=db, schema=schema, branch=self._branch)
336
+ await number_pool.new(
337
+ db=db,
338
+ id=number_pool_parameters.number_pool_id,
339
+ name=f"{pool_node}.{attribute.schema.name} [{number_pool_parameters.number_pool_id}]",
340
+ node=pool_node,
341
+ node_attribute=attribute.schema.name,
342
+ start_range=number_pool_parameters.start_range,
343
+ end_range=number_pool_parameters.end_range,
344
+ pool_type=NumberPoolType.SCHEMA.value,
345
+ )
346
+ await number_pool.save(db=db)
347
+
335
348
  # Do a lookup of the number pool to get the correct mapped type from the registry
336
349
  # without this we don't get access to the .get_resource() method.
337
- return await registry.manager.get_one_by_id_or_default_filter(db=db, id=number_pool.id, kind=CoreNumberPool)
350
+ created_pool: CoreNumberPool = number_pool_from_db or await registry.manager.get_one_by_id_or_default_filter(
351
+ db=db, id=number_pool.id, kind=CoreNumberPool
352
+ )
353
+ return created_pool
338
354
 
339
355
  async def handle_object_template(self, fields: dict, db: InfrahubDatabase, errors: list) -> None:
340
356
  """Fill the `fields` parameters with values from an object template if one is in use."""
@@ -541,17 +557,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
541
557
  relationship_attribute: RelationshipManager = getattr(
542
558
  self, attribute_path.active_relationship_schema.name
543
559
  )
544
- peer = await relationship_attribute.get_peer(db=db, raise_on_error=True)
545
-
546
- related_node = await registry.manager.get_one_by_id_or_default_filter(
547
- db=db, id=peer.id, kind=attribute_path.active_relationship_schema.peer, branch=self._branch.name
548
- )
560
+ if peer := await relationship_attribute.get_peer(db=db, raise_on_error=False):
561
+ related_node = await registry.manager.get_one_by_id_or_default_filter(
562
+ db=db,
563
+ id=peer.id,
564
+ kind=attribute_path.active_relationship_schema.peer,
565
+ branch=self._branch.name,
566
+ )
549
567
 
550
- attribute: BaseAttribute = getattr(
551
- getattr(related_node, attribute_path.active_attribute_schema.name),
552
- attribute_path.active_attribute_property_name,
553
- )
554
- variables[variable] = attribute
568
+ attribute: BaseAttribute = getattr(
569
+ getattr(related_node, attribute_path.active_attribute_schema.name),
570
+ attribute_path.active_attribute_property_name,
571
+ )
572
+ variables[variable] = attribute
573
+ else:
574
+ variables[variable] = None
555
575
 
556
576
  elif attribute_path.is_type_attribute:
557
577
  attribute = getattr(
@@ -962,3 +982,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
962
982
  for name in self._relationships:
963
983
  relm: RelationshipManager = getattr(self, name)
964
984
  relm.validate()
985
+
986
+ async def get_parent_relationship_peer(self, db: InfrahubDatabase, name: str) -> Node | None:
987
+ """When a node has a parent relationship of a given name, this method returns the peer of that relationship."""
988
+ relationship = self.get_schema().get_relationship(name=name)
989
+ if relationship.kind != RelationshipKind.PARENT:
990
+ raise ValueError(f"Relationship '{name}' is not of kind 'parent'")
991
+
992
+ relm: RelationshipManager = getattr(self, name)
993
+ return await relm.get_peer(db=db)
infrahub/core/path.py CHANGED
@@ -63,6 +63,20 @@ class DataPath(InfrahubPath):
63
63
  peer_id: str | None = Field(default=None, description="")
64
64
  value: Any | None = Field(default=None, description="Optional value of the resource")
65
65
 
66
+ def __hash__(self) -> int:
67
+ return hash(
68
+ (
69
+ self.branch,
70
+ self.path_type,
71
+ self.node_id,
72
+ self.kind,
73
+ self.field_name,
74
+ self.property_name,
75
+ self.peer_id,
76
+ str(self.value),
77
+ )
78
+ )
79
+
66
80
  @property
67
81
  def resource_type(self) -> PathResourceType:
68
82
  return PathResourceType.DATA
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from infrahub.core import registry
4
4
  from infrahub.core.branch import Branch
5
5
  from infrahub.core.constants import RelationshipCardinality, RelationshipDirection
6
+ from infrahub.core.node import Node
6
7
  from infrahub.core.query.relationship import RelationshipCountPerNodeQuery
7
8
  from infrahub.core.schema import MainSchemaTypes
8
9
  from infrahub.database import InfrahubDatabase
@@ -25,7 +26,7 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
25
26
  self.db = db
26
27
  self.branch = branch
27
28
 
28
- async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None: # noqa: ARG002
29
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
29
30
  branch = await registry.get_branch(db=self.db) if not self.branch else self.branch
30
31
 
31
32
  # NOTE adding resolve here because we need to retrieve the real ID
@@ -63,7 +64,7 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
63
64
 
64
65
  query = await RelationshipCountPerNodeQuery.init(
65
66
  db=self.db,
66
- node_ids=[node.uuid for node in nodes_to_validate],
67
+ node_ids=[n.uuid for n in nodes_to_validate],
67
68
  identifier=relm.schema.identifier,
68
69
  direction=relm.schema.direction.neighbor_direction,
69
70
  branch=branch,
@@ -74,14 +75,14 @@ class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
74
75
  # Need to adjust the number based on what we will add / remove
75
76
  # +1 for max_count
76
77
  # -1 for min_count
77
- for node in nodes_to_validate:
78
- if node.max_count and count_per_peer[node.uuid] + 1 > node.max_count:
78
+ for node_to_validate in nodes_to_validate:
79
+ if node_to_validate.max_count and count_per_peer[node_to_validate.uuid] + 1 > node_to_validate.max_count:
79
80
  raise ValidationError(
80
- f"Node {node.uuid} has {count_per_peer[node.uuid] + 1} peers "
81
- f"for {relm.schema.identifier}, maximum of {node.max_count} allowed",
81
+ f"Node {node_to_validate.uuid} has {count_per_peer[node_to_validate.uuid] + 1} peers "
82
+ f"for {relm.schema.identifier}, maximum of {node_to_validate.max_count} allowed",
82
83
  )
83
- if node.min_count and count_per_peer[node.uuid] - 1 < node.min_count:
84
+ if node_to_validate.min_count and count_per_peer[node_to_validate.uuid] - 1 < node_to_validate.min_count:
84
85
  raise ValidationError(
85
- f"Node {node.uuid} has {count_per_peer[node.uuid] - 1} peers "
86
- f"for {relm.schema.identifier}, no fewer than {node.min_count} allowed",
86
+ f"Node {node_to_validate.uuid} has {count_per_peer[node_to_validate.uuid] - 1} peers "
87
+ f"for {relm.schema.identifier}, no fewer than {node_to_validate.min_count} allowed",
87
88
  )
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
+ from infrahub.core.node import Node
3
4
  from infrahub.core.schema import MainSchemaTypes
4
5
 
5
6
  from ..model import RelationshipManager
@@ -7,4 +8,4 @@ from ..model import RelationshipManager
7
8
 
8
9
  class RelationshipManagerConstraintInterface(ABC):
9
10
  @abstractmethod
10
- async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None: ...
11
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: ...
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from infrahub.core import registry
4
4
  from infrahub.core.branch import Branch
5
5
  from infrahub.core.constants import RelationshipCardinality
6
+ from infrahub.core.node import Node
6
7
  from infrahub.core.query.node import NodeListGetInfoQuery
7
8
  from infrahub.core.schema import MainSchemaTypes
8
9
  from infrahub.core.schema.generic_schema import GenericSchema
@@ -26,7 +27,7 @@ class RelationshipPeerKindConstraint(RelationshipManagerConstraintInterface):
26
27
  self.db = db
27
28
  self.branch = branch
28
29
 
29
- async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None: # noqa: ARG002
30
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
30
31
  branch = await registry.get_branch(db=self.db) if not self.branch else self.branch
31
32
  peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch, duplicate=False)
32
33
  if isinstance(peer_schema, GenericSchema):
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Mapping
4
+
5
+ from infrahub.exceptions import ValidationError
6
+
7
+ from .interface import RelationshipManagerConstraintInterface
8
+
9
+ if TYPE_CHECKING:
10
+ from infrahub.core.branch import Branch
11
+ from infrahub.core.node import Node
12
+ from infrahub.core.schema import MainSchemaTypes
13
+ from infrahub.database import InfrahubDatabase
14
+
15
+ from ..model import RelationshipManager
16
+
17
+
18
+ class RelationshipPeerParentConstraint(RelationshipManagerConstraintInterface):
19
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
20
+ self.db = db
21
+ self.branch = branch
22
+
23
+ async def _check_relationship_peers_parent(
24
+ self, relm: RelationshipManager, parent_rel_name: str, node: Node, peers: Mapping[str, Node]
25
+ ) -> None:
26
+ """Validate that all peers of a given `relm` have the same parent for the given `relationship_name`."""
27
+ node_parent = await node.get_parent_relationship_peer(db=self.db, name=parent_rel_name)
28
+ if not node_parent:
29
+ # If the schema is properly validated we are not expecting this to happen
30
+ raise ValidationError(f"Node {node.id} ({node.get_kind()}) does not have a parent peer")
31
+
32
+ parents: set[str] = {node_parent.id}
33
+ for peer in peers.values():
34
+ parent = await peer.get_parent_relationship_peer(db=self.db, name=parent_rel_name)
35
+ if not parent:
36
+ # If the schema is properly validated we are not expecting this to happen
37
+ raise ValidationError(f"Peer {peer.id} ({peer.get_kind()}) does not have a parent peer")
38
+ parents.add(parent.id)
39
+
40
+ if len(parents) != 1:
41
+ raise ValidationError(
42
+ f"All the elements of the '{relm.name}' relationship on node {node.id} ({node.get_kind()}) must have the same parent "
43
+ "as the node"
44
+ )
45
+
46
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
47
+ if not relm.schema.common_parent:
48
+ return
49
+
50
+ peers = await relm.get_peers(db=self.db)
51
+ if not peers:
52
+ return
53
+
54
+ await self._check_relationship_peers_parent(
55
+ relm=relm, parent_rel_name=relm.schema.common_parent, node=node, peers=peers
56
+ )
@@ -58,7 +58,7 @@ class RelationshipPeerRelativesConstraint(RelationshipManagerConstraintInterface
58
58
  f"for their '{node.schema.kind}.{relationship_name}' relationship"
59
59
  )
60
60
 
61
- async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None:
61
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
62
62
  if relm.schema.cardinality != RelationshipCardinality.MANY or not relm.schema.common_relatives:
63
63
  return
64
64
 
@@ -23,7 +23,7 @@ class RelationshipProfilesKindConstraint(RelationshipManagerConstraintInterface)
23
23
  self.branch = branch
24
24
  self.schema_branch = registry.schema.get_schema_branch(branch.name if branch else registry.default_branch)
25
25
 
26
- async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None:
26
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
27
27
  if relm.name != "profiles" or not isinstance(node_schema, NodeSchema):
28
28
  return
29
29
 
@@ -754,13 +754,20 @@ relationship_schema = SchemaNode(
754
754
  optional=True,
755
755
  extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
756
756
  ),
757
+ SchemaAttribute(
758
+ name="common_parent",
759
+ kind="Text",
760
+ optional=True,
761
+ description="Name of a parent relationship on the peer schema that must share the same related object with the object's parent.",
762
+ extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
763
+ ),
757
764
  SchemaAttribute(
758
765
  name="common_relatives",
759
766
  kind="List",
760
767
  internal_kind=str,
761
768
  optional=True,
762
769
  description="List of relationship names on the peer schema for which all objects must share the same set of peers.",
763
- extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
770
+ extra={"update": UpdateSupport.ALLOWED},
764
771
  ),
765
772
  SchemaAttribute(
766
773
  name="order_weight",