infrahub-server 1.4.12__py3-none-any.whl → 1.5.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 (234) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/internal.py +2 -0
  5. infrahub/api/query.py +2 -0
  6. infrahub/api/schema.py +27 -3
  7. infrahub/auth.py +5 -5
  8. infrahub/cli/__init__.py +2 -0
  9. infrahub/cli/db.py +160 -157
  10. infrahub/cli/dev.py +118 -0
  11. infrahub/cli/tasks.py +46 -0
  12. infrahub/cli/upgrade.py +56 -9
  13. infrahub/computed_attribute/tasks.py +19 -7
  14. infrahub/config.py +7 -2
  15. infrahub/core/attribute.py +35 -24
  16. infrahub/core/branch/enums.py +1 -1
  17. infrahub/core/branch/models.py +9 -5
  18. infrahub/core/branch/needs_rebase_status.py +11 -0
  19. infrahub/core/branch/tasks.py +72 -10
  20. infrahub/core/changelog/models.py +2 -10
  21. infrahub/core/constants/__init__.py +4 -0
  22. infrahub/core/constants/infrahubkind.py +1 -0
  23. infrahub/core/convert_object_type/object_conversion.py +201 -0
  24. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  25. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  26. infrahub/core/diff/calculator.py +2 -2
  27. infrahub/core/diff/model/path.py +4 -0
  28. infrahub/core/diff/payload_builder.py +1 -1
  29. infrahub/core/diff/query/artifact.py +1 -0
  30. infrahub/core/diff/query/delete_query.py +9 -5
  31. infrahub/core/diff/query/field_summary.py +1 -0
  32. infrahub/core/diff/query/merge.py +39 -23
  33. infrahub/core/graph/__init__.py +1 -1
  34. infrahub/core/initialization.py +7 -4
  35. infrahub/core/manager.py +3 -81
  36. infrahub/core/migrations/__init__.py +3 -0
  37. infrahub/core/migrations/exceptions.py +4 -0
  38. infrahub/core/migrations/graph/__init__.py +13 -10
  39. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  40. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  41. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  42. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  43. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +149 -0
  44. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  45. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  46. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  47. infrahub/core/migrations/query/__init__.py +7 -8
  48. infrahub/core/migrations/query/attribute_add.py +8 -6
  49. infrahub/core/migrations/query/attribute_remove.py +134 -0
  50. infrahub/core/migrations/runner.py +54 -0
  51. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  52. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  53. infrahub/core/migrations/schema/node_attribute_add.py +26 -5
  54. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  55. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  56. infrahub/core/migrations/schema/node_remove.py +2 -1
  57. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  58. infrahub/core/migrations/shared.py +66 -19
  59. infrahub/core/models.py +2 -2
  60. infrahub/core/node/__init__.py +207 -54
  61. infrahub/core/node/create.py +53 -49
  62. infrahub/core/node/lock_utils.py +124 -0
  63. infrahub/core/node/node_property_attribute.py +230 -0
  64. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  65. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  66. infrahub/core/node/resource_manager/number_pool.py +2 -1
  67. infrahub/core/node/standard.py +1 -1
  68. infrahub/core/property.py +11 -0
  69. infrahub/core/protocols.py +8 -1
  70. infrahub/core/query/attribute.py +82 -15
  71. infrahub/core/query/diff.py +61 -16
  72. infrahub/core/query/ipam.py +16 -4
  73. infrahub/core/query/node.py +92 -212
  74. infrahub/core/query/relationship.py +44 -26
  75. infrahub/core/query/subquery.py +0 -8
  76. infrahub/core/relationship/model.py +69 -24
  77. infrahub/core/schema/__init__.py +56 -0
  78. infrahub/core/schema/attribute_schema.py +4 -2
  79. infrahub/core/schema/basenode_schema.py +42 -2
  80. infrahub/core/schema/definitions/core/__init__.py +2 -0
  81. infrahub/core/schema/definitions/core/check.py +1 -1
  82. infrahub/core/schema/definitions/core/generator.py +2 -0
  83. infrahub/core/schema/definitions/core/group.py +16 -2
  84. infrahub/core/schema/definitions/core/repository.py +7 -0
  85. infrahub/core/schema/definitions/core/transform.py +1 -1
  86. infrahub/core/schema/definitions/internal.py +12 -3
  87. infrahub/core/schema/generated/attribute_schema.py +2 -2
  88. infrahub/core/schema/generated/base_node_schema.py +6 -1
  89. infrahub/core/schema/manager.py +3 -0
  90. infrahub/core/schema/node_schema.py +1 -0
  91. infrahub/core/schema/relationship_schema.py +0 -1
  92. infrahub/core/schema/schema_branch.py +295 -10
  93. infrahub/core/schema/schema_branch_display.py +135 -0
  94. infrahub/core/schema/schema_branch_hfid.py +120 -0
  95. infrahub/core/validators/aggregated_checker.py +1 -1
  96. infrahub/database/graph.py +21 -0
  97. infrahub/display_labels/__init__.py +0 -0
  98. infrahub/display_labels/gather.py +48 -0
  99. infrahub/display_labels/models.py +240 -0
  100. infrahub/display_labels/tasks.py +192 -0
  101. infrahub/display_labels/triggers.py +22 -0
  102. infrahub/events/branch_action.py +27 -1
  103. infrahub/events/group_action.py +1 -1
  104. infrahub/events/node_action.py +1 -1
  105. infrahub/generators/constants.py +7 -0
  106. infrahub/generators/models.py +38 -12
  107. infrahub/generators/tasks.py +34 -16
  108. infrahub/git/base.py +42 -2
  109. infrahub/git/integrator.py +22 -14
  110. infrahub/git/tasks.py +52 -2
  111. infrahub/graphql/analyzer.py +9 -0
  112. infrahub/graphql/api/dependencies.py +2 -4
  113. infrahub/graphql/api/endpoints.py +16 -6
  114. infrahub/graphql/app.py +2 -4
  115. infrahub/graphql/initialization.py +2 -3
  116. infrahub/graphql/manager.py +213 -137
  117. infrahub/graphql/middleware.py +12 -0
  118. infrahub/graphql/mutations/branch.py +16 -0
  119. infrahub/graphql/mutations/computed_attribute.py +110 -3
  120. infrahub/graphql/mutations/convert_object_type.py +44 -13
  121. infrahub/graphql/mutations/display_label.py +118 -0
  122. infrahub/graphql/mutations/generator.py +25 -7
  123. infrahub/graphql/mutations/hfid.py +125 -0
  124. infrahub/graphql/mutations/ipam.py +73 -41
  125. infrahub/graphql/mutations/main.py +61 -178
  126. infrahub/graphql/mutations/profile.py +195 -0
  127. infrahub/graphql/mutations/proposed_change.py +8 -1
  128. infrahub/graphql/mutations/relationship.py +2 -2
  129. infrahub/graphql/mutations/repository.py +22 -83
  130. infrahub/graphql/mutations/resource_manager.py +2 -2
  131. infrahub/graphql/mutations/webhook.py +1 -1
  132. infrahub/graphql/queries/resource_manager.py +1 -1
  133. infrahub/graphql/registry.py +173 -0
  134. infrahub/graphql/resolvers/resolver.py +2 -0
  135. infrahub/graphql/schema.py +8 -1
  136. infrahub/graphql/schema_sort.py +170 -0
  137. infrahub/graphql/types/branch.py +4 -1
  138. infrahub/graphql/types/enums.py +3 -0
  139. infrahub/groups/tasks.py +1 -1
  140. infrahub/hfid/__init__.py +0 -0
  141. infrahub/hfid/gather.py +48 -0
  142. infrahub/hfid/models.py +240 -0
  143. infrahub/hfid/tasks.py +191 -0
  144. infrahub/hfid/triggers.py +22 -0
  145. infrahub/lock.py +119 -42
  146. infrahub/locks/__init__.py +0 -0
  147. infrahub/locks/tasks.py +37 -0
  148. infrahub/message_bus/types.py +1 -0
  149. infrahub/patch/plan_writer.py +2 -2
  150. infrahub/permissions/constants.py +2 -0
  151. infrahub/profiles/__init__.py +0 -0
  152. infrahub/profiles/node_applier.py +101 -0
  153. infrahub/profiles/queries/__init__.py +0 -0
  154. infrahub/profiles/queries/get_profile_data.py +98 -0
  155. infrahub/profiles/tasks.py +63 -0
  156. infrahub/proposed_change/tasks.py +67 -14
  157. infrahub/repositories/__init__.py +0 -0
  158. infrahub/repositories/create_repository.py +113 -0
  159. infrahub/server.py +9 -1
  160. infrahub/services/__init__.py +8 -5
  161. infrahub/services/adapters/http/__init__.py +5 -0
  162. infrahub/services/adapters/workflow/worker.py +14 -3
  163. infrahub/task_manager/event.py +5 -0
  164. infrahub/task_manager/models.py +7 -0
  165. infrahub/task_manager/task.py +73 -0
  166. infrahub/tasks/registry.py +6 -4
  167. infrahub/trigger/catalogue.py +4 -0
  168. infrahub/trigger/models.py +2 -0
  169. infrahub/trigger/setup.py +13 -4
  170. infrahub/trigger/tasks.py +6 -0
  171. infrahub/webhook/models.py +1 -1
  172. infrahub/workers/dependencies.py +3 -1
  173. infrahub/workers/infrahub_async.py +10 -2
  174. infrahub/workflows/catalogue.py +118 -3
  175. infrahub/workflows/initialization.py +21 -0
  176. infrahub/workflows/models.py +17 -2
  177. infrahub/workflows/utils.py +2 -1
  178. infrahub_sdk/branch.py +17 -8
  179. infrahub_sdk/checks.py +1 -1
  180. infrahub_sdk/client.py +376 -95
  181. infrahub_sdk/config.py +29 -2
  182. infrahub_sdk/convert_object_type.py +61 -0
  183. infrahub_sdk/ctl/branch.py +3 -0
  184. infrahub_sdk/ctl/check.py +2 -3
  185. infrahub_sdk/ctl/cli_commands.py +20 -12
  186. infrahub_sdk/ctl/config.py +8 -2
  187. infrahub_sdk/ctl/generator.py +6 -3
  188. infrahub_sdk/ctl/graphql.py +184 -0
  189. infrahub_sdk/ctl/repository.py +39 -1
  190. infrahub_sdk/ctl/schema.py +40 -10
  191. infrahub_sdk/ctl/task.py +110 -0
  192. infrahub_sdk/ctl/utils.py +4 -0
  193. infrahub_sdk/ctl/validate.py +5 -3
  194. infrahub_sdk/diff.py +4 -5
  195. infrahub_sdk/exceptions.py +2 -0
  196. infrahub_sdk/generator.py +7 -1
  197. infrahub_sdk/graphql/__init__.py +12 -0
  198. infrahub_sdk/graphql/constants.py +1 -0
  199. infrahub_sdk/graphql/plugin.py +85 -0
  200. infrahub_sdk/graphql/query.py +77 -0
  201. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  202. infrahub_sdk/graphql/utils.py +40 -0
  203. infrahub_sdk/node/attribute.py +2 -0
  204. infrahub_sdk/node/node.py +28 -20
  205. infrahub_sdk/node/relationship.py +1 -3
  206. infrahub_sdk/playback.py +1 -2
  207. infrahub_sdk/protocols.py +54 -6
  208. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  209. infrahub_sdk/pytest_plugin/utils.py +40 -0
  210. infrahub_sdk/repository.py +1 -2
  211. infrahub_sdk/schema/__init__.py +70 -4
  212. infrahub_sdk/schema/main.py +1 -0
  213. infrahub_sdk/schema/repository.py +8 -0
  214. infrahub_sdk/spec/models.py +7 -0
  215. infrahub_sdk/spec/object.py +54 -6
  216. infrahub_sdk/spec/processors/__init__.py +0 -0
  217. infrahub_sdk/spec/processors/data_processor.py +10 -0
  218. infrahub_sdk/spec/processors/factory.py +34 -0
  219. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  220. infrahub_sdk/spec/range_expansion.py +118 -0
  221. infrahub_sdk/task/models.py +6 -4
  222. infrahub_sdk/timestamp.py +18 -6
  223. infrahub_sdk/transforms.py +1 -1
  224. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
  225. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +233 -176
  226. infrahub_testcontainers/container.py +114 -2
  227. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  228. infrahub_testcontainers/docker-compose.test.yml +5 -0
  229. infrahub_testcontainers/models.py +2 -2
  230. infrahub_testcontainers/performance_test.py +4 -4
  231. infrahub/core/convert_object_type/conversion.py +0 -134
  232. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
  233. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
  234. {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -18,6 +18,7 @@ from infrahub.computed_attribute.constants import VALID_KINDS as VALID_COMPUTED_
18
18
  from infrahub.core.constants import (
19
19
  OBJECT_TEMPLATE_NAME_ATTR,
20
20
  OBJECT_TEMPLATE_RELATIONSHIP_NAME,
21
+ PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
21
22
  RESERVED_ATTR_GEN_NAMES,
22
23
  RESERVED_ATTR_REL_NAMES,
23
24
  RESTRICTED_NAMESPACES,
@@ -65,6 +66,8 @@ from ... import config
65
66
  from ..constants.schema import PARENT_CHILD_IDENTIFIER
66
67
  from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
67
68
  from .schema_branch_computed import ComputedAttributes
69
+ from .schema_branch_display import DisplayLabels
70
+ from .schema_branch_hfid import HFIDs
68
71
 
69
72
  log = get_logger()
70
73
 
@@ -76,6 +79,8 @@ class SchemaBranch:
76
79
  name: str | None = None,
77
80
  data: dict[str, dict[str, str]] | None = None,
78
81
  computed_attributes: ComputedAttributes | None = None,
82
+ display_labels: DisplayLabels | None = None,
83
+ hfids: HFIDs | None = None,
79
84
  ):
80
85
  self._cache: dict[str, NodeSchema | GenericSchema] = cache
81
86
  self.name: str | None = name
@@ -84,6 +89,8 @@ class SchemaBranch:
84
89
  self.profiles: dict[str, str] = {}
85
90
  self.templates: dict[str, str] = {}
86
91
  self.computed_attributes = computed_attributes or ComputedAttributes()
92
+ self.display_labels = display_labels or DisplayLabels()
93
+ self.hfids = hfids or HFIDs()
87
94
 
88
95
  if data:
89
96
  self.nodes = data.get("nodes", {})
@@ -277,6 +284,8 @@ class SchemaBranch:
277
284
  data=copy.deepcopy(self.to_dict()),
278
285
  cache=self._cache,
279
286
  computed_attributes=self.computed_attributes.duplicate(),
287
+ display_labels=self.display_labels.duplicate(),
288
+ hfids=self.hfids.duplicate(),
280
289
  )
