infrahub-server 1.5.4__py3-none-any.whl → 1.6.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 (129) hide show
  1. infrahub/api/artifact.py +5 -3
  2. infrahub/auth.py +5 -6
  3. infrahub/cli/db.py +3 -3
  4. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +2 -2
  5. infrahub/cli/dev.py +30 -0
  6. infrahub/config.py +62 -14
  7. infrahub/constants/database.py +5 -5
  8. infrahub/core/branch/models.py +24 -6
  9. infrahub/core/constants/__init__.py +1 -0
  10. infrahub/core/diff/model/diff.py +2 -2
  11. infrahub/core/graph/constraints.py +2 -2
  12. infrahub/core/manager.py +191 -60
  13. infrahub/core/merge.py +29 -2
  14. infrahub/core/migrations/shared.py +2 -2
  15. infrahub/core/models.py +5 -6
  16. infrahub/core/node/__init__.py +12 -6
  17. infrahub/core/node/create.py +36 -8
  18. infrahub/core/node/ipam.py +4 -4
  19. infrahub/core/node/node_property_attribute.py +2 -2
  20. infrahub/core/node/standard.py +1 -1
  21. infrahub/core/query/attribute.py +1 -1
  22. infrahub/core/query/branch.py +11 -0
  23. infrahub/core/query/node.py +9 -5
  24. infrahub/core/query/standard_node.py +3 -0
  25. infrahub/core/relationship/model.py +15 -10
  26. infrahub/core/schema/__init__.py +3 -3
  27. infrahub/core/schema/generic_schema.py +1 -1
  28. infrahub/core/schema/schema_branch.py +35 -16
  29. infrahub/core/task/user_task.py +2 -2
  30. infrahub/core/validators/determiner.py +3 -6
  31. infrahub/core/validators/enum.py +2 -2
  32. infrahub/database/__init__.py +1 -1
  33. infrahub/dependencies/interface.py +2 -2
  34. infrahub/events/constants.py +2 -2
  35. infrahub/git/base.py +42 -1
  36. infrahub/git/models.py +2 -1
  37. infrahub/git/repository.py +5 -1
  38. infrahub/git/tasks.py +28 -1
  39. infrahub/git/utils.py +9 -0
  40. infrahub/graphql/analyzer.py +4 -4
  41. infrahub/graphql/loaders/peers.py +6 -0
  42. infrahub/graphql/mutations/computed_attribute.py +1 -1
  43. infrahub/graphql/mutations/convert_object_type.py +1 -1
  44. infrahub/graphql/mutations/display_label.py +1 -1
  45. infrahub/graphql/mutations/hfid.py +1 -1
  46. infrahub/graphql/mutations/ipam.py +1 -1
  47. infrahub/graphql/mutations/profile.py +9 -1
  48. infrahub/graphql/mutations/relationship.py +2 -2
  49. infrahub/graphql/mutations/resource_manager.py +1 -1
  50. infrahub/graphql/queries/__init__.py +2 -1
  51. infrahub/graphql/queries/branch.py +58 -3
  52. infrahub/graphql/queries/ipam.py +9 -4
  53. infrahub/graphql/queries/resource_manager.py +7 -11
  54. infrahub/graphql/queries/search.py +5 -6
  55. infrahub/graphql/resolvers/ipam.py +20 -0
  56. infrahub/graphql/resolvers/many_relationship.py +12 -11
  57. infrahub/graphql/resolvers/resolver.py +6 -2
  58. infrahub/graphql/resolvers/single_relationship.py +1 -11
  59. infrahub/graphql/schema.py +2 -0
  60. infrahub/graphql/types/__init__.py +3 -1
  61. infrahub/graphql/types/branch.py +98 -2
  62. infrahub/lock.py +6 -6
  63. infrahub/log.py +1 -1
  64. infrahub/message_bus/messages/__init__.py +0 -12
  65. infrahub/patch/constants.py +2 -2
  66. infrahub/profiles/node_applier.py +9 -0
  67. infrahub/proposed_change/tasks.py +1 -1
  68. infrahub/task_manager/task.py +4 -4
  69. infrahub/telemetry/constants.py +2 -2
  70. infrahub/trigger/models.py +2 -2
  71. infrahub/trigger/setup.py +6 -9
  72. infrahub/utils.py +19 -1
  73. infrahub/validators/tasks.py +1 -1
  74. infrahub/workers/infrahub_async.py +39 -1
  75. infrahub_sdk/async_typer.py +2 -1
  76. infrahub_sdk/batch.py +2 -2
  77. infrahub_sdk/client.py +121 -10
  78. infrahub_sdk/config.py +2 -2
  79. infrahub_sdk/ctl/branch.py +176 -2
  80. infrahub_sdk/ctl/check.py +3 -3
  81. infrahub_sdk/ctl/cli.py +2 -2
  82. infrahub_sdk/ctl/cli_commands.py +10 -9
  83. infrahub_sdk/ctl/generator.py +2 -2
  84. infrahub_sdk/ctl/graphql.py +3 -4
  85. infrahub_sdk/ctl/importer.py +2 -3
  86. infrahub_sdk/ctl/repository.py +5 -6
  87. infrahub_sdk/ctl/task.py +2 -4
  88. infrahub_sdk/ctl/utils.py +4 -4
  89. infrahub_sdk/ctl/validate.py +1 -2
  90. infrahub_sdk/diff.py +80 -3
  91. infrahub_sdk/graphql/constants.py +14 -1
  92. infrahub_sdk/graphql/renderers.py +5 -1
  93. infrahub_sdk/node/attribute.py +10 -10
  94. infrahub_sdk/node/constants.py +2 -3
  95. infrahub_sdk/node/node.py +54 -11
  96. infrahub_sdk/node/related_node.py +1 -2
  97. infrahub_sdk/node/relationship.py +1 -2
  98. infrahub_sdk/object_store.py +4 -4
  99. infrahub_sdk/operation.py +2 -2
  100. infrahub_sdk/protocols_base.py +0 -1
  101. infrahub_sdk/protocols_generator/generator.py +1 -1
  102. infrahub_sdk/pytest_plugin/items/jinja2_transform.py +1 -1
  103. infrahub_sdk/pytest_plugin/models.py +1 -1
  104. infrahub_sdk/pytest_plugin/plugin.py +1 -1
  105. infrahub_sdk/query_groups.py +2 -2
  106. infrahub_sdk/schema/__init__.py +10 -14
  107. infrahub_sdk/schema/main.py +2 -2
  108. infrahub_sdk/schema/repository.py +2 -2
  109. infrahub_sdk/spec/object.py +2 -2
  110. infrahub_sdk/spec/range_expansion.py +1 -1
  111. infrahub_sdk/template/__init__.py +2 -1
  112. infrahub_sdk/transfer/importer/json.py +3 -3
  113. infrahub_sdk/types.py +2 -2
  114. infrahub_sdk/utils.py +2 -2
  115. {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info}/METADATA +58 -59
  116. {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info}/RECORD +239 -245
  117. {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info}/WHEEL +1 -1
  118. infrahub_server-1.6.0.dist-info/entry_points.txt +12 -0
  119. infrahub_testcontainers/container.py +2 -2
  120. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  121. infrahub_testcontainers/docker-compose.test.yml +1 -1
  122. infrahub/core/schema/generated/__init__.py +0 -0
  123. infrahub/core/schema/generated/attribute_schema.py +0 -133
  124. infrahub/core/schema/generated/base_node_schema.py +0 -111
  125. infrahub/core/schema/generated/genericnode_schema.py +0 -30
  126. infrahub/core/schema/generated/node_schema.py +0 -40
  127. infrahub/core/schema/generated/relationship_schema.py +0 -141
  128. infrahub_server-1.5.4.dist-info/entry_points.txt +0 -13
  129. {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info/licenses}/LICENSE.txt +0 -0
