Skip to content

Commit 52cf5f9

Browse files
authored
IFC-1348 Make templates for generics generics (#5938)
1 parent c7ce291 commit 52cf5f9

File tree

10 files changed

+244
-93
lines changed

10 files changed

+244
-93
lines changed

backend/infrahub/core/migrations/graph/m018_uniqueness_nulls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
5050
schema_branch = await manager.load_schema_from_db(db=db, branch=default_branch)
5151
manager.set_schema_branch(name=default_branch.name, schema=schema_branch)
5252

53-
for schema_kind in schema_branch.node_names + schema_branch.generic_names:
53+
for schema_kind in schema_branch.node_names + schema_branch.generic_names_without_templates:
5454
schema = schema_branch.get(name=schema_kind, duplicate=False)
5555
if not isinstance(schema, NodeSchema | GenericSchema):
5656
continue

backend/infrahub/core/schema/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ async def load_schema_to_db(
251251

252252
branch = await registry.get_branch(branch=branch, db=db)
253253

254-
for item_kind in schema.node_names + schema.generic_names:
254+
for item_kind in schema.node_names + schema.generic_names_without_templates:
255255
if limit and item_kind not in limit:
256256
continue
257257
item = schema.get(name=item_kind, duplicate=False)

backend/infrahub/core/schema/schema_branch.py

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ def node_names(self) -> list[str]:
107107
def generic_names(self) -> list[str]:
108108
return list(self.generics.keys())
109109

110+
@property
111+
def generic_names_without_templates(self) -> list[str]:
112+
return [g for g in self.generic_names if not g.startswith("Template")]
113+
110114
@property
111115
def profile_names(self) -> list[str]:
112116
return list(self.profiles.keys())
@@ -115,10 +119,10 @@ def profile_names(self) -> list[str]:
115119
def template_names(self) -> list[str]:
116120
return list(self.templates.keys())
117121

118-
def get_all_kind_id_map(self, exclude_profiles: bool = False) -> dict[str, str]:
122+
def get_all_kind_id_map(self, nodes_and_generics_only: bool = False) -> dict[str, str]:
119123
kind_id_map = {}
120-
if exclude_profiles:
121-
names = self.node_names + self.generic_names
124+
if nodes_and_generics_only:
125+
names = self.node_names + self.generic_names_without_templates
122126
else:
123127
names = self.all_names
124128
for name in names:
@@ -182,8 +186,8 @@ def from_dict_schema_object(cls, data: dict) -> Self:
182186

183187
def diff(self, other: SchemaBranch) -> SchemaDiff:
184188
# Identify the nodes or generics that have been added or removed
185-
local_kind_id_map = self.get_all_kind_id_map(exclude_profiles=True)
186-
other_kind_id_map = other.get_all_kind_id_map(exclude_profiles=True)
189+
local_kind_id_map = self.get_all_kind_id_map(nodes_and_generics_only=True)
190+
other_kind_id_map = other.get_all_kind_id_map(nodes_and_generics_only=True)
187191
clean_local_ids = [id for id in local_kind_id_map.values() if id is not None]
188192
clean_other_ids = [id for id in other_kind_id_map.values() if id is not None]
189193
shared_ids = intersection(list1=clean_local_ids, list2=clean_other_ids)
@@ -687,7 +691,7 @@ def validate_schema_path(
687691
return schema_attribute_path
688692

689693
def sync_uniqueness_constraints_and_unique_attributes(self) -> None:
690-
for name in self.generic_names + self.node_names:
694+
for name in self.generic_names_without_templates + self.node_names:
691695
node_schema = self.get(name=name, duplicate=False)
692696

693697
if not node_schema.unique_attributes and not node_schema.uniqueness_constraints:
@@ -802,7 +806,7 @@ def validate_default_filters(self) -> None:
802806
)
803807

804808
def validate_default_values(self) -> None:
805-
for name in self.generic_names + self.node_names:
809+
for name in self.generic_names_without_templates + self.node_names:
806810
node_schema = self.get(name=name, duplicate=False)
807811
for node_attr in node_schema.local_attributes:
808812
if node_attr.default_value is None:
@@ -822,7 +826,7 @@ def validate_default_values(self) -> None:
822826
) from exc
823827

824828
def validate_human_friendly_id(self) -> None:
825-
for name in self.generic_names + self.node_names:
829+
for name in self.generic_names_without_templates + self.node_names:
826830
node_schema = self.get(name=name, duplicate=False)
827831
hf_attr_names = set()
828832

@@ -843,7 +847,7 @@ def validate_human_friendly_id(self) -> None:
843847

844848
def validate_required_relationships(self) -> None:
845849
reverse_dependency_map: dict[str, set[str]] = {}
846-
for name in self.node_names + self.generic_names:
850+
for name in self.node_names + self.generic_names_without_templates:
847851
node_schema = self.get(name=name, duplicate=False)
848852
for relationship_schema in node_schema.relationships:
849853
if relationship_schema.optional:
@@ -861,7 +865,7 @@ def validate_required_relationships(self) -> None:
861865
def validate_parent_component(self) -> None:
862866
# {parent_kind: {component_kind_1, component_kind_2, ...}}
863867
dependency_map: dict[str, set[str]] = defaultdict(set)
864-
for name in self.generic_names + self.node_names:
868+
for name in self.generic_names_without_templates + self.node_names:
865869
node_schema = self.get(name=name, duplicate=False)
866870

867871
parent_relationships: list[RelationshipSchema] = []
@@ -1147,7 +1151,7 @@ def process_relationships(self) -> None:
11471151
self.set(name=schema_to_update.kind, schema=schema_to_update)
11481152

11491153
def process_human_friendly_id(self) -> None:
1150-
for name in self.generic_names + self.node_names:
1154+
for name in self.generic_names_without_templates + self.node_names:
11511155
node = self.get(name=name, duplicate=False)
11521156

11531157
# If human_friendly_id IS NOT defined
@@ -1634,7 +1638,7 @@ def manage_profile_schemas(self) -> None:
16341638
self.set(name=core_profile_schema.kind, schema=core_profile_schema)
16351639

16361640
profile_schema_kinds = set()
1637-
for node_name in self.node_names + self.generic_names:
1641+
for node_name in self.node_names + self.generic_names_without_templates:
16381642
node = self.get(name=node_name, duplicate=False)
16391643
if (
16401644
node.namespace in RESTRICTED_NAMESPACES
@@ -1865,37 +1869,64 @@ def add_relationships_to_template(self, node: NodeSchema) -> None:
18651869
)
18661870
)
18671871