281
290
 
282
291
  def set(self, name: str, schema: MainSchemaTypes) -> str:
@@ -319,14 +328,23 @@ class SchemaBranch:
319
328
  elif name in self.templates:
320
329
  key = self.templates[name]
321
330
 
322
- if key and duplicate:
323
- return self._cache[key].duplicate()
324
- if key and not duplicate:
325
- return self._cache[key]
331
+ if not key:
332
+ raise SchemaNotFoundError(
333
+ branch_name=self.name, identifier=name, message=f"Unable to find the schema {name!r} in the registry"
334
+ )
326
335
 
327
- raise SchemaNotFoundError(
328
- branch_name=self.name, identifier=name, message=f"Unable to find the schema {name!r} in the registry"
329
- )
336
+ schema: MainSchemaTypes | None = None
337
+ try:
338
+ schema = self._cache[key]
339
+ except KeyError:
340
+ pass
341
+
342
+ if not schema:
343
+ raise ValueError(f"Schema {name!r} on branch {self.name} has incorrect hash: {key!r}")
344
+
345
+ if duplicate:
346
+ return schema.duplicate()
347
+ return schema
330
348
 
331
349
  def get_node(self, name: str, duplicate: bool = True) -> NodeSchema:
332
350
  """Access a specific NodeSchema, defined by its kind."""