infrahub/core/manager.py CHANGED
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Iterable, Literal, TypeVar, overload
5
5
 
6
6
  from infrahub_sdk.utils import deep_merge_dict, is_valid_uuid
7
7
 
8
- from infrahub.core.constants import RelationshipCardinality, RelationshipDirection
8
+ from infrahub.core.constants import InfrahubKind, RelationshipCardinality, RelationshipDirection
9
9
  from infrahub.core.node import Node
10
10
  from infrahub.core.node.delete_validator import NodeDeleteValidator
11
11
  from infrahub.core.query.node import (
@@ -59,7 +59,7 @@ def identify_node_class(node: NodeToProcess) -> type[Node]:
59
59
  return Node
60
60
 
61
61
 
62
- def get_schema(
62
+ def get_schema[SchemaProtocol](
63
63
  db: InfrahubDatabase,
64
64
  branch: Branch,
65
65
  node_schema: type[SchemaProtocol] | MainSchemaTypes | str,
@@ -188,17 +188,22 @@ class NodeManager:
188
188
  await query.execute(db=db)
189
189
  node_ids = query.get_node_ids()
190
190
 
191
- # if display_label or hfid has been requested we need to ensure we are querying the right fields
192
- if fields and "display_label" in fields:
191
+ if (
192
+ fields
193
+ and "identifier" in fields
194
+ and node_schema.kind
195
+ in [
196
+ InfrahubKind.BASEPERMISSION,
197
+ InfrahubKind.GLOBALPERMISSION,
198
+ InfrahubKind.OBJECTPERMISSION,
199
+ ]
200
+ ):
201
+ # This is a workaround to ensure we are querying the right fields for permissions
202
+ # The identifier for permissions needs the same fields as the display label
193
203
  schema_branch = db.schema.get_schema_branch(name=branch.name)
194
204
  display_label_fields = schema_branch.generate_fields_for_display_label(name=node_schema.kind)
195
205
  if display_label_fields:
196
- fields = deep_merge_dict(dicta=fields, dictb=display_label_fields)
197
-
198
- if fields and "hfid" in fields and node_schema.human_friendly_id:
199
- hfid_fields = node_schema.generate_fields_for_hfid()
200
- if hfid_fields:
201
- fields = deep_merge_dict(dicta=fields, dictb=hfid_fields)
206
+ deep_merge_dict(dicta=fields, dictb=display_label_fields)
202
207
 
203
208
  response = await cls.get_many(
204
209
  ids=node_ids,
@@ -300,6 +305,8 @@ class NodeManager:
300
305
  branch: Branch | str | None = None,
301
306
  branch_agnostic: bool = False,
302
307
  fetch_peers: bool = False,
308
+ include_source: bool = False,
309
+ include_owner: bool = False,
303
310
  ) -> list[Relationship]:
304
311
  branch = await registry.get_branch(branch=branch, db=db)
305
312
  at = Timestamp(at)
@@ -324,24 +331,31 @@ class NodeManager:
324
331
  if not peers_info:
325
332
  return []
326
333
 
327
- # if display_label has been requested we need to ensure we are querying the right fields
328
- if fields and "display_label" in fields:
334
+ if fields and "identifier" in fields:
335
+ # This is a workaround to ensure we are querying the right fields for permissions
336
+ # The identifier for permissions needs the same fields as the display label
329
337
  peer_schema = schema.get_peer_schema(db=db, branch=branch)
330
- schema_branch = db.schema.get_schema_branch(name=branch.name)
331
- display_label_fields = schema_branch.generate_fields_for_display_label(name=peer_schema.kind)
332
- if display_label_fields:
333
- fields = deep_merge_dict(dicta=fields, dictb=display_label_fields)
334
-
335
- if fields and "hfid" in fields:
336
- peer_schema = schema.get_peer_schema(db=db, branch=branch)
337
- hfid_fields = peer_schema.generate_fields_for_hfid()
338
- if hfid_fields:
339
- fields = deep_merge_dict(dicta=fields, dictb=hfid_fields)
338
+ if peer_schema.kind in [
339
+ InfrahubKind.BASEPERMISSION,
340
+ InfrahubKind.GLOBALPERMISSION,
341
+ InfrahubKind.OBJECTPERMISSION,
342
+ ]:
343
+ schema_branch = db.schema.get_schema_branch(name=branch.name)
344
+ display_label_fields = schema_branch.generate_fields_for_display_label(name=peer_schema.kind)
345
+ if display_label_fields:
346
+ deep_merge_dict(dicta=fields, dictb=display_label_fields)
340
347
 
341
348
  if fetch_peers:
342
349
  peer_ids = [peer.peer_id for peer in peers_info]
343
350
  peer_nodes = await cls.get_many(
344
- db=db, ids=peer_ids, fields=fields, at=at, branch=branch, branch_agnostic=branch_agnostic
351
+ db=db,
352
+ ids=peer_ids,
353
+ fields=fields,
354
+ at=at,
355
+ branch=branch,
356
+ branch_agnostic=branch_agnostic,
357
+ include_source=include_source,
358
+ include_owner=include_owner,
345
359
  )
346
360
 
347
361
  results = []
@@ -420,15 +434,6 @@ class NodeManager:
420
434
  if not peers_ids:
421
435
  return {}
422
436
 
423
- hierarchy_schema = node_schema.get_hierarchy_schema(db=db, branch=branch)
424
-
425
- # if display_label has been requested we need to ensure we are querying the right fields
426
- if fields and "display_label" in fields:
427
- schema_branch = db.schema.get_schema_branch(name=branch.name)
428
- display_label_fields = schema_branch.generate_fields_for_display_label(name=hierarchy_schema.kind)
429
- if display_label_fields:
430
- fields = deep_merge_dict(dicta=fields, dictb=display_label_fields)
431
-
432
437
  return await cls.get_many(
433
438
  db=db, ids=peers_ids, fields=fields, at=at, branch=branch, include_owner=True, include_source=True
434
439
  )
@@ -455,7 +460,7 @@ class NodeManager:
455
460
  branch: Branch | str | None = ...,
456
461
  id: str | None = ...,
457
462
  hfid: list[str] | None = ...,
458
- ) -> Any: ...
463
+ ) -> Node: ...
459
464
 
460
465
  @classmethod
461
466
  async def find_object(
@@ -466,10 +471,7 @@ class NodeManager:
466
471
  branch: Branch | str | None = None,
467
472
  id: str | None = None,
468
473
  hfid: list[str] | None = None,
469
- ) -> Any:
470
- if not id and not hfid:
471
- raise ProcessingError(message="either id or hfid must be provided.")
472
-
474
+ ) -> Node | SchemaProtocol:
473
475
  if id and is_valid_uuid(id):
