infrahub-server 1.1.6__py3-none-any.whl → 1.1.8__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 (97) hide show
  1. infrahub/core/attribute.py +4 -1
  2. infrahub/core/branch/tasks.py +7 -4
  3. infrahub/core/diff/combiner.py +11 -7
  4. infrahub/core/diff/coordinator.py +49 -70
  5. infrahub/core/diff/data_check_synchronizer.py +86 -7
  6. infrahub/core/diff/enricher/aggregated.py +3 -3
  7. infrahub/core/diff/enricher/cardinality_one.py +6 -6
  8. infrahub/core/diff/enricher/hierarchy.py +17 -4
  9. infrahub/core/diff/enricher/labels.py +18 -3
  10. infrahub/core/diff/enricher/path_identifier.py +7 -8
  11. infrahub/core/diff/merger/merger.py +5 -3
  12. infrahub/core/diff/model/path.py +66 -25
  13. infrahub/core/diff/parent_node_adder.py +78 -0
  14. infrahub/core/diff/payload_builder.py +13 -2
  15. infrahub/core/diff/query/all_conflicts.py +5 -2
  16. infrahub/core/diff/query/diff_get.py +2 -1
  17. infrahub/core/diff/query/field_specifiers.py +2 -0
  18. infrahub/core/diff/query/field_summary.py +2 -1
  19. infrahub/core/diff/query/filters.py +12 -1
  20. infrahub/core/diff/query/has_conflicts_query.py +5 -2
  21. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +3 -3
  22. infrahub/core/diff/query/roots_metadata.py +8 -1
  23. infrahub/core/diff/query/save.py +230 -139
  24. infrahub/core/diff/query/summary_counts_enricher.py +267 -0
  25. infrahub/core/diff/query/time_range_query.py +2 -1
  26. infrahub/core/diff/query_parser.py +49 -24
  27. infrahub/core/diff/repository/deserializer.py +31 -27
  28. infrahub/core/diff/repository/repository.py +215 -41
  29. infrahub/core/diff/tasks.py +4 -4
  30. infrahub/core/graph/__init__.py +1 -1
  31. infrahub/core/graph/index.py +3 -0
  32. infrahub/core/migrations/graph/__init__.py +4 -0
  33. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +256 -0
  34. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  35. infrahub/core/migrations/query/node_duplicate.py +38 -18
  36. infrahub/core/migrations/schema/node_remove.py +26 -12
  37. infrahub/core/migrations/shared.py +10 -8
  38. infrahub/core/node/__init__.py +19 -9
  39. infrahub/core/node/constraints/grouped_uniqueness.py +25 -5
  40. infrahub/core/node/ipam.py +6 -1
  41. infrahub/core/node/permissions.py +4 -0
  42. infrahub/core/query/attribute.py +2 -0
  43. infrahub/core/query/diff.py +41 -3
  44. infrahub/core/query/node.py +74 -21
  45. infrahub/core/query/relationship.py +107 -17
  46. infrahub/core/query/resource_manager.py +5 -1
  47. infrahub/core/relationship/model.py +8 -12
  48. infrahub/core/schema/definitions/core.py +1 -0
  49. infrahub/core/utils.py +1 -0
  50. infrahub/core/validators/uniqueness/query.py +20 -17
  51. infrahub/database/__init__.py +14 -0
  52. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  53. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  54. infrahub/dependencies/builder/diff/deserializer.py +3 -1
  55. infrahub/dependencies/builder/diff/enricher/hierarchy.py +3 -1
  56. infrahub/dependencies/builder/diff/parent_node_adder.py +8 -0
  57. infrahub/graphql/mutations/computed_attribute.py +3 -1
  58. infrahub/graphql/mutations/diff.py +41 -10
  59. infrahub/graphql/mutations/main.py +11 -6
  60. infrahub/graphql/mutations/relationship.py +29 -1
  61. infrahub/graphql/mutations/resource_manager.py +3 -3
  62. infrahub/graphql/mutations/tasks.py +6 -3
  63. infrahub/graphql/queries/resource_manager.py +7 -3
  64. infrahub/permissions/__init__.py +2 -1
  65. infrahub/permissions/types.py +26 -0
  66. infrahub_sdk/client.py +10 -2
  67. infrahub_sdk/config.py +3 -0
  68. infrahub_sdk/ctl/check.py +3 -3
  69. infrahub_sdk/ctl/cli_commands.py +16 -11
  70. infrahub_sdk/ctl/exceptions.py +0 -6
  71. infrahub_sdk/ctl/exporter.py +1 -1
  72. infrahub_sdk/ctl/generator.py +5 -5
  73. infrahub_sdk/ctl/importer.py +3 -2
  74. infrahub_sdk/ctl/menu.py +1 -1
  75. infrahub_sdk/ctl/object.py +1 -1
  76. infrahub_sdk/ctl/repository.py +23 -15
  77. infrahub_sdk/ctl/schema.py +2 -2
  78. infrahub_sdk/ctl/utils.py +4 -3
  79. infrahub_sdk/ctl/validate.py +2 -1
  80. infrahub_sdk/exceptions.py +12 -0
  81. infrahub_sdk/generator.py +3 -0
  82. infrahub_sdk/node.py +7 -4
  83. infrahub_sdk/testing/schemas/animal.py +9 -0
  84. infrahub_sdk/utils.py +11 -1
  85. infrahub_sdk/yaml.py +2 -3
  86. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/METADATA +41 -7
  87. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/RECORD +94 -91
  88. infrahub_testcontainers/container.py +12 -3
  89. infrahub_testcontainers/docker-compose.test.yml +22 -3
  90. infrahub_testcontainers/haproxy.cfg +43 -0
  91. infrahub_testcontainers/helpers.py +85 -1
  92. infrahub/core/diff/enricher/summary_counts.py +0 -105
  93. infrahub/dependencies/builder/diff/enricher/summary_counts.py +0 -8
  94. infrahub_sdk/ctl/_file.py +0 -13
  95. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/LICENSE.txt +0 -0
  96. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/WHEEL +0 -0
  97. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/entry_points.txt +0 -0