1868-
def generate_object_template_from_node(self, node: NodeSchema) -> TemplateSchema:
1872+
def generate_object_template_from_node(
1873+
self, node: NodeSchema | GenericSchema, need_templates: set[NodeSchema | GenericSchema]
1874+
) -> TemplateSchema | GenericSchema:
18691875
core_template_schema = self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
18701876
core_name_attr = core_template_schema.get_attribute(name=OBJECT_TEMPLATE_NAME_ATTR)
18711877
template_name_attr = AttributeSchema(
18721878
**core_name_attr.model_dump(exclude=["id", "inherited"]),
18731879
)
18741880
template_name_attr.branch = node.branch
18751881

1876-
template = TemplateSchema(
1877-
name=node.kind,
1878-
namespace="Template",
1879-
label=f"Object template {node.label}",
1880-
description=f"Object template for {node.kind}",
1881-
branch=node.branch,
1882-
include_in_menu=False,
1883-
display_labels=["template_name__value"],
1884-
inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.OBJECTTEMPLATE, InfrahubKind.NODE],
1885-
human_friendly_id=["template_name__value"],
1886-
default_filter="template_name__value",
1887-
attributes=[template_name_attr],
1888-
relationships=[
1889-
RelationshipSchema(
1890-
name="related_nodes",
1891-
identifier="node__objecttemplate",
1892-
peer=node.kind,
1893-
kind=RelationshipKind.TEMPLATE,
1894-
cardinality=RelationshipCardinality.MANY,
1895-
branch=BranchSupportType.AWARE,
1896-
)
1897-
],
1898-
)
1882+
template: TemplateSchema | GenericSchema
1883+
need_template_kinds = [n.kind for n in need_templates]
1884+
1885+
if isinstance(node, GenericSchema):
1886+
# When needing a template for a generic, we generate an empty shell mostly to make sure that schemas (including the GraphQL one) will
1887+
# look right. We don't really care about applying inheritance of fields as it was already processed and actual templates will have the
1888+
# correct attributes and relationships
1889+
template = GenericSchema(
1890+
name=node.kind,
1891+
namespace="Template",
1892+
label=f"Generic object template {node.label}",
1893+
description=f"Generic object template for generic {node.kind}",
1894+
generate_profile=False,
1895+
branch=node.branch,
1896+
include_in_menu=False,
1897+
)
1898+
1899+
for used in node.used_by:
1900+
if used in need_template_kinds:
1901+
template.used_by.append(self._get_object_template_kind(node_kind=used))
1902+
else:
1903+
template = TemplateSchema(
1904+
name=node.kind,
1905+
namespace="Template",
1906+
label=f"Object template {node.label}",
1907+
description=f"Object template for {node.kind}",
1908+
branch=node.branch,
1909+
include_in_menu=False,
1910+
display_labels=["template_name__value"],
1911+
inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.OBJECTTEMPLATE, InfrahubKind.NODE],
1912+
human_friendly_id=["template_name__value"],
1913+
default_filter="template_name__value",
1914+
attributes=[template_name_attr],
1915+
relationships=[
1916+
RelationshipSchema(
1917+
name="related_nodes",
1918+
identifier="node__objecttemplate",
1919+
peer=node.kind,
1920+
kind=RelationshipKind.TEMPLATE,
1921+
cardinality=RelationshipCardinality.MANY,
1922+
branch=BranchSupportType.AWARE,
1923+
)
1924+
],
1925+
)
1926+
1927+
for inherited in node.inherit_from:
1928+
if inherited in need_template_kinds:
1929+
template.inherit_from.append(self._get_object_template_kind(node_kind=inherited))
18991930