474
476
  return await cls.get_one(
475
477
  db=db,
@@ -494,16 +496,19 @@ class NodeManager:
494
496
  raise_on_error=True,
495
497
  )
496
498
 
497
- return await cls.get_one_by_default_filter(
498
- db=db,
499
- kind=kind,
500
- id=id,
501
- branch=branch,
502
- at=at,
503
- include_owner=True,
504
- include_source=True,
505
- raise_on_error=True,
506
- )
499
+ if id:
500
+ return await cls.get_one_by_default_filter(
501
+ db=db,
502
+ kind=kind,
503
+ id=id,
504
+ branch=branch,
505
+ at=at,
506
+ include_owner=True,
507
+ include_source=True,
508
+ raise_on_error=True,
509
+ )
510
+
511
+ raise ProcessingError(message="either id or hfid must be provided.")
507
512
 
508
513
  @overload
509
514
  @classmethod
@@ -557,7 +562,43 @@ class NodeManager:
557
562
  prefetch_relationships: bool = ...,
558
563
  account=...,
559
564
  branch_agnostic: bool = ...,
560
- ) -> SchemaProtocol: ...
565
+ ) -> SchemaProtocol | None: ...
566
+
567
+ @overload
568
+ @classmethod
569
+ async def get_one_by_default_filter(
570
+ cls,
571
+ db: InfrahubDatabase,
572
+ id: str,
573
+ kind: str,
574
+ raise_on_error: Literal[False] = ...,
575
+ fields: dict | None = ...,
576
+ at: Timestamp | str | None = ...,
577
+ branch: Branch | str | None = ...,
578
+ include_source: bool = ...,
579
+ include_owner: bool = ...,
580
+ prefetch_relationships: bool = ...,
581
+ account=...,
582
+ branch_agnostic: bool = ...,
583
+ ) -> Node | None: ...
584
+
585
+ @overload
586
+ @classmethod
587
+ async def get_one_by_default_filter(
588
+ cls,
589
+ db: InfrahubDatabase,
590
+ id: str,
591
+ kind: str,
592
+ raise_on_error: Literal[True] = ...,
593
+ fields: dict | None = ...,
594
+ at: Timestamp | str | None = ...,
595
+ branch: Branch | str | None = ...,
596
+ include_source: bool = ...,
597
+ include_owner: bool = ...,
598
+ prefetch_relationships: bool = ...,
599
+ account=...,
600
+ branch_agnostic: bool = ...,
601
+ ) -> Node: ...
561
602
 