@@ -511,6 +529,9 @@ class SchemaBranch:
511
529
  self.process_post_validation()
512
530
 
513
531
  def process_pre_validation(self) -> None:
532
+ self.process_nodes_state()
533
+ self.process_attributes_state()
534
+ self.process_relationships_state()
514
535
  self.generate_identifiers()
515
536
  self.process_default_values()
516
537
  self.process_deprecations()
@@ -537,11 +558,13 @@ class SchemaBranch:
537
558
  self.sync_uniqueness_constraints_and_unique_attributes()
538
559
  self.validate_uniqueness_constraints()
539
560
  self.validate_display_labels()
561
+ self.validate_display_label()
540
562
  self.validate_order_by()
541
563
  self.validate_default_filters()
542
564
  self.validate_parent_component()
543
565
  self.validate_human_friendly_id()
544
566
  self.validate_required_relationships()
567
+ self.validate_inherited_relationships_fields()
545
568
 
546
569
  def process_post_validation(self) -> None:
547
570
  self.cleanup_inherited_elements()
@@ -551,6 +574,7 @@ class SchemaBranch:
551
574
  self.process_dropdowns()
552
575
  self.process_relationships()
553
576
  self.process_human_friendly_id()
577
+ self.register_human_friendly_id()
554
578
 