19001931
for node_attr in node.attributes:
19011932
if node_attr.unique:
@@ -1918,15 +1949,22 @@ def identify_required_object_templates(
19181949
identified.add(node_schema)
19191950

19201951
for relationship in node_schema.relationships:
1921-
if relationship.peer in [
1922-
InfrahubKind.GENERICGROUP,
1923-
InfrahubKind.PROFILE,
1924-
] or relationship.kind not in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]:
1952+
if relationship.peer in [InfrahubKind.GENERICGROUP, InfrahubKind.PROFILE] or relationship.kind not in [
1953+
RelationshipKind.COMPONENT,
1954+
RelationshipKind.PARENT,
1955+
]:
19251956
continue
19261957

19271958
peer_schema = self.get(name=relationship.peer, duplicate=False)
19281959
if not isinstance(peer_schema, NodeSchema | GenericSchema) or peer_schema in identified:
19291960
continue
1961+
# In a context of a generic, we won't be able to create objects out of it, so any kind of nodes implementing the generic is a valid
1962+
# option, we therefore need to have a template for each of those nodes
1963+
if isinstance(peer_schema, GenericSchema) and peer_schema.used_by:
1964+
for used_by in peer_schema.used_by:
1965+
identified |= self.identify_required_object_templates(
1966+
node_schema=self.get(name=used_by, duplicate=False), identified=identified
1967+
)
19301968

19311969
identified |= self.identify_required_object_templates(node_schema=peer_schema, identified=identified)
19321970

@@ -1936,7 +1974,7 @@ def manage_object_template_schemas(self) -> None:
19361974
need_templates: set[NodeSchema | GenericSchema] = set()
19371975
template_schema_kinds: set[str] = set()
19381976