562
603
  @overload
563
604
  @classmethod
@@ -575,7 +616,7 @@ class NodeManager:
575
616
  prefetch_relationships: bool = ...,
576
617
  account=...,
577
618
  branch_agnostic: bool = ...,
578
- ) -> Any: ...
619
+ ) -> Node | None: ...
579
620
 
580
621
  @classmethod
581
622
  async def get_one_by_default_filter(
@@ -592,7 +633,7 @@ class NodeManager:
592
633
  prefetch_relationships: bool = False,
593
634
  account=None,
594
635
  branch_agnostic: bool = False,
595
- ) -> Any:
636
+ ) -> Node | SchemaProtocol | None:
596
637
  branch = await registry.get_branch(branch=branch, db=db)
597
638
  at = Timestamp(at)
598
639
 
@@ -688,7 +729,7 @@ class NodeManager:
688
729
  prefetch_relationships: bool = ...,
689
730
  account=...,
690
731
  branch_agnostic: bool = ...,
691
- ) -> SchemaProtocol: ...
732
+ ) -> SchemaProtocol | None: ...
692
733
 
693
734
  @overload
694
735
  @classmethod
@@ -706,7 +747,25 @@ class NodeManager:
706
747
  prefetch_relationships: bool = ...,
707
748
  account=...,
708
749
  branch_agnostic: bool = ...,
709
- ) -> Any: ...
750
+ ) -> Node: ...
751
+
752
+ @overload
753
+ @classmethod
754
+ async def get_one_by_hfid(
755
+ cls,
756
+ db: InfrahubDatabase,
757
+ hfid: list[str],
758
+ kind: str,
759
+ raise_on_error: Literal[False],
760
+ fields: dict | None = ...,
761
+ at: Timestamp | str | None = ...,
762
+ branch: Branch | str | None = ...,
763
+ include_source: bool = ...,
764
+ include_owner: bool = ...,
765
+ prefetch_relationships: bool = ...,
766
+ account=...,
767
+ branch_agnostic: bool = ...,
768
+ ) -> Node | None: ...
710
769
 