555
579
  def _generate_identifier_string(self, node_kind: str, peer_kind: str) -> str:
556
580
  return "__".join(sorted([node_kind, peer_kind])).lower()
@@ -759,6 +783,36 @@ class SchemaBranch:
759
783
  element_name=element_name,
760
784
  )
761
785
 
786
+ def validate_display_label(self) -> None:
787
+ self.display_labels = DisplayLabels()
788
+ for name in self.all_names:
789
+ node_schema = self.get(name=name, duplicate=False)
790
+
791
+ if node_schema.display_label is None and node_schema.display_labels:
792
+ update_candidate = self.get(name=name, duplicate=True)
793
+ if len(node_schema.display_labels) == 1:
794
+ # If the previous display_labels consist of a single attribute convert
795
+ # it to an attribute based display label
796
+ update_candidate.display_label = _format_display_label_component(
797
+ component=node_schema.display_labels[0]
798
+ )
799
+ else:
800
+ # If the previous display label consists of multiple attributes
801
+ # convert it to a Jinja2 based display label
802
+ update_candidate.display_label = " ".join(
803
+ [
804
+ f"{{{{ {_format_display_label_component(component=display_label)} }}}}"
805
+ for display_label in node_schema.display_labels
806
+ ]
807
+ )
808
+ self.set(name=name, schema=update_candidate)
809
+
810
+ node_schema = self.get(name=name, duplicate=False)
811
+ if not node_schema.display_label:
812
+ continue
813
+
814
+ self._validate_display_label(node=node_schema)
815
+
762
816
  def validate_display_labels(self) -> None:
763
817
  for name in self.all_names:
764
818
  node_schema = self.get(name=name, duplicate=False)
@@ -867,7 +921,14 @@ class SchemaBranch:
867
921
  # Mapping relationship identifiers -> list of attributes paths
868
922
  rel_schemas_to_paths: dict[str, tuple[MainSchemaTypes, list[str]]] = {}
869
923
 
924
+ visited_paths: list[str] = []
870
925
  for hfid_path in node_schema.human_friendly_id:
926
+ if config.SETTINGS.main.schema_strict_mode and hfid_path in visited_paths:
927
+ raise ValidationError(
928
+ f"HFID of {node_schema.kind} cannot use the same path more than once: {hfid_path}"
929
+ )
930
+
931
+ visited_paths.append(hfid_path)
871
932
  schema_path = self.validate_schema_path(
872
933
  node_schema=node_schema,
873
934
  path=hfid_path,
@@ -1144,6 +1205,50 @@ class SchemaBranch:
1144
1205
  node=node_schema, attribute=attribute, generic=generic_schema
1145
1206
  )
1146
1207
 
1208
+ def _validate_display_label(self, node: MainSchemaTypes) -> None:
1209
+ if not node.display_label:
1210
+ return
1211
+
1212
+ if not any(c in node.display_label for c in "{}"):
1213
+ schema_path = self.validate_schema_path(
1214
+ node_schema=node,
1215
+ path=node.display_label,
1216
+ allowed_path_types=SchemaElementPathType.ATTR_WITH_PROP,
1217
+ element_name="display_label - non Jinja2",
1218
+ )
1219
+ if schema_path.attribute_schema and node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
1220
+ self.display_labels.register_attribute_based_display_label(
1221
+ kind=node.kind, attribute_name=schema_path.attribute_schema.name
1222
+ )
1223
+ return
1224
+
1225
+ jinja_template = Jinja2Template(template=node.display_label)
1226
+ try:
1227
+ variables = jinja_template.get_variables()
1228
+ jinja_template.validate(restricted=config.SETTINGS.security.restrict_untrusted_jinja2_filters)
1229
+ except (JinjaTemplateOperationViolationError, JinjaTemplateError) as exc:
1230
+ raise ValueError(
1231
+ f"{node.kind}: display_label is set to a jinja2 template, but has an invalid template: {exc.message}"
1232
+ ) from exc
1233
+
1234
+ allowed_path_types = (
1235
+ SchemaElementPathType.ATTR_WITH_PROP
1236
+ | SchemaElementPathType.REL_ONE_MANDATORY_ATTR_WITH_PROP
1237
+ | SchemaElementPathType.REL_ONE_ATTR_WITH_PROP
1238
+ )
1239
+ for variable in variables:
1240
+ schema_path = self.validate_schema_path(
1241
+ node_schema=node, path=variable, allowed_path_types=allowed_path_types, element_name="display_label"
1242
+ )
1243
+
1244
+ if schema_path.is_type_attribute and schema_path.active_attribute_schema.name == "display_label":
1245
+ raise ValueError(f"{node.kind}: display_label the '{variable}' variable is a reference to itself")
1246
+
1247
+ if node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
1248
+ self.display_labels.register_template_schema_path(
1249
+ kind=node.kind, schema_path=schema_path, template=node.display_label
1250
+ )
1251
+
1147
1252
  def _validate_computed_attribute(self, node: NodeSchema, attribute: AttributeSchema) -> None:
1148
1253
  if not attribute.computed_attribute or attribute.computed_attribute.kind == ComputedAttributeKind.USER:
1149
1254
  return
@@ -1236,6 +1341,81 @@ class SchemaBranch:
1236
1341
  f"{node.kind}: Relationship {rel.name!r} max_count must be 0 or greater than 1 when cardinality is MANY"
1237
1342
  )
1238
1343
 
1344
+ def validate_inherited_relationships_fields(self) -> None:
1345
+ for name in self.node_names:
1346
+ node_schema = self.get(name=name, duplicate=False)
1347
+ if not node_schema.inherit_from:
1348
+ continue
1349
+
1350
+ self.validate_node_inherited_relationship_fields(node_schema)
1351
+
1352
+ def validate_node_inherited_relationship_fields(self, node_schema: NodeSchema) -> None:
1353
+ generics = [self.get(name=node_name, duplicate=False) for node_name in node_schema.inherit_from]
1354
+ relationship_names = [node.relationship_names for node in generics]
1355
+ related_relationship_names = set().union(
1356
+ *[
1357
+ set(relationship_name_a) & set(relationship_name_b)
1358
+ for index, relationship_name_a in enumerate(relationship_names)
1359
+ for relationship_name_b in relationship_names[index + 1 :]
1360
+ ]
1361
+ )
1362
+ # Check that the relationship properties match
1363
+ # for every generic node in generics list having related relationship names
1364
+ for index, generic_a in enumerate(generics):
1365
+ for generic_b in generics[index + 1 :]:
1366
+ for relationship_name in related_relationship_names:
1367
+ try:
1368
+ relationship_a = generic_a.get_relationship(name=relationship_name)
1369
+ relationship_b = generic_b.get_relationship(name=relationship_name)
1370
+ except ValueError:
1371
+ continue
1372
+
1373
+ matched, _property = self._check_relationship_properties_match(
1374
+ relationship_a=relationship_a, relationship_b=relationship_b
1375
+ )
1376
+ if not matched:
1377
+ raise ValueError(
1378
+ f"{node_schema.kind} inherits from '{generic_a.kind}' & '{generic_b.kind}'"
1379
+ f" with different '{_property}' on the '{relationship_name}' relationship"
1380
+ )
1381
+
1382
+ def _check_relationship_properties_match(
1383
+ self, relationship_a: RelationshipSchema, relationship_b: RelationshipSchema
1384
+ ) -> tuple[bool, str | None]:
1385
+ compulsorily_matching_properties = (
1386
+ "name",
1387
+ "peer",
1388
+ "kind",
1389
+ "identifier",
1390
+ "cardinality",
1391
+ "min_count",
1392
+ "max_count",
1393
+ "common_parent",
1394
+ "common_relatives",
1395
+ "optional",
1396
+ "branch",
1397
+ "direction",
1398
+ "on_delete",
1399
+ "read_only",
1400
+ "hierarchical",
1401
+ "allow_override",
1402
+ )
1403
+ for _property in compulsorily_matching_properties:
1404
+ if not hasattr(relationship_a, _property) or not hasattr(relationship_b, _property):
1405
+ continue
1406
+
1407
+ equal_delete_actions = (None, RelationshipDeleteBehavior.NO_ACTION)
1408
+ if (
1409
+ _property == "on_delete"
1410
+ and getattr(relationship_a, _property) in equal_delete_actions
1411
+ and getattr(relationship_b, _property) in equal_delete_actions
1412
+ ):
1413
+ continue
1414
+
1415
+ if getattr(relationship_a, _property) != getattr(relationship_b, _property):
1416
+ return False, _property
1417
+ return True, None
1418
+
1239
1419
  def process_dropdowns(self) -> None:
1240
1420
  for name in self.all_names:
1241
1421
  node = self.get(name=name, duplicate=False)
@@ -1364,6 +1544,34 @@ class SchemaBranch:
1364
1544
  node.uniqueness_constraints = [hfid_uniqueness_constraint]
1365
1545
  self.set(name=node.kind, schema=node)
1366
1546
 
1547
+ def register_human_friendly_id(self) -> None:
1548
+ """Register HFID automations
1549
+
1550
+ Register the HFIDs after all processing and validation has been done.
1551
+ """
1552
+
1553
+ self.hfids = HFIDs()
1554
+ for name in self.generic_names_without_templates + self.node_names:
1555
+ node_schema = self.get(name=name, duplicate=False)
1556
+
1557
+ if not node_schema.human_friendly_id:
1558
+ continue
1559
+
1560
+ allowed_types = SchemaElementPathType.ATTR_WITH_PROP | SchemaElementPathType.REL_ONE_MANDATORY_ATTR
1561
+
1562
+ for hfid_path in node_schema.human_friendly_id:
1563
+ schema_path = self.validate_schema_path(
1564
+ node_schema=node_schema,
1565
+ path=hfid_path,
1566
+ allowed_path_types=allowed_types,
1567
+ element_name="human_friendly_id",
1568
+ )
1569
+
1570
+ if node_schema.is_node_schema and node_schema.namespace not in ["Schema", "Internal"]:
1571
+ self.hfids.register_hfid_schema_path(
1572
+ kind=node_schema.kind, schema_path=schema_path, hfid=node_schema.human_friendly_id
1573
+ )
1574
+
1367
1575
  def process_hierarchy(self) -> None:
1368
1576
  for name in self.nodes.keys():
1369
1577
  node = self.get_node(name=name, duplicate=False)
@@ -1619,10 +1827,48 @@ class SchemaBranch:
1619
1827
 
1620
1828
  self.set(name=name, schema=node)
1621
1829
 
1830
+ def process_relationships_state(self) -> None:
1831
+ for name in self.node_names + self.generic_names_without_templates:
1832
+ node = self.get(name=name, duplicate=False)
1833
+ if node.id or (not node.id and not node.relationships):
1834
+ continue
1835
+
1836
+ filtered_relationships = [
1837
+ relationship for relationship in node.relationships if relationship.state != HashableModelState.ABSENT
1838
+ ]
1839
+ if len(filtered_relationships) == len(node.relationships):
1840
+ continue
1841
+ updated_node = node.duplicate()
1842
+ updated_node.relationships = filtered_relationships
1843
+ self.set(name=name, schema=updated_node)
1844
+
1845
+ def process_attributes_state(self) -> None:
1846
+ for name in self.node_names + self.generic_names_without_templates:
1847
+ node = self.get(name=name, duplicate=False)
1848
+ if not node.attributes:
1849
+ continue
1850
+
1851
+ filtered_attributes = [
1852
+ attribute for attribute in node.attributes if attribute.state != HashableModelState.ABSENT
1853
+ ]
1854
+ if len(filtered_attributes) == len(node.attributes):
1855
+ continue
1856
+ updated_node = node.duplicate()
1857
+ updated_node.attributes = filtered_attributes
1858
+ self.set(name=name, schema=updated_node)
1859
+
1860
+ def process_nodes_state(self) -> None:
1861
+ for name in self.node_names + self.generic_names_without_templates:
1862
+ node = self.get(name=name, duplicate=False)
1863
+ if not node.id and node.state == HashableModelState.ABSENT:
1864
+ self.delete(name=name)
1865
+
1622
1866
  def _generate_weight_generics(self) -> None:
1623
1867
  """Generate order_weight for all generic schemas."""
1624
1868
  for name in self.generic_names:
1625
1869
  node = self.get(name=name, duplicate=False)
1870
+ if node.namespace == "Template":
1871
+ continue
1626
1872
 
1627
1873
  items_to_update = [item for item in node.attributes + node.relationships if not item.order_weight]
1628
1874
 
@@ -1686,10 +1932,36 @@ class SchemaBranch:
1686
1932
 
1687
1933
  self.set(name=name, schema=template)
1688
1934
 
1935
+ def _generate_generics_templates_weight(self) -> None:
1936
+ """Generate order_weight for generic templates.
1937
+
1938
+ The order of the fields for the template must respect the order of the node.
1939
+ """
1940
+ for name in self.generic_names:
1941
+ generic_node = self.get(name=name, duplicate=False)
1942
+ try:
1943
+ template = self.get(name=self._get_object_template_kind(node_kind=generic_node.kind), duplicate=True)
1944
+ except SchemaNotFoundError:
1945
+ continue
1946
+
1947
+ generic_node_weights = {
1948
+ item.name: item.order_weight
1949
+ for item in generic_node.attributes + generic_node.relationships
1950
+ if item.order_weight is not None
1951
+ }
1952
+
1953
+ for item in template.attributes + template.relationships:
1954
+ if item.order_weight:
1955
+ continue
1956
+ item.order_weight = generic_node_weights[item.name] if item.name in generic_node_weights else None
1957
+
1958
+ self.set(name=template.kind, schema=template)
1959
+
1689
1960
  def generate_weight(self) -> None:
1690
1961
  self._generate_weight_generics()
1691
1962
  self._generate_weight_nodes_profiles()
1692
1963
  self._generate_weight_templates()
1964
+ self._generate_generics_templates_weight()
1693
1965
 
1694
1966
  def cleanup_inherited_elements(self) -> None:
1695
1967
  for name in self.node_names:
@@ -1942,7 +2214,7 @@ class SchemaBranch:
1942
2214
 