1939-
for node_name in self.node_names + self.generic_names:
1977+
for node_name in self.node_names + self.generic_names_without_templates:
19401978
node = self.get(name=node_name, duplicate=False)
19411979

19421980
# Delete old object templates if schemas were removed
@@ -1955,7 +1993,7 @@ def manage_object_template_schemas(self) -> None:
19551993

19561994
# Generate templates with their attributes
19571995
for node in need_templates:
1958-
template = self.generate_object_template_from_node(node=node)
1996+
template = self.generate_object_template_from_node(node=node, need_templates=need_templates)
19591997
self.set(name=template.kind, schema=template)
19601998
template_schema_kinds.add(template.kind)
19611999

backend/infrahub/graphql/mutations/main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,13 @@ async def _handle_template_relationships(
288288
if not template_relationship_peers:
289289
continue
290290

291-
obj_peer_schema = relationship.get_peer_schema(db=db, branch=branch)
292291
for template_relationship_peer in template_relationship_peers.values():
292+
# We retrieve peer schema for each peer in case we are processing a relationship which is based on a generic
293+
obj_peer_schema = registry.schema.get_node_schema(
294+
name=template_relationship_peer.get_schema().kind.removeprefix("Template"),
295+
branch=branch,
296+
duplicate=False,
297+
)
293298
obj_peer_data = await cls._extract_peer_data(
294299
db=db,
295300
template_peer=template_relationship_peer,

backend/tests/constants/kind.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
COUNTRY = "TestingCountry"
66
DEVICE = "TestingDevice"
77
INTERFACE = "TestingInterface"
8+
INTERFACE_HOLDER = "TestingInterfaceHolder"
89
LOCATION = "TestingLocation"
910
MANUFACTURER = "TestingManufacturer"
1011
PERSON = "TestingPerson"
12+
PHYSICAL_INTERFACE = "TestingPhysicalInterface"
1113
SFP = "TestingSfp"
1214
SITE = "TestingSite"
1315
THING = "TestingThing"
1416
TICKET = "TestingTicket"
1517
TSHIRT = "TestingTShirt"
18+
VIRTUAL_INTERFACE = "TestingVirtualInterface"

backend/tests/helpers/schema/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .car import CAR
1010
from .child import CHILD
1111
from .color import COLOR
12-
from .device import DEVICE, INTERFACE, SFP
12+
from .device import DEVICE, INTERFACE, INTERFACE_HOLDER, PHYSICAL_INTERFACE, SFP, VIRTUAL_INTERFACE
1313
from .location import CONTINENT, COUNTRY, LOCATION, SITE
1414
from .manufacturer import MANUFACTURER
1515
from .person import PERSON
@@ -23,7 +23,9 @@
2323

2424

2525
CAR_SCHEMA = SchemaRoot(nodes=[CAR, MANUFACTURER, PERSON])
26-
DEVICE_SCHEMA = SchemaRoot(nodes=[DEVICE, INTERFACE, SFP])
26+
DEVICE_SCHEMA = SchemaRoot(
27+
generics=[INTERFACE, INTERFACE_HOLDER], nodes=[DEVICE, PHYSICAL_INTERFACE, VIRTUAL_INTERFACE, SFP]
28+
)
2729
LOCATION_SCHEMA = SchemaRoot(generics=[LOCATION], nodes=[CONTINENT, COUNTRY, SITE])
2830

2931

@@ -52,14 +54,17 @@ async def load_schema(
5254
"DEVICE",
5355
"DEVICE_SCHEMA",
5456
"INTERFACE",
57+
"INTERFACE_HOLDER",
5558
"LOCATION",
5659
"LOCATION_SCHEMA",
5760
"MANUFACTURER",
5861
"PERSON",
62+
"PHYSICAL_INTERFACE",
5963
"SFP",
6064
"SITE",
6165
"THING",
6266
"TICKET",
6367
"TSHIRT",
68+
"VIRTUAL_INTERFACE",
6469
"WIDGET",
6570
]

0 commit comments

Comments
 (0)