711
770
  @overload
712
771
  @classmethod
@@ -724,7 +783,7 @@ class NodeManager:
724
783
  prefetch_relationships: bool = ...,
725
784
  account=...,
726
785
  branch_agnostic: bool = ...,
727
- ) -> Any: ...
786
+ ) -> Node | None: ...
728
787
 
729
788
  @classmethod
730
789
  async def get_one_by_hfid(
@@ -741,7 +800,7 @@ class NodeManager:
741
800
  prefetch_relationships: bool = False,
742
801
  account=None,
743
802
  branch_agnostic: bool = False,
744
- ) -> Any:
803
+ ) -> Node | SchemaProtocol | None:
745
804
  branch = await registry.get_branch(branch=branch, db=db)
746
805
  at = Timestamp(at)
747
806
 
@@ -770,14 +829,14 @@ class NodeManager:
770
829
  for key, item in zip(node_schema.human_friendly_id, hfid, strict=False):
771
830
  path = node_schema.parse_schema_path(path=key, schema=registry.schema.get_schema_branch(name=branch.name))
772
831
 
773
- if path.is_type_relationship:
832
+ if path.is_type_relationship and path.related_schema:
774
833
  rel_schema = path.related_schema
775
834
  # Keep the relationship attribute path and parse it
776
835
  path = rel_schema.parse_schema_path(
777
836
  path=key.split("__", maxsplit=1)[1], schema=registry.schema.get_schema_branch(name=branch.name)
778
837
  )
779
838
 
780
- filters[key] = path.attribute_schema.get_class().deserialize_from_string(item)
839
+ filters[key] = path.active_attribute_schema.get_class().deserialize_from_string(item)
781
840
 
782
841
  items = await NodeManager.query(
783
842
  db=db,
@@ -948,6 +1007,42 @@ class NodeManager:
948
1007
  branch_agnostic: bool = ...,
949
1008
  ) -> SchemaProtocol: ...
950
1009
 
1010
+ @overload
1011
+ @classmethod
1012
+ async def get_one(
1013
+ cls,
1014
+ id: str,
1015
+ db: InfrahubDatabase,
1016
+ kind: str,
1017
+ raise_on_error: Literal[True] = ...,
1018
+ fields: dict | None = ...,
1019
+ at: Timestamp | str | None = ...,
1020
+ branch: Branch | str | None = ...,
1021
+ include_source: bool = ...,
1022
+ include_owner: bool = ...,
1023
+ prefetch_relationships: bool = ...,
1024
+ account=...,
1025
+ branch_agnostic: bool = ...,
1026
+ ) -> Node: ...
1027
+
1028
+ @overload
1029
+ @classmethod
1030
+ async def get_one(
1031
+ cls,
1032
+ id: str,
1033
+ db: InfrahubDatabase,
1034
+ kind: str,
1035
+ raise_on_error: Literal[False] = ...,
1036
+ fields: dict | None = ...,
1037
+ at: Timestamp | str | None = ...,
1038
+ branch: Branch | str | None = ...,
1039
+ include_source: bool = ...,
1040
+ include_owner: bool = ...,
1041
+ prefetch_relationships: bool = ...,
1042
+ account=...,
1043
+ branch_agnostic: bool = ...,
1044
+ ) -> Node | None: ...
1045
+
951
1046
  @overload
952
1047
  @classmethod
953
1048
  async def get_one(
@@ -964,7 +1059,43 @@ class NodeManager:
964
1059
  prefetch_relationships: bool = ...,
965
1060
  account=...,
966
1061
  branch_agnostic: bool = ...,
967
- ) -> Any: ...
1062
+ ) -> Node | None: ...
1063
+
1064
+ @overload
1065
+ @classmethod
1066
+ async def get_one(
1067
+ cls,
1068
+ id: str,
1069
+ db: InfrahubDatabase,
1070
+ kind: None = ...,
1071
+ raise_on_error: Literal[True] = ...,
1072
+ fields: dict | None = ...,
1073
+ at: Timestamp | str | None = ...,
1074
+ branch: Branch | str | None = ...,
1075
+ include_source: bool = ...,
1076
+ include_owner: bool = ...,
1077
+ prefetch_relationships: bool = ...,
1078
+ account=...,
1079
+ branch_agnostic: bool = ...,
1080
+ ) -> Node: ...
1081
+
1082
+ @overload
1083
+ @classmethod
1084
+ async def get_one(
1085
+ cls,
1086
+ id: str,
1087
+ db: InfrahubDatabase,
1088
+ kind: None = ...,
1089
+ raise_on_error: Literal[False] = ...,
1090
+ fields: dict | None = ...,
1091
+ at: Timestamp | str | None = ...,
1092
+ branch: Branch | str | None = ...,
1093
+ include_source: bool = ...,
1094
+ include_owner: bool = ...,
1095
+ prefetch_relationships: bool = ...,
1096
+ account=...,
1097
+ branch_agnostic: bool = ...,
1098
+ ) -> Node | None: ...
968
1099
 