@@ -884,6 +884,8 @@ class RelationshipManager:
884
884
  """If the attribute is branch aware, return the Branch object associated with this attribute
885
885
  If the attribute is branch agnostic return the Global Branch
886
886
 
887
+ Note that if this relationship is Aware and source node is Agnostic, it will return -global- branch.
888
+
887
889
  Returns:
888
890
  Branch:
889
891
  """
@@ -959,7 +961,7 @@ class RelationshipManager:
959
961
  self.has_fetched_relationships = True
960
962
 
961
963
  for peer_id in details.peer_ids_present_local_only:
962
- await self.remove(peer_id=peer_id, db=db)
964
+ await self.remove_locally(peer_id=peer_id, db=db)
963
965
 
964
966
  async def get(self, db: InfrahubDatabase) -> Relationship | list[Relationship] | None:
965
967
  rels = await self.get_relationships(db=db)
@@ -1077,22 +1079,17 @@ class RelationshipManager:
1077
1079
  for rel in self._relationships:
1078
1080
  await rel.resolve(db=db)
1079
1081
 
1080
- async def remove(
1082
+ async def remove_locally(
1081
1083
  self,
1082
1084
  peer_id: Union[str, UUID],
1083
1085
  db: InfrahubDatabase,
1084
- update_db: bool = False,
1085
1086
  ) -> bool:
1086
- """Remove a peer id from the local relationships list,
1087
- need to investigate if and when we should update the relationship in the database."""
1087
+ """Remove a peer id from the local relationships list"""
1088
1088
 
1089
1089
  for idx, rel in enumerate(await self.get_relationships(db=db)):
1090
1090
  if str(rel.peer_id) != str(peer_id):
1091
1091
  continue
1092
1092
 
1093
- if update_db:
1094
- await rel.delete(db=db)
1095
-
1096
1093
  self._relationships.pop(idx)
1097
1094
  return True
1098
1095
 
@@ -1109,14 +1106,13 @@ class RelationshipManager:
1109
1106
 
1110
1107
  # - Update the existing relationship if we are on the same branch
1111
1108
  rel_ids_per_branch = peer_data.rel_ids_per_branch()
1109
+
1110
+ # In which cases do we end up here and do not want to set `to` time?
1112
1111
  if branch.name in rel_ids_per_branch:
1113
1112
  await update_relationships_to([str(ri) for ri in rel_ids_per_branch[branch.name]], to=remove_at, db=db)
1114
1113
 
1115
1114
  # - Create a new rel of type DELETED if the existing relationship is on a different branch
1116
- rel_branches: set[str] = set()
1117
- if peer_data.rels:
1118
- rel_branches = {r.branch for r in peer_data.rels}
1119
- if rel_branches == {peer_data.branch}:
1115
+ if peer_data.rels and {r.branch for r in peer_data.rels} == {peer_data.branch}:
1120
1116
  return
1121
1117
 
1122
1118
  query = await RelationshipDataDeleteQuery.init(
@@ -223,6 +223,7 @@ core_models: dict[str, Any] = {
223
223
  "optional": True,
224
224
  "identifier": "group_member",
225
225
  "cardinality": "many",
226
+ "branch": BranchSupportType.AWARE,
226
227
  },
227
228
  {
228
229
  "name": "subscribers",
infrahub/core/utils.py CHANGED
@@ -72,6 +72,7 @@ async def update_relationships_to(ids: list[str], db: InfrahubDatabase, to: Time
72
72
  query = """
73
73
  MATCH ()-[r]->()
74
74
  WHERE %(id_func)s(r) IN $ids
75
+ AND r.to IS NULL
75
76
  SET r.to = $to
76
77
  RETURN %(id_func)s(r)
77
78
  """ % {"id_func": db.get_id_function_name()}
@@ -30,7 +30,7 @@ class NodeUniqueAttributeConstraintQuery(Query):
30
30
  def get_context(self) -> dict[str, str]:
31
31
  return {"kind": self.query_request.kind}
32
32
 
33
- async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
33
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # pylint: disable=too-many-branches
34
34
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string(), is_isolated=False)
35
35
  self.params.update(branch_params)
36
36
  from_times = db.render_list_comprehension(items="relationships(potential_path)", item_name="from")
@@ -56,6 +56,7 @@ class NodeUniqueAttributeConstraintQuery(Query):
56
56
  relationship_names = set()
57
57
  relationship_attr_paths = []
58
58
  relationship_only_attr_paths = []
59
+ relationship_only_attr_values = []
59
60
  relationship_attr_paths_with_value = []
60
61
  for rel_path in self.query_request.relationship_attribute_paths:
61
62
  relationship_names.add(rel_path.identifier)
@@ -67,6 +68,8 @@ class NodeUniqueAttributeConstraintQuery(Query):
67
68
  relationship_attr_paths.append((rel_path.identifier, rel_path.attribute_name))
68
69
  else:
69
70
  relationship_only_attr_paths.append(rel_path.identifier)
71
+ if rel_path.value:
72
+ relationship_only_attr_values.append(rel_path.value)
70
73
 
71
74
  if (
72
75
  not attr_paths
@@ -89,34 +92,37 @@ class NodeUniqueAttributeConstraintQuery(Query):
89
92
  "relationship_attr_paths": relationship_attr_paths,
90
93
  "relationship_attr_paths_with_value": relationship_attr_paths_with_value,
91
94
  "relationship_only_attr_paths": relationship_only_attr_paths,
95
+ "relationship_only_attr_values": relationship_only_attr_values,
92
96
  "min_count_required": self.min_count_required,
93
97
  }
94
98
  )
95
99
 
96
100
  attr_paths_subquery = """
97
- WITH start_node
98
- MATCH attr_path = (start_node)-[:HAS_ATTRIBUTE]->(attr:Attribute)-[r:HAS_VALUE]->(attr_value:AttributeValue)
101
+ MATCH attr_path = (start_node:%(node_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)-[r:HAS_VALUE]->(attr_value:AttributeValue)
99
102
  WHERE attr.name in $attribute_names
100
103
  AND ([attr.name, type(r)] in $attr_paths
101
104
  OR [attr.name, type(r), attr_value.value] in $attr_paths_with_value)
102
- RETURN attr_path as potential_path, NULL as rel_identifier, attr.name as potential_attr, attr_value.value as potential_attr_value
103
- """
105
+ RETURN start_node, attr_path as potential_path, NULL as rel_identifier, attr.name as potential_attr, attr_value.value as potential_attr_value
106
+ """ % {"node_kind": self.query_request.kind}
104
107
 
105
108
  relationship_attr_paths_with_value_subquery = """
106
- WITH start_node
107
- MATCH rel_path = (start_node)-[:IS_RELATED]-(relationship_node:Relationship)-[:IS_RELATED]-(related_n:Node)-[:HAS_ATTRIBUTE]->(rel_attr:Attribute)-[:HAS_VALUE]->(rel_attr_value:AttributeValue)
109
+ MATCH rel_path = (start_node:%(node_kind)s)-[:IS_RELATED]-(relationship_node:Relationship)-[:IS_RELATED]-(related_n:Node)-[:HAS_ATTRIBUTE]->(rel_attr:Attribute)-[:HAS_VALUE]->(rel_attr_value:AttributeValue)
108
110
  WHERE relationship_node.name in $relationship_names
109
111
  AND ([relationship_node.name, rel_attr.name] in $relationship_attr_paths
110
112
  OR [relationship_node.name, rel_attr.name, rel_attr_value.value] in $relationship_attr_paths_with_value)
111
- RETURN rel_path as potential_path, relationship_node.name as rel_identifier, rel_attr.name as potential_attr, rel_attr_value.value as potential_attr_value
112
- """
113
+ RETURN start_node, rel_path as potential_path, relationship_node.name as rel_identifier, rel_attr.name as potential_attr, rel_attr_value.value as potential_attr_value
114
+ """ % {"node_kind": self.query_request.kind}
113
115
 
114
116
  relationship_only_attr_paths_subquery = """
115
- WITH start_node
116
- MATCH rel_path = (start_node)-[:IS_RELATED]-(relationship_node:Relationship)-[:IS_RELATED]-(related_n:Node)
117
- WHERE relationship_node.name in $relationship_only_attr_paths
118
- RETURN rel_path as potential_path, relationship_node.name as rel_identifier, "id" as potential_attr, related_n.uuid as potential_attr_value
119
- """
117
+ MATCH rel_path = (start_node:%(node_kind)s)-[:IS_RELATED]-(relationship_node:Relationship)-[:IS_RELATED]-(related_n:Node)
118
+ WHERE %(rel_node_filter)s relationship_node.name in $relationship_only_attr_paths
119
+ RETURN start_node, rel_path as potential_path, relationship_node.name as rel_identifier, "id" as potential_attr, related_n.uuid as potential_attr_value
120
+ """ % {
121
+ "node_kind": self.query_request.kind,
122
+ "rel_node_filter": "related_n.uuid IN $relationship_only_attr_values AND "
123
+ if relationship_only_attr_values
124
+ else "",
125
+ }
120
126
 
121
127
  select_subqueries = []
122
128
  if attr_paths or attr_paths_with_value:
@@ -130,8 +136,6 @@ class NodeUniqueAttributeConstraintQuery(Query):
130
136
 
131
137
  # ruff: noqa: E501
132
138
  query = """
133
- // group by node
134
- MATCH (start_node:%(node_kind)s)
135
139
  // get attributes for node and its relationships
136
140
  CALL {
137
141
  %(select_subqueries_str)s
@@ -201,7 +205,6 @@ class NodeUniqueAttributeConstraintQuery(Query):
201
205
  attr_value,
202
206
  relationship_identifier
203
207
  """ % {
204
- "node_kind": self.query_request.kind,
205
208
  "select_subqueries_str": select_subqueries_str,
206
209
  "branch_filter": branch_filter,
207
210
  "from_times": from_times,
@@ -173,6 +173,19 @@ class InfrahubDatabase:
173
173
  elif self.db_type == DatabaseType.MEMGRAPH:
174
174
  self.manager = DatabaseManagerMemgraph(db=self)
175
175
 
176
+ def __del__(self) -> None:
177
+ if not self._session or not self._is_session_local or self._session.closed():
178
+ return
179
+
180
+ try:
181
+ loop = asyncio.get_running_loop()
182
+ except RuntimeError:
183
+ loop = None
184
+ if loop and loop.is_running():
185
+ loop.create_task(self._session.close())
186
+ else:
187
+ asyncio.run(self._session.close())
188
+
176
189
  @property
177
190
  def is_session(self) -> bool:
178
191
  if self._mode == InfrahubDatabaseMode.SESSION:
@@ -501,6 +514,7 @@ def retry_db_transaction(
501
514
  if exc.code != "Neo.ClientError.Statement.EntityNotFound":
502
515
  raise exc
503
516
  retry_time: float = random.randrange(100, 500) / 1000
517
+ log.exception("Retry handler caught database error")
504
518
  log.info(
505
519
  f"Retrying database transaction, attempt {attempt}/{config.SETTINGS.database.retry_limit}",
506
520
  retry_time=retry_time,
@@ -2,7 +2,6 @@ from infrahub.core.constraint.node.runner import NodeConstraintRunner
2
2
  from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
3
 
4
4
  from ..node.grouped_uniqueness import NodeGroupedUniquenessConstraintDependency
5
- from ..node.uniqueness import NodeAttributeUniquenessConstraintDependency
6
5
  from ..relationship_manager.count import RelationshipCountConstraintDependency
7
6
  from ..relationship_manager.peer_kind import RelationshipPeerKindConstraintDependency
8
7
  from ..relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
@@ -15,7 +14,6 @@ class NodeConstraintRunnerDependency(DependencyBuilder[NodeConstraintRunner]):
15
14
  db=context.db,
16
15
  branch=context.branch,
17
16
  node_constraints=[
18
- NodeAttributeUniquenessConstraintDependency.build(context=context),
19
17
  NodeGroupedUniquenessConstraintDependency.build(context=context),
20
18
  ],
21
19
  relationship_manager_constraints=[
@@ -8,7 +8,6 @@ from .conflicts_enricher import DiffConflictsEnricherDependency
8
8
  from .data_check_synchronizer import DiffDataCheckSynchronizerDependency
9
9
  from .enricher.aggregated import DiffAggregatedEnricherDependency
10
10
  from .enricher.labels import DiffLabelsEnricherDependency
11
- from .enricher.summary_counts import DiffSummaryCountsEnricherDependency
12
11
  from .repository import DiffRepositoryDependency
13
12
 
14
13
 
@@ -22,7 +21,6 @@ class DiffCoordinatorDependency(DependencyBuilder[DiffCoordinator]):
22
21
  diff_enricher=DiffAggregatedEnricherDependency.build(context=context),
23
22
  conflicts_enricher=DiffConflictsEnricherDependency.build(context=context),
24
23
  labels_enricher=DiffLabelsEnricherDependency.build(context=context),
25
- summary_counts_enricher=DiffSummaryCountsEnricherDependency.build(context=context),
26
24
  data_check_synchronizer=DiffDataCheckSynchronizerDependency.build(context=context),
27
25
  conflict_transferer=DiffConflictTransfererDependency.build(context=context),
28
26
  )
@@ -1,8 +1,10 @@
1
1
  from infrahub.core.diff.repository.deserializer import EnrichedDiffDeserializer
2
2
  from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
3
 
4
+ from .parent_node_adder import DiffParentNodeAdderDependency
5
+
4
6
 
5
7
  class DiffDeserializerDependency(DependencyBuilder[EnrichedDiffDeserializer]):
6
8
  @classmethod
7
9
  def build(cls, context: DependencyBuilderContext) -> EnrichedDiffDeserializer:
8
- return EnrichedDiffDeserializer()
10
+ return EnrichedDiffDeserializer(parent_adder=DiffParentNodeAdderDependency.build(context=context))
@@ -1,8 +1,10 @@
1
1
  from infrahub.core.diff.enricher.hierarchy import DiffHierarchyEnricher
2
2
  from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
3
 
4
+ from ..parent_node_adder import DiffParentNodeAdderDependency
5
+
4
6
 
5
7
  class DiffHierarchyEnricherDependency(DependencyBuilder[DiffHierarchyEnricher]):
6
8
  @classmethod
7
9
  def build(cls, context: DependencyBuilderContext) -> DiffHierarchyEnricher:
8
- return DiffHierarchyEnricher(db=context.db)
10
+ return DiffHierarchyEnricher(db=context.db, parent_adder=DiffParentNodeAdderDependency.build(context=context))
@@ -0,0 +1,8 @@
1
+ from infrahub.core.diff.parent_node_adder import DiffParentNodeAdder
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class DiffParentNodeAdderDependency(DependencyBuilder[DiffParentNodeAdder]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> DiffParentNodeAdder:
8
+ return DiffParentNodeAdder()
@@ -87,7 +87,9 @@ class UpdateComputedAttribute(Mutation):
87
87
  log_data = get_log_data()
88
88
  request_id = log_data.get("request_id", "")
89
89
 
90
- graphql_payload = await target_node.to_graphql(db=context.db, filter_sensitive=True)
90
+ graphql_payload = await target_node.to_graphql(
91
+ db=context.db, filter_sensitive=True, include_properties=False
92
+ )
91
93
 
92
94
  event = NodeMutatedEvent(
93
95
  branch=context.branch.name,
@@ -1,15 +1,20 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
- from graphene import Boolean, DateTime, InputObjectType, Mutation, String
3
+ from graphene import Boolean, DateTime, Field, InputObjectType, Mutation, String
4
4
  from graphql import GraphQLResolveInfo
5
5
 
6
6
  from infrahub.core import registry
7
7
  from infrahub.core.diff.coordinator import DiffCoordinator
8
+ from infrahub.core.diff.model.path import NameTrackingId
8
9
  from infrahub.core.diff.models import RequestDiffUpdate
9
- from infrahub.database import retry_db_transaction
10
+ from infrahub.core.diff.repository.repository import DiffRepository
11
+ from infrahub.core.timestamp import Timestamp
10
12
  from infrahub.dependencies.registry import get_component_registry
13
+ from infrahub.exceptions import ValidationError
11
14
  from infrahub.workflows.catalogue import DIFF_UPDATE
12
15
 
16
+ from ..types.task import TaskInfo
17
+
13
18
  if TYPE_CHECKING:
14
19
  from ..initialization import GraphqlContext
15
20
 
@@ -19,32 +24,57 @@ class DiffUpdateInput(InputObjectType):
19
24
  name = String(required=False)
20
25
  from_time = DateTime(required=False)
21
26
  to_time = DateTime(required=False)
22
- wait_for_completion = Boolean(required=False)
27
+ wait_for_completion = Boolean(required=False, deprecation_reason="Please use `wait_until_completion` instead")
23
28
 
24
29
 
25
30
  class DiffUpdateMutation(Mutation):
26
31
  class Arguments:
27
32
  data = DiffUpdateInput(required=True)
33
+ wait_until_completion = Boolean(required=False)
28
34
 
29
35
  ok = Boolean()
36
+ task = Field(TaskInfo, required=False)
30
37
 
31
38
  @classmethod
32
- @retry_db_transaction(name="diff_update")
33
39
  async def mutate(
34
40
  cls,
35
41
  root: dict, # pylint: disable=unused-argument
36
42
  info: GraphQLResolveInfo,
37
43
  data: DiffUpdateInput,
38
- ) -> dict[str, bool]:
44
+ wait_until_completion: bool = False,
45
+ ) -> dict[str, bool | dict[str, str]]:
39
46
  context: GraphqlContext = info.context
40
47
 
48
+ if data.wait_for_completion is True:
49
+ wait_until_completion = True
50
+
41
51
  from_timestamp_str = DateTime.serialize(data.from_time) if data.from_time else None
42
52
  to_timestamp_str = DateTime.serialize(data.to_time) if data.to_time else None
43
- if data.wait_for_completion is True:
44
- component_registry = get_component_registry()
45
- base_branch = await registry.get_branch(db=context.db, branch=registry.default_branch)
46
- diff_branch = await registry.get_branch(db=context.db, branch=data.branch)
53
+ if (data.from_time or data.to_time) and not data.name:
54
+ raise ValidationError("diff with specified time range requires a name")
55
+
56
+ component_registry = get_component_registry()
57
+ base_branch = await registry.get_branch(db=context.db, branch=registry.default_branch)
58
+ diff_branch = await registry.get_branch(db=context.db, branch=data.branch)
59
+ diff_repository = await component_registry.get_component(DiffRepository, db=context.db, branch=diff_branch)
60
+
61
+ tracking_id = NameTrackingId(name=data.name)
62
+ existing_diffs_metadatas = await diff_repository.get_roots_metadata(
63
+ diff_branch_names=[diff_branch.name], base_branch_names=[base_branch.name], tracking_id=tracking_id
64
+ )
65
+ if existing_diffs_metadatas:
66
+ metadata = existing_diffs_metadatas[0]
67
+ from_time = Timestamp(from_timestamp_str) if from_timestamp_str else None
68
+ to_time = Timestamp(to_timestamp_str) if to_timestamp_str else None
69
+ branched_from_timestamp = Timestamp(diff_branch.get_branched_from())
70
+ if from_time and from_time > metadata.from_time:
71
+ raise ValidationError(f"from_time must be null or less than or equal to {metadata.from_time}")
72
+ if from_time and from_time < branched_from_timestamp:
73
+ raise ValidationError(f"from_time must be null or greater than or equal to {branched_from_timestamp}")
74
+ if to_time and to_time < metadata.to_time:
75
+ raise ValidationError(f"to_time must be null or greater than or equal to {metadata.to_time}")
47
76
 
77
+ if wait_until_completion is True:
48
78
  diff_coordinator = await component_registry.get_component(
49
79
  DiffCoordinator, db=context.db, branch=diff_branch
50
80
  )
@@ -65,6 +95,7 @@ class DiffUpdateMutation(Mutation):
65
95
  to_time=to_timestamp_str,
66
96
  )
67
97
  if context.service:
68
- await context.service.workflow.submit_workflow(workflow=DIFF_UPDATE, parameters={"model": model})
98
+ workflow = await context.service.workflow.submit_workflow(workflow=DIFF_UPDATE, parameters={"model": model})
99
+ return {"ok": True, "task": {"id": str(workflow.id)}}
69
100
 
70
101
  return {"ok": True}
@@ -97,7 +97,7 @@ class InfrahubMutationMixin:
97
97
  log_data = get_log_data()
98
98
  request_id = log_data.get("request_id", "")
99
99
 
100
- graphql_payload = await obj.to_graphql(db=context.db, filter_sensitive=True)
100
+ graphql_payload = await obj.to_graphql(db=context.db, filter_sensitive=True, include_properties=False)
101
101
  event = NodeMutatedEvent(
102
102
  branch=context.branch.name,
103
103
  kind=obj._schema.kind,
@@ -175,20 +175,25 @@ class InfrahubMutationMixin:
175
175
  branch: Branch,
176
176
  ) -> Node:
177
177
  component_registry = get_component_registry()
178
- node_constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
178
+ node_constraint_runner = await component_registry.get_component(
179
+ NodeConstraintRunner, db=db.start_session(), branch=branch
180
+ )
179
181
  node_class = Node
180
182
  if cls._meta.schema.kind in registry.node:
181
183
  node_class = registry.node[cls._meta.schema.kind]
182
184
 
185
+ fields_to_validate = list(data)
183
186
  try:
184
- obj = await node_class.init(db=db, schema=cls._meta.schema, branch=branch)
185
- await obj.new(db=db, **data)
186
- fields_to_validate = list(data)
187
- await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
188
187
  if db.is_transaction:
188
+ obj = await node_class.init(db=db, schema=cls._meta.schema, branch=branch)
189
+ await obj.new(db=db, **data)
190
+ await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
189
191
  await obj.save(db=db)
190
192
  else:
191
193
  async with db.start_transaction() as dbt:
194
+ obj = await node_class.init(db=dbt, schema=cls._meta.schema, branch=branch)
195
+ await obj.new(db=dbt, **data)
196
+ await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
192
197
  await obj.save(db=dbt)
193
198
 
194
199
  except ValidationError as exc:
@@ -5,7 +5,8 @@ from typing import TYPE_CHECKING
5
5
  from graphene import Boolean, InputField, InputObjectType, List, Mutation, String
6
6
  from infrahub_sdk.utils import compare_lists
7
7
 
8
- from infrahub.core.constants import InfrahubKind, RelationshipCardinality
8
+ from infrahub.core.account import GlobalPermission, ObjectPermission
9
+ from infrahub.core.constants import InfrahubKind, PermissionAction, PermissionDecision, RelationshipCardinality
9
10
  from infrahub.core.manager import NodeManager
10
11
  from infrahub.core.query.relationship import (
11
12
  RelationshipGetPeerQuery,
@@ -14,6 +15,7 @@ from infrahub.core.query.relationship import (
14
15
  from infrahub.core.relationship import Relationship
15
16
  from infrahub.database import retry_db_transaction
16
17
  from infrahub.exceptions import NodeNotFoundError, ValidationError
18
+ from infrahub.permissions import get_global_permission_for_kind
17
19
 
18
20
  from ..types import RelatedNodeInput
19
21
 
@@ -76,6 +78,32 @@ class RelationshipMixin:
76
78
  db=context.db, ids=node_ids, fields={"display_label": None}, branch=context.branch
77
79
  )
78
80
 
81
+ if context.account_session:
82
+ impacted_schemas = {node.get_schema() for node in [source] + list(nodes.values())}
83
+ required_permissions: list[GlobalPermission | ObjectPermission] = []
84
+ decision = (
85
+ PermissionDecision.ALLOW_DEFAULT.value
86
+ if context.branch.is_default
87
+ else PermissionDecision.ALLOW_OTHER.value
88
+ )
89
+
90
+ for impacted_schema in impacted_schemas:
91
+ global_action = get_global_permission_for_kind(schema=impacted_schema)
92
+
93
+ if global_action:
94
+ required_permissions.append(GlobalPermission(action=global_action, decision=decision))
95
+ else:
96
+ required_permissions.append(
97
+ ObjectPermission(
98
+ namespace=impacted_schema.namespace,
99
+ name=impacted_schema.name,
100
+ action=PermissionAction.UPDATE.value,
101
+ decision=decision,
102
+ )
103
+ )
104
+
105
+ context.active_permissions.raise_for_permissions(permissions=required_permissions)
106
+
79
107
  _, _, in_list2 = compare_lists(list1=list(nodes.keys()), list2=node_ids)
80
108
  if in_list2:
81
109
  for node_id in in_list2:
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
- from graphene import Boolean, Field, InputField, InputObjectType, Int, Mutation, String
5
+ from graphene import Boolean, Field, InputField, InputObjectType, Int, List, Mutation, String
6
6
  from graphene.types.generic import GenericScalar
7
7
  from typing_extensions import Self
8
8
 
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
30
30
 
31
31
  class IPPrefixPoolGetResourceInput(InputObjectType):
32
32
  id = InputField(String(required=False), description="ID of the pool to allocate from")
33
- hfid = InputField(String(required=False), description="HFID of the pool to allocate from")
33
+ hfid = InputField(List(of_type=String, required=False), description="HFID of the pool to allocate from")
34
34
  identifier = InputField(String(required=False), description="Identifier for the allocated resource")
35
35
  prefix_length = InputField(Int(required=False), description="Size of the prefix to allocate")
36
36
  member_type = InputField(String(required=False), description="Type of members for the newly created prefix")
@@ -40,7 +40,7 @@ class IPPrefixPoolGetResourceInput(InputObjectType):
40
40
 
41
41
  class IPAddressPoolGetResourceInput(InputObjectType):
42
42
  id = InputField(String(required=False), description="ID of the pool to allocate from")
43
- hfid = InputField(String(required=False), description="HFID of the pool to allocate from")
43
+ hfid = InputField(List(of_type=String, required=False), description="HFID of the pool to allocate from")
44
44
  identifier = InputField(String(required=False), description="Identifier for the allocated resource")
45
45
  prefix_length = InputField(
46
46
  Int(required=False), description="Size of the prefix mask to allocate on the new IP address"
@@ -31,14 +31,17 @@ async def merge_branch_mutation(branch: str) -> None:
31
31
  diff_coordinator = await component_registry.get_component(DiffCoordinator, db=db, branch=obj)
32
32
  diff_repository = await component_registry.get_component(DiffRepository, db=db, branch=obj)
33
33
  diff_merger = await component_registry.get_component(DiffMerger, db=db, branch=obj)
34
- enriched_diff = await diff_coordinator.update_branch_diff_and_return(base_branch=base_branch, diff_branch=obj)
35
- if enriched_diff.get_all_conflicts():
34
+ enriched_diff_metadata = await diff_coordinator.update_branch_diff(base_branch=base_branch, diff_branch=obj)
35
+ async for _ in diff_repository.get_all_conflicts_for_diff(
36
+ diff_branch_name=enriched_diff_metadata.diff_branch_name, diff_id=enriched_diff_metadata.uuid
37
+ ):
38
+ # if there are any conflicts, raise the error
36
39
  raise ValidationError(
37
40
  f"Branch {obj.name} contains conflicts with the default branch."
38
41
  " Please create a Proposed Change to resolve the conflicts or manually update them before merging."
39
42
  )
40
43
  node_diff_field_summaries = await diff_repository.get_node_field_summaries(
41
- diff_branch_name=enriched_diff.diff_branch_name, diff_id=enriched_diff.uuid
44
+ diff_branch_name=enriched_diff_metadata.diff_branch_name, diff_id=enriched_diff_metadata.uuid
42
45
  )
43
46
 
44
47
  merger = BranchMerger(
@@ -21,8 +21,10 @@ from infrahub.pools.number import NumberUtilizationGetter
21
21
  if TYPE_CHECKING:
22
22
  from graphql import GraphQLResolveInfo
23
23
 
24
+ from infrahub.core.branch import Branch
24
25
  from infrahub.core.node import Node
25
26
  from infrahub.core.protocols import CoreNode
27
+ from infrahub.core.timestamp import Timestamp
26
28
  from infrahub.database import InfrahubDatabase
27
29
  from infrahub.graphql.initialization import GraphqlContext
28
30
 
@@ -184,7 +186,7 @@ class PoolUtilization(ObjectType):
184
186
  pool: CoreNode | None = await NodeManager.get_one(id=pool_id, db=db, branch=context.branch)
185
187
  pool = _validate_pool_type(pool_id=pool_id, pool=pool)
186
188
  if pool.get_kind() == "CoreNumberPool":
187
- return await resolve_number_pool_utilization(db=db, context=context, pool=pool)
189
+ return await resolve_number_pool_utilization(db=db, at=context.at, pool=pool, branch=context.branch)
188
190
 
189
191
  resources_map: dict[str, Node] = {}
190
192
 
@@ -290,8 +292,10 @@ async def resolve_number_pool_allocation(
290
292
  return response
291
293
 
292
294
 
293
- async def resolve_number_pool_utilization(db: InfrahubDatabase, context: GraphqlContext, pool: CoreNode) -> dict:
294
- number_pool = NumberUtilizationGetter(db=db, pool=pool, at=context.at, branch=context.branch)
295
+ async def resolve_number_pool_utilization(
296
+ db: InfrahubDatabase, pool: CoreNode, at: Timestamp | str | None, branch: Branch
297
+ ) -> dict:
298
+ number_pool = NumberUtilizationGetter(db=db, pool=pool, at=at, branch=branch)
295
299
  await number_pool.load_data()
296
300
 
297
301
  return {
@@ -2,12 +2,13 @@ from infrahub.permissions.backend import PermissionBackend
2
2
  from infrahub.permissions.local_backend import LocalPermissionBackend
3
3
  from infrahub.permissions.manager import PermissionManager
4
4
  from infrahub.permissions.report import report_schema_permissions
5
- from infrahub.permissions.types import AssignedPermissions
5
+ from infrahub.permissions.types import AssignedPermissions, get_global_permission_for_kind
6
6
 
7
7
  __all__ = [
8
8
  "AssignedPermissions",
9
9
  "LocalPermissionBackend",
10
10
  "PermissionBackend",
11
11
  "PermissionManager",
12
+ "get_global_permission_for_kind",
12
13
  "report_schema_permissions",
13
14
  ]
@@ -2,8 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, TypedDict
4
4
 
5
+ from infrahub.core.constants import GlobalPermissions, InfrahubKind
6
+ from infrahub.core.schema import NodeSchema
7
+
5
8
  if TYPE_CHECKING:
6
9
  from infrahub.core.account import GlobalPermission, ObjectPermission
10
+ from infrahub.core.schema import MainSchemaTypes
7
11
  from infrahub.permissions.constants import BranchRelativePermissionDecision
8
12
 
9
13
 
@@ -18,3 +22,25 @@ class KindPermissions(TypedDict):
18
22
  delete: BranchRelativePermissionDecision
19
23
  update: BranchRelativePermissionDecision
20
24
  view: BranchRelativePermissionDecision
25
+
26
+
27
+ def get_global_permission_for_kind(schema: MainSchemaTypes) -> GlobalPermissions | None:
28
+ kind_permission_map = {
29
+ InfrahubKind.GENERICACCOUNT: GlobalPermissions.MANAGE_ACCOUNTS,
30
+ InfrahubKind.ACCOUNTGROUP: GlobalPermissions.MANAGE_ACCOUNTS,
31
+ InfrahubKind.ACCOUNTROLE: GlobalPermissions.MANAGE_ACCOUNTS,
32
+ InfrahubKind.BASEPERMISSION: GlobalPermissions.MANAGE_PERMISSIONS,
33
+ InfrahubKind.GENERICREPOSITORY: GlobalPermissions.MANAGE_REPOSITORIES,
34
+ }
35
+
36
+ if schema.kind in kind_permission_map:
37
+ return kind_permission_map[schema.kind]
38
+
39
+ if isinstance(schema, NodeSchema):
40
+ for base in schema.inherit_from:
41
+ try:
42
+ return kind_permission_map[base]
43
+ except KeyError:
44
+ continue
45
+
46
+ return None