1943
2215
  profiles_rel_settings: dict[str, Any] = {
1944
2216
  "name": "profiles",
1945
- "identifier": "node__profile",
2217
+ "identifier": PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
1946
2218
  "peer": InfrahubKind.PROFILE,
1947
2219
  "kind": RelationshipKind.PROFILE,
1948
2220
  "cardinality": RelationshipCardinality.MANY,
@@ -2005,7 +2277,7 @@ class SchemaBranch:
2005
2277
  relationships=[
2006
2278
  RelationshipSchema(
2007
2279
  name="related_nodes",
2008
- identifier="node__profile",
2280
+ identifier=PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
2009
2281
  peer=node.kind,
2010
2282
  kind=RelationshipKind.PROFILE,
2011
2283
  cardinality=RelationshipCardinality.MANY,
@@ -2015,7 +2287,7 @@ class SchemaBranch:
2015
2287
  )
2016
2288
 
2017
2289
  for node_attr in node.attributes:
2018
- if node_attr.read_only or node_attr.optional is False:
2290
+ if not node_attr.support_profiles:
2019
2291
  continue
2020
2292
  attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
2021
2293
  attr = attr_schema_class(
@@ -2320,3 +2592,16 @@ class SchemaBranch:
2320
2592
  updated_used_by_node = set(chain(template_schema_kinds, set(core_node_schema.used_by)))
2321
2593
  core_node_schema.used_by = sorted(updated_used_by_node)
2322
2594
  self.set(name=InfrahubKind.NODE, schema=core_node_schema)
2595
+
2596
+
2597
+ def _format_display_label_component(component: str) -> str:
2598
+ """Return correct format for display_label.
2599
+
2600
+ Previously both the format of 'name' and 'name__value' was
2601
+ supported this function ensures that the proper 'name__value'
2602
+ format is used
2603
+ """
2604
+ if "__" in component:
2605
+ return component
2606
+
2607
+ return f"{component}__value"
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from infrahub.core.schema import SchemaAttributePath
10
+
11
+
12
+ @dataclass
13
+ class TemplateLabel:
14
+ template: str
15
+ attributes: set[str] = field(default_factory=set)
16
+ relationships: set[str] = field(default_factory=set)
17
+ relationship_fields: dict[str, set[str]] = field(default_factory=dict)
18
+ filter_key: str = "ids"
19
+
20
+ @property
21
+ def fields(self) -> list[str]:
22
+ return sorted(list(self.attributes) + list(self.relationships))
23
+
24
+ @property
25
+ def has_related_components(self) -> bool:
26
+ """Indicate if the associated template use variables from relationships"""
27
+ return len(self.relationships) > 0
28
+
29
+ def get_hash(self) -> str:
30
+ return hashlib.md5(self.template.encode(), usedforsecurity=False).hexdigest()
31
+
32
+
33
+ @dataclass
34
+ class RelationshipIdentifier:
35
+ kind: str
36
+ filter_key: str
37
+ template: str
38
+
39
+ def __hash__(self) -> int:
40
+ return hash(f"{self.kind}::{self.filter_key}::{self.template}")
41
+
42
+
43
+ @dataclass
44
+ class RelationshipTriggers:
45
+ attributes: dict[str, set[RelationshipIdentifier]] = field(default_factory=dict)
46
+
47
+
48
+ class DisplayLabels:
49
+ def __init__(
50
+ self,
51
+ template_based_display_labels: dict[str, TemplateLabel] | None = None,
52
+ template_relationship_triggers: dict[str, RelationshipTriggers] | None = None,
53
+ ) -> None:
54
+ self._template_based_display_labels: dict[str, TemplateLabel] = template_based_display_labels or {}
55
+ self._template_relationship_triggers: dict[str, RelationshipTriggers] = template_relationship_triggers or {}
56
+
57
+ def duplicate(self) -> DisplayLabels:
58
+ """Clone the current object."""
59
+ return self.__class__(
60
+ template_based_display_labels=deepcopy(self._template_based_display_labels),
61
+ template_relationship_triggers=deepcopy(self._template_relationship_triggers),
62
+ )
63
+
64
+ def register_attribute_based_display_label(self, kind: str, attribute_name: str) -> None:
65
+ """Register nodes where the display label consists of a single defined attribute name."""
66
+ self._template_based_display_labels[kind] = TemplateLabel(
67
+ template=f"{{{{ {attribute_name}__value }}}}", attributes={attribute_name}
68
+ )
69
+
70
+ def register_template_schema_path(self, kind: str, schema_path: SchemaAttributePath, template: str) -> None:
71
+ """Register Jinja2 template based display labels using the schema path of each impacted variable in the node."""
72
+
73
+ if kind not in self._template_based_display_labels:
74
+ self._template_based_display_labels[kind] = TemplateLabel(template=template)
75
+
76
+ if schema_path.is_type_attribute:
77
+ self._template_based_display_labels[kind].attributes.add(schema_path.active_attribute_schema.name)
78
+ elif schema_path.is_type_relationship and schema_path.related_schema:
79
+ self._template_based_display_labels[kind].relationships.add(schema_path.active_relationship_schema.name)
80
+ if (
81
+ schema_path.active_relationship_schema.name
82
+ not in self._template_based_display_labels[kind].relationship_fields
83
+ ):
84
+ self._template_based_display_labels[kind].relationship_fields[
85
+ schema_path.active_relationship_schema.name
86
+ ] = set()
87
+ self._template_based_display_labels[kind].relationship_fields[
88
+ schema_path.active_relationship_schema.name
89
+ ].add(schema_path.active_attribute_schema.name)
90
+
91
+ if schema_path.related_schema.kind not in self._template_relationship_triggers:
92
+ self._template_relationship_triggers[schema_path.related_schema.kind] = RelationshipTriggers()
93
+ if (
94
+ schema_path.active_attribute_schema.name
95
+ not in self._template_relationship_triggers[schema_path.related_schema.kind].attributes
96
+ ):
97
+ self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
98
+ schema_path.active_attribute_schema.name
99
+ ] = set()
100
+ self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
101
+ schema_path.active_attribute_schema.name
102
+ ].add(
103
+ RelationshipIdentifier(
104
+ kind=kind, filter_key=f"{schema_path.active_relationship_schema.name}__ids", template=template
105
+ )
106
+ )
107
+
108
+ def targets_node(self, kind: str) -> bool:
109
+ """Indicates if there is a display_label defined for the targeted node"""
110
+ return kind in self._template_based_display_labels
111
+
112
+ def get_template_node(self, kind: str) -> TemplateLabel:
113
+ """Return node kinds together with their template definitions."""
114
+ return self._template_based_display_labels[kind]
115
+
116
+ def get_template_nodes(self) -> dict[str, TemplateLabel]:
117
+ """Return node kinds together with their template definitions."""
118
+ return self._template_based_display_labels
119
+
120
+ def get_related_trigger_nodes(self) -> dict[str, RelationshipTriggers]:
121
+ """Return node kinds that other nodes use within their templates for display_labels."""
122
+ return self._template_relationship_triggers
123
+
124
+ def get_related_template(self, related_kind: str, target_kind: str) -> TemplateLabel:
125
+ relationship_trigger = self._template_relationship_triggers[related_kind]
126
+ for applicable_kinds in relationship_trigger.attributes.values():
127
+ for relationship_identifier in applicable_kinds:
128
+ if target_kind == relationship_identifier.kind:
129
+ template_label = self.get_template_node(kind=target_kind)
130
+ template_label.filter_key = relationship_identifier.filter_key
131
+ return template_label
132
+
133
+ raise ValueError(
134
+ f"Unable to find registered template for {target_kind} registered on related node {related_kind}"
135
+ )