969
1100
  @overload
970
1101
  @classmethod
@@ -982,7 +1113,7 @@ class NodeManager:
982
1113
  prefetch_relationships: bool = ...,
983
1114
  account=...,
984
1115
  branch_agnostic: bool = ...,
985
- ) -> Any: ...
1116
+ ) -> Node | None: ...
986
1117
 
987
1118
  @classmethod
988
1119
  async def get_one(
@@ -999,7 +1130,7 @@ class NodeManager:
999
1130
  prefetch_relationships: bool = False,
1000
1131
  account=None,
1001
1132
  branch_agnostic: bool = False,
1002
- ) -> Any | None:
1133
+ ) -> Node | SchemaProtocol | None:
1003
1134
  """Return one node based on its ID."""
1004
1135
  branch = await registry.get_branch(branch=branch, db=db)
1005
1136
 
@@ -1264,7 +1395,7 @@ class NodeManager:
1264
1395
  db: InfrahubDatabase,
1265
1396
  nodes: list[Node],
1266
1397
  branch: Branch | str | None = None,
1267
- at: Timestamp | str | None = None,
1398
+ at: Timestamp | None = None,
1268
1399
  cascade_delete: bool = True,
1269
1400
  ) -> list[Node]:
1270
1401
  """Returns list of deleted nodes because of cascading deletes"""
infrahub/core/merge.py CHANGED
@@ -2,11 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from infrahub.core.constants import RepositoryInternalStatus
5
+ from infrahub.core.constants import InfrahubKind, RepositoryInternalStatus
6
6
  from infrahub.core.diff.model.path import BranchTrackingId
7
7
  from infrahub.core.manager import NodeManager
8
8
  from infrahub.core.models import SchemaUpdateValidationResult
9
- from infrahub.core.protocols import CoreRepository
9
+ from infrahub.core.protocols import CoreReadOnlyRepository, CoreRepository
10
10
  from infrahub.core.registry import registry
11
11
  from infrahub.core.timestamp import Timestamp
12
12
  from infrahub.exceptions import MergeFailedError, ValidationError
@@ -223,6 +223,32 @@ class BranchMerger:
223
223
  await self.diff_merger.rollback(at=self._merge_at)
224
224
 
225
225
  async def merge_repositories(self) -> None:
226
+ await self.merge_core_read_only_repositories()
227
+ await self.merge_core_repositories()
228
+
229
+ async def merge_core_read_only_repositories(self) -> None:
230
+ repos_in_main_list = await NodeManager.query(schema=CoreReadOnlyRepository, db=self.db)
231
+ repos_in_main = {repo.id: repo for repo in repos_in_main_list}
232
+
233
+ repos_in_branch_list = await NodeManager.query(
234
+ schema=CoreReadOnlyRepository, db=self.db, branch=self.source_branch
235
+ )
236
+ for repo in repos_in_branch_list:
237
+ if repo.id not in repos_in_main:
238
+ continue
239
+
240
+ model = GitRepositoryMerge(
241
+ repository_id=repo.id,
242
+ repository_name=repo.name.value,
243
+ source_branch=self.source_branch.name,
244
+ destination_branch=self.destination_branch.name,
245
+ destination_branch_id=str(self.destination_branch.get_uuid()),
246
+ internal_status=repo.internal_status.value,
247
+ repository_kind=InfrahubKind.READONLYREPOSITORY,
248
+ )
249
+ await self.workflow.submit_workflow(workflow=GIT_REPOSITORIES_MERGE, parameters={"model": model})
250
+
251
+ async def merge_core_repositories(self) -> None:
226
252
  # Collect all Repositories in Main because we'll need the commit in Main for each one.
227
253
  repos_in_main_list = await NodeManager.query(schema=CoreRepository, db=self.db)
228
254
  repos_in_main = {repo.id: repo for repo in repos_in_main_list}
@@ -245,5 +271,6 @@ class BranchMerger:
245
271
  destination_branch=self.destination_branch.name,
246
272
  destination_branch_id=str(self.destination_branch.get_uuid()),
247
273
  default_branch=repo.default_branch.value,
274
+ repository_kind=InfrahubKind.REPOSITORY,
248
275
  )
249
276
  await self.workflow.submit_workflow(workflow=GIT_REPOSITORIES_MERGE, parameters={"model": model})
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Sequence, TypeAlias
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
6
  from rich.console import Console
