Skip to content

Commit 97390e1

Browse files
authored
Add common_parent relationship attribute (#6626)
This relationship attribute is used to specify that peers of a given relationship must share a common parent with the source object. While the parent of the source object is infered automatically (as there is only one parent per object), the `common_parent` value must be set to the name of the parent relationship of the peer schema.
1 parent 014ed76 commit 97390e1

File tree

24 files changed

+500
-30
lines changed

24 files changed

+500
-30
lines changed

backend/infrahub/core/constraint/node/runner.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ async def check(
3838
relationship_manager: RelationshipManager = getattr(node, relationship_name)
3939
await relationship_manager.fetch_relationship_ids(db=db, force_refresh=True)
4040
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+
)

backend/infrahub/core/node/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,3 +962,12 @@ def validate_relationships(self) -> None:
962962
for name in self._relationships:
963963
relm: RelationshipManager = getattr(self, name)
964964
relm.validate()
965+
966+
async def get_parent_relationship_peer(self, db: InfrahubDatabase, name: str) -> Node | None:
967+
"""When a node has a parent relationship of a given name, this method returns the peer of that relationship."""
968+
relationship = self.get_schema().get_relationship(name=name)
969+
if relationship.kind != RelationshipKind.PARENT:
970+
raise ValueError(f"Relationship '{name}' is not of kind 'parent'")
971+
972+
relm: RelationshipManager = getattr(self, name)
973+
return await relm.get_peer(db=db)

backend/infrahub/core/relationship/constraints/count.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from infrahub.core import registry
44
from infrahub.core.branch import Branch
55
from infrahub.core.constants import RelationshipCardinality, RelationshipDirection
6+
from infrahub.core.node import Node
67
from infrahub.core.query.relationship import RelationshipCountPerNodeQuery
78
from infrahub.core.schema import MainSchemaTypes
89
from infrahub.database import InfrahubDatabase
@@ -25,7 +26,7 @@ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
2526
self.db = db
2627
self.branch = branch
2728

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
2930
branch = await registry.get_branch(db=self.db) if not self.branch else self.branch
3031

3132
# NOTE adding resolve here because we need to retrieve the real ID
@@ -63,7 +64,7 @@ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -
6364

6465
query = await RelationshipCountPerNodeQuery.init(
6566
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],
6768
identifier=relm.schema.identifier,
6869
direction=relm.schema.direction.neighbor_direction,
6970
branch=branch,
@@ -74,14 +75,14 @@ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -
7475
# Need to adjust the number based on what we will add / remove
7576
# +1 for max_count
7677
# -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:
7980
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",
8283
)
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:
8485
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",
8788
)
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from abc import ABC, abstractmethod
22

3+
from infrahub.core.node import Node
34
from infrahub.core.schema import MainSchemaTypes
45

56
from ..model import RelationshipManager
67

78

89
class RelationshipManagerConstraintInterface(ABC):
910
@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: ...

backend/infrahub/core/relationship/constraints/peer_kind.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from infrahub.core import registry
44
from infrahub.core.branch import Branch
55
from infrahub.core.constants import RelationshipCardinality
6+
from infrahub.core.node import Node
67
from infrahub.core.query.node import NodeListGetInfoQuery
78
from infrahub.core.schema import MainSchemaTypes
89
from infrahub.core.schema.generic_schema import GenericSchema
@@ -26,7 +27,7 @@ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
2627
self.db = db
2728
self.branch = branch
2829

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
3031
branch = await registry.get_branch(db=self.db) if not self.branch else self.branch
3132
peer_schema = registry.schema.get(name=relm.schema.peer, branch=branch, duplicate=False)
3233
if isinstance(peer_schema, GenericSchema):
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
)

backend/infrahub/core/relationship/constraints/peer_relatives.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def _check_relationship_peers_relatives(
5858
f"for their '{node.schema.kind}.{relationship_name}' relationship"
5959
)
6060

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
6262
if relm.schema.cardinality != RelationshipCardinality.MANY or not relm.schema.common_relatives:
6363
return
6464

backend/infrahub/core/relationship/constraints/profiles_kind.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
2323
self.branch = branch
2424
self.schema_branch = registry.schema.get_schema_branch(branch.name if branch else registry.default_branch)
2525

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
2727
if relm.name != "profiles" or not isinstance(node_schema, NodeSchema):
2828
return
2929

backend/infrahub/core/schema/definitions/internal.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,13 +754,20 @@ def to_dict(self) -> dict[str, Any]:
754754
optional=True,
755755
extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
756756
),
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.ALLOWED},
763+
),
757764
SchemaAttribute(
758765
name="common_relatives",
759766
kind="List",
760767
internal_kind=str,
761768
optional=True,
762769
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},
764771
),
765772
SchemaAttribute(
766773
name="order_weight",

backend/infrahub/core/schema/generated/relationship_schema.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,15 @@ class GeneratedRelationshipSchema(HashableModel):
7373
description="Defines the maximum objects allowed on the other side of the relationship.",
7474
json_schema_extra={"update": "validate_constraint"},
7575
)
76+
common_parent: str | None = Field(
77+
default=None,
78+
description="Name of a parent relationship on the peer schema that must share the same related object with the object's parent.",
79+
json_schema_extra={"update": "allowed"},
80+
)
7681
common_relatives: list[str] | None = Field(
7782
default=None,
7883
description="List of relationship names on the peer schema for which all objects must share the same set of peers.",
79-
json_schema_extra={"update": "validate_constraint"},
84+
json_schema_extra={"update": "allowed"},
8085
)
8186
order_weight: int | None = Field(
8287
default=None,

0 commit comments

Comments
 (0)