@@ -261,4 +261,4 @@ class MigrationRequiringRebase(BaseModel):
261
261
  raise NotImplementedError()
262
262
 
263
263
 
264
- MigrationTypes: TypeAlias = GraphMigration | InternalSchemaMigration | ArbitraryMigration | MigrationRequiringRebase
264
+ type MigrationTypes = GraphMigration | InternalSchemaMigration | ArbitraryMigration | MigrationRequiringRebase
infrahub/core/models.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import hashlib
4
5
  from typing import TYPE_CHECKING, Any
5
6
 
@@ -359,7 +360,7 @@ class SchemaUpdateValidationResult(BaseModel):
359
360
 
360
361
  def validate_migrations(self, migration_map: dict[str, Any]) -> None:
361
362
  for migration in self.migrations:
362
- if migration_map.get(migration.migration_name, None) is None:
363
+ if migration_map.get(migration.migration_name) is None:
363
364
  self.errors.append(
364
365
  SchemaUpdateValidationError(
365
366
  path=migration.path,
@@ -370,7 +371,7 @@ class SchemaUpdateValidationResult(BaseModel):
370
371
 
371
372
  def validate_constraints(self, validator_map: dict[str, Any]) -> None:
372
373
  for constraint in self.constraints:
373
- if validator_map.get(constraint.constraint_name, None) is None:
374
+ if validator_map.get(constraint.constraint_name) is None:
374
375
  self.errors.append(
375
376
  SchemaUpdateValidationError(
376
377
  path=constraint.path,
@@ -578,11 +579,9 @@ class HashableModel(BaseModel):
578
579
 
579
580
  for field_name in other.model_fields.keys():
580
581
  if not hasattr(self, field_name):
581
- try:
582
- setattr(self, field_name, getattr(other, field_name))
583
- except ValueError:
582
+ with contextlib.suppress(ValueError):
584
583
  # handles the case where self and other are different types and other has fields that self does not
585
- pass
584
+ setattr(self, field_name, getattr(other, field_name))
586
585
  continue
587
586
 
588
587
  attr_other = getattr(other, field_name)
@@ -467,15 +467,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
467
467
  for attribute_name in template._attributes:
468
468
  if attribute_name in list(fields) + [OBJECT_TEMPLATE_NAME_ATTR]:
469
469
  continue
470
- attr_value = getattr(template, attribute_name).value
470
+ attr = getattr(template, attribute_name)
471
+ attr_value = attr.value
471
472
  if attr_value is not None:
472
- fields[attribute_name] = {"value": attr_value, "source": template.id}
473
+ # Preserve is_from_profile flag when copying from template
474
+ field_data = {"value": attr_value, "source": attr.source_id or template.id}
475
+ if attr.is_from_profile:
476
+ field_data["is_from_profile"] = True
477
+ fields[attribute_name] = field_data
473
478
 
474
479
  for relationship_name in template._relationships:
475
480
  relationship_schema = template._schema.get_relationship(name=relationship_name)
476
481
  if (
477
482
  relationship_name in list(fields)
478
- or relationship_schema.kind not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC]
483
+ or relationship_schema.kind
484
+ not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC, RelationshipKind.PROFILE]
479
485
  or relationship_name == OBJECT_TEMPLATE_RELATIONSHIP_NAME
480
486
  ):
481
487
  continue
@@ -570,7 +576,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
570
576
  self,
571
577
  rel_schema.name,
572
578
  await generator_method(
573
- db=db, name=rel_schema.name, schema=rel_schema, data=fields.get(rel_schema.name, None)
579
+ db=db, name=rel_schema.name, schema=rel_schema, data=fields.get(rel_schema.name)
574
580
  ),
575
581
  )
576
582
  except ValidationError as exc:
@@ -600,7 +606,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
600
606
  self,
601
607
  attr_schema.name,
602
608
  await generator_method(
603
- db=db, name=attr_schema.name, schema=attr_schema, data=fields.get(attr_schema.name, None)
609
+ db=db, name=attr_schema.name, schema=attr_schema, data=fields.get(attr_schema.name)
604
610
  ),
605
611
  )
606
612
  if not self._existing:
@@ -1088,7 +1094,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
1088
1094
 
1089
1095
  if key in self._relationships:
1090
1096
  rel: RelationshipManager = getattr(self, key)
1091
- changed |= await rel.update(db=db, data=value, process_delete=process_pools)
1097
+ changed |= await rel.update(db=db, data=value)
1092
1098
 
1093
1099
  return changed
1094
1100
 
@@ -58,18 +58,34 @@ async def extract_peer_data(
58
58
  except ValueError:
59
59
  pass
60
60
 
61
- obj_peer_data[attr_name] = {"value": template_attr.value, "source": template_peer.id}
61
+ # If the template attribute comes from a profile, preserve the profile as the source
62
+ # Otherwise, use the template itself as the source
63
+ source_id = template_attr.source_id or template_peer.id
64
+ attr_data = {"value": template_attr.value, "source": source_id}
65
+ if template_attr.is_from_profile:
66
+ attr_data["is_from_profile"] = True
67
+ obj_peer_data[attr_name] = attr_data
62
68
 
63
69
  for rel in template_peer.get_schema().relationship_names:
64
70
  rel_manager: RelationshipManager = getattr(template_peer, rel)
65
-
66
- if rel_manager.schema.name not in obj_peer_schema.relationship_names:
71
+ if (
72
+ rel_manager.schema.kind
73
+ not in [
74
+ RelationshipKind.COMPONENT,
75
+ RelationshipKind.PARENT,
76
+ RelationshipKind.PROFILE,
77
+ RelationshipKind.ATTRIBUTE,
78
+ ]
79
+ or rel_manager.schema.name not in obj_peer_schema.relationship_names
80
+ ):
67
81
  continue
68
82
 
69
83
  peers_map = await rel_manager.get_peers(db=db)
70
- if rel_manager.schema.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT] and list(
71
- peers_map.keys()
72
- ) == [current_template.id]:
84
+ if rel_manager.schema.kind in [
85
+ RelationshipKind.COMPONENT,
86
+ RelationshipKind.PARENT,
87
+ RelationshipKind.PROFILE,
88
+ ] and list(peers_map.keys()) == [current_template.id]:
73
89
  obj_peer_data[rel] = {"id": parent_obj.id}
74
90
  continue
75
91
 
@@ -80,7 +96,13 @@ async def extract_peer_data(
80
96
  continue
81
97
  rel_peer_ids.append({"id": peer_id})
82
98
 
83
- obj_peer_data[rel] = rel_peer_ids
99
+ # Only set the relationship data if there are actual peers to set
100
+ if rel_peer_ids:
101
+ obj_peer_data[rel] = rel_peer_ids
102
+
103
+ if rel_manager.schema.kind == RelationshipKind.PROFILE:
104
+ profiles = list(await rel_manager.get_peers(db=db))
105
+ obj_peer_data[rel] = profiles
84
106
 
85
107
  return obj_peer_data
86
108
 
@@ -125,6 +147,12 @@ async def handle_template_relationships(
125
147
  await constraint_runner.check(node=obj_peer, field_filters=list(obj_peer_data))
126
148
  await obj_peer.save(db=db)
127
149
 
150
+ template_profile_ids = await get_profile_ids(db=db, obj=template_relationship_peer)
151
+ if template_profile_ids:
152
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
153
+ await node_profiles_applier.apply_profiles(node=obj_peer)
154
+ await obj_peer.save(db=db)
155
+
128
156
  await handle_template_relationships(
129
157
  db=db,
130
158
  branch=branch,
@@ -136,7 +164,7 @@ async def handle_template_relationships(
136
164
  )
137
165
 
138
166
 
139
- async def get_profile_ids(db: InfrahubDatabase, obj: Node) -> set[str]:
167
+ async def get_profile_ids(db: InfrahubDatabase, obj: Node | CoreObjectTemplate) -> set[str]:
140
168
  if not hasattr(obj, "profiles"):
141
169
  return set()
142
170
  profile_rels = await obj.profiles.get_relationships(db=db)
@@ -40,8 +40,8 @@ class BuiltinIPPrefix(Node):
40
40
  retrieved = await NodeManager.get_one(
41
41
  db=db, branch=self._branch, id=self.id, fields={"member_type": None, "prefix": None}
42
42
  )
43
- self.member_type = retrieved.member_type # type: ignore[union-attr]
44
- self.prefix = retrieved.prefix # type: ignore[union-attr]
43
+ self.member_type = retrieved.member_type # type: ignore[attr-defined,union-attr]
44
+ self.prefix = retrieved.prefix # type: ignore[attr-defined,union-attr]
45
45
  utilization_getter = PrefixUtilizationGetter(db=db, ip_prefixes=[self])
46
46
  utilization = await utilization_getter.get_use_percentage(
47
47
  ip_prefixes=[self], branch_names=[self._branch.name]
@@ -57,6 +57,6 @@ class BuiltinIPPrefix(Node):
57
57
  retrieved = await NodeManager.get_one(
58
58
  db=db, branch=self._branch, id=self.id, fields={"member_type": None, "prefix": None}
59
59
  )
60
- self.member_type = retrieved.member_type # type: ignore[union-attr]
61
- self.prefix = retrieved.prefix # type: ignore[union-attr]
60
+ self.member_type = retrieved.member_type # type: ignore[attr-defined,union-attr]
61
+ self.prefix = retrieved.prefix # type: ignore[attr-defined,union-attr]
62
62
  return get_prefix_space(self)