infrahub-server 1.3.5__py3-none-any.whl → 1.4.0b0__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 (158) hide show
  1. infrahub/api/internal.py +5 -0
  2. infrahub/artifacts/tasks.py +17 -22
  3. infrahub/branch/merge_mutation_checker.py +38 -0
  4. infrahub/cli/__init__.py +2 -2
  5. infrahub/cli/context.py +7 -3
  6. infrahub/cli/db.py +5 -16
  7. infrahub/cli/upgrade.py +7 -29
  8. infrahub/computed_attribute/tasks.py +36 -46
  9. infrahub/config.py +53 -2
  10. infrahub/constants/environment.py +1 -0
  11. infrahub/core/attribute.py +9 -7
  12. infrahub/core/branch/tasks.py +43 -41
  13. infrahub/core/constants/__init__.py +20 -6
  14. infrahub/core/constants/infrahubkind.py +2 -0
  15. infrahub/core/diff/coordinator.py +3 -1
  16. infrahub/core/diff/repository/repository.py +0 -8
  17. infrahub/core/diff/tasks.py +11 -8
  18. infrahub/core/graph/__init__.py +1 -1
  19. infrahub/core/graph/index.py +1 -2
  20. infrahub/core/graph/schema.py +50 -29
  21. infrahub/core/initialization.py +62 -33
  22. infrahub/core/ipam/tasks.py +4 -3
  23. infrahub/core/merge.py +8 -10
  24. infrahub/core/migrations/graph/__init__.py +2 -0
  25. infrahub/core/migrations/graph/m035_drop_attr_value_index.py +45 -0
  26. infrahub/core/migrations/query/attribute_add.py +27 -2
  27. infrahub/core/migrations/schema/tasks.py +6 -5
  28. infrahub/core/node/proposed_change.py +43 -0
  29. infrahub/core/protocols.py +12 -0
  30. infrahub/core/query/attribute.py +32 -14
  31. infrahub/core/query/diff.py +11 -0
  32. infrahub/core/query/ipam.py +13 -7
  33. infrahub/core/query/node.py +51 -10
  34. infrahub/core/query/resource_manager.py +3 -3
  35. infrahub/core/schema/basenode_schema.py +8 -0
  36. infrahub/core/schema/definitions/core/__init__.py +10 -1
  37. infrahub/core/schema/definitions/core/ipam.py +28 -2
  38. infrahub/core/schema/definitions/core/propose_change.py +15 -0
  39. infrahub/core/schema/definitions/core/webhook.py +3 -0
  40. infrahub/core/schema/generic_schema.py +10 -0
  41. infrahub/core/schema/manager.py +10 -1
  42. infrahub/core/schema/node_schema.py +22 -17
  43. infrahub/core/schema/profile_schema.py +8 -0
  44. infrahub/core/schema/schema_branch.py +9 -5
  45. infrahub/core/schema/template_schema.py +8 -0
  46. infrahub/core/validators/checks_runner.py +5 -5
  47. infrahub/core/validators/tasks.py +6 -7
  48. infrahub/core/validators/uniqueness/checker.py +4 -2
  49. infrahub/core/validators/uniqueness/model.py +1 -0
  50. infrahub/core/validators/uniqueness/query.py +57 -7
  51. infrahub/database/__init__.py +2 -1
  52. infrahub/events/__init__.py +18 -0
  53. infrahub/events/constants.py +7 -0
  54. infrahub/events/generator.py +29 -2
  55. infrahub/events/proposed_change_action.py +181 -0
  56. infrahub/generators/tasks.py +24 -20
  57. infrahub/git/base.py +4 -7
  58. infrahub/git/integrator.py +21 -12
  59. infrahub/git/repository.py +15 -30
  60. infrahub/git/tasks.py +121 -106
  61. infrahub/graphql/field_extractor.py +69 -0
  62. infrahub/graphql/manager.py +15 -11
  63. infrahub/graphql/mutations/account.py +2 -2
  64. infrahub/graphql/mutations/action.py +8 -2
  65. infrahub/graphql/mutations/artifact_definition.py +4 -1
  66. infrahub/graphql/mutations/branch.py +10 -5
  67. infrahub/graphql/mutations/graphql_query.py +2 -1
  68. infrahub/graphql/mutations/main.py +14 -8
  69. infrahub/graphql/mutations/menu.py +2 -1
  70. infrahub/graphql/mutations/proposed_change.py +225 -8
  71. infrahub/graphql/mutations/relationship.py +5 -0
  72. infrahub/graphql/mutations/repository.py +2 -1
  73. infrahub/graphql/mutations/tasks.py +7 -9
  74. infrahub/graphql/mutations/webhook.py +4 -1
  75. infrahub/graphql/parser.py +15 -6
  76. infrahub/graphql/queries/__init__.py +10 -1
  77. infrahub/graphql/queries/account.py +3 -3
  78. infrahub/graphql/queries/branch.py +2 -2
  79. infrahub/graphql/queries/diff/tree.py +3 -3
  80. infrahub/graphql/queries/event.py +13 -3
  81. infrahub/graphql/queries/ipam.py +23 -1
  82. infrahub/graphql/queries/proposed_change.py +84 -0
  83. infrahub/graphql/queries/relationship.py +2 -2
  84. infrahub/graphql/queries/resource_manager.py +3 -3
  85. infrahub/graphql/queries/search.py +3 -2
  86. infrahub/graphql/queries/status.py +3 -2
  87. infrahub/graphql/queries/task.py +2 -2
  88. infrahub/graphql/resolvers/ipam.py +440 -0
  89. infrahub/graphql/resolvers/many_relationship.py +4 -3
  90. infrahub/graphql/resolvers/resolver.py +5 -5
  91. infrahub/graphql/resolvers/single_relationship.py +3 -2
  92. infrahub/graphql/schema.py +25 -5
  93. infrahub/graphql/types/__init__.py +2 -2
  94. infrahub/graphql/types/attribute.py +3 -3
  95. infrahub/graphql/types/event.py +60 -0
  96. infrahub/groups/tasks.py +6 -6
  97. infrahub/lock.py +3 -2
  98. infrahub/menu/generator.py +8 -0
  99. infrahub/message_bus/operations/__init__.py +9 -12
  100. infrahub/message_bus/operations/git/file.py +6 -5
  101. infrahub/message_bus/operations/git/repository.py +12 -20
  102. infrahub/message_bus/operations/refresh/registry.py +15 -9
  103. infrahub/message_bus/operations/send/echo.py +7 -4
  104. infrahub/message_bus/types.py +1 -0
  105. infrahub/permissions/globals.py +1 -4
  106. infrahub/permissions/manager.py +8 -5
  107. infrahub/pools/prefix.py +7 -5
  108. infrahub/prefect_server/app.py +31 -0
  109. infrahub/prefect_server/bootstrap.py +18 -0
  110. infrahub/proposed_change/action_checker.py +206 -0
  111. infrahub/proposed_change/approval_revoker.py +40 -0
  112. infrahub/proposed_change/branch_diff.py +3 -1
  113. infrahub/proposed_change/checker.py +45 -0
  114. infrahub/proposed_change/constants.py +32 -2
  115. infrahub/proposed_change/tasks.py +182 -150
  116. infrahub/py.typed +0 -0
  117. infrahub/server.py +29 -17
  118. infrahub/services/__init__.py +13 -28
  119. infrahub/services/adapters/cache/__init__.py +4 -0
  120. infrahub/services/adapters/cache/nats.py +2 -0
  121. infrahub/services/adapters/cache/redis.py +3 -0
  122. infrahub/services/adapters/message_bus/__init__.py +0 -2
  123. infrahub/services/adapters/message_bus/local.py +1 -2
  124. infrahub/services/adapters/message_bus/nats.py +6 -8
  125. infrahub/services/adapters/message_bus/rabbitmq.py +7 -9
  126. infrahub/services/adapters/workflow/__init__.py +1 -0
  127. infrahub/services/adapters/workflow/local.py +1 -8
  128. infrahub/services/component.py +2 -1
  129. infrahub/task_manager/event.py +52 -0
  130. infrahub/task_manager/models.py +9 -0
  131. infrahub/tasks/artifact.py +6 -7
  132. infrahub/tasks/check.py +4 -7
  133. infrahub/telemetry/tasks.py +15 -18
  134. infrahub/transformations/tasks.py +10 -6
  135. infrahub/trigger/tasks.py +4 -3
  136. infrahub/types.py +4 -0
  137. infrahub/validators/events.py +7 -7
  138. infrahub/validators/tasks.py +6 -7
  139. infrahub/webhook/models.py +45 -45
  140. infrahub/webhook/tasks.py +25 -24
  141. infrahub/workers/dependencies.py +143 -0
  142. infrahub/workers/infrahub_async.py +19 -43
  143. infrahub/workflows/catalogue.py +16 -2
  144. infrahub/workflows/initialization.py +5 -4
  145. infrahub/workflows/models.py +2 -0
  146. infrahub_sdk/client.py +6 -6
  147. infrahub_sdk/ctl/repository.py +51 -0
  148. infrahub_sdk/ctl/schema.py +9 -9
  149. infrahub_sdk/protocols.py +40 -6
  150. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/METADATA +5 -4
  151. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/RECORD +158 -144
  152. infrahub_testcontainers/container.py +17 -0
  153. infrahub_testcontainers/docker-compose-cluster.test.yml +56 -1
  154. infrahub_testcontainers/docker-compose.test.yml +56 -1
  155. infrahub_testcontainers/helpers.py +4 -1
  156. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/LICENSE.txt +0 -0
  157. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/WHEEL +0 -0
  158. {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import TYPE_CHECKING, Any, Self
2
4
 
3
- from graphene import Boolean, Field, InputObjectType, Mutation, String
5
+ from graphene import Boolean, Enum, Field, InputObjectType, List, Mutation, String
4
6
  from graphql import GraphQLResolveInfo
5
7
 
8
+ from infrahub import lock
6
9
  from infrahub.core.account import GlobalPermission
7
10
  from infrahub.core.branch import Branch
8
11
  from infrahub.core.constants import (
@@ -12,26 +15,45 @@ from infrahub.core.constants import (
12
15
  PermissionDecision,
13
16
  )
14
17
  from infrahub.core.manager import NodeManager
15
- from infrahub.core.node import Node
18
+ from infrahub.core.protocols import CoreProposedChange
16
19
  from infrahub.core.schema import NodeSchema
17
20
  from infrahub.database import InfrahubDatabase, retry_db_transaction
21
+ from infrahub.events import (
22
+ EventMeta,
23
+ ProposedChangeApprovalRevokedEvent,
24
+ ProposedChangeApprovedEvent,
25
+ ProposedChangeRejectedEvent,
26
+ ProposedChangeRejectionRevokedEvent,
27
+ )
18
28
  from infrahub.exceptions import BranchNotFoundError, PermissionDeniedError, ValidationError
19
29
  from infrahub.graphql.mutations.main import InfrahubMutationMixin
20
30
  from infrahub.graphql.types.enums import CheckType as GraphQLCheckType
21
- from infrahub.proposed_change.constants import ProposedChangeState
31
+ from infrahub.graphql.types.task import TaskInfo
32
+ from infrahub.lock import InfrahubLock, build_object_lock_name
33
+ from infrahub.proposed_change.approval_revoker import do_revoke_approvals_on_updated_pcs
34
+ from infrahub.proposed_change.constants import ProposedChangeApprovalDecision, ProposedChangeState
35
+ from infrahub.proposed_change.models import RequestProposedChangePipeline
36
+ from infrahub.workers.dependencies import get_event_service
22
37
  from infrahub.workflows.catalogue import PROPOSED_CHANGE_MERGE, REQUEST_PROPOSED_CHANGE_PIPELINE
23
38
 
24
- from ...proposed_change.models import RequestProposedChangePipeline
25
- from ..types.task import TaskInfo
26
39
  from .main import InfrahubMutationOptions
27
40
 
28
41
  if TYPE_CHECKING:
42
+ from graphql import GraphQLResolveInfo
43
+
44
+ from infrahub.core.node import Node
45
+ from infrahub.events.models import InfrahubEvent
46
+
29
47
  from ..initialization import GraphqlContext
30
48
 
49
+ ProposedChangeApprovalDecisionInput = Enum.from_enum(ProposedChangeApprovalDecision)
50
+
31
51
 
32
52
  class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
33
53
  @classmethod
34
- def __init_subclass_with_meta__(cls, schema: NodeSchema = None, _meta=None, **options) -> None:
54
+ def __init_subclass_with_meta__(
55
+ cls, schema: NodeSchema, _meta: InfrahubMutationOptions | None = None, **options: dict[str, Any]
56
+ ) -> None:
35
57
  # Make sure schema is a valid NodeSchema Node Class
36
58
  if not isinstance(schema, NodeSchema):
37
59
  raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
@@ -50,11 +72,19 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
50
72
  data: InputObjectType,
51
73
  branch: Branch,
52
74
  database: InfrahubDatabase | None = None, # noqa: ARG003
75
+ override_data: dict[str, Any] | None = None,
53
76
  ) -> tuple[Node, Self]:
54
77
  graphql_context: GraphqlContext = info.context
55
78
 
79
+ override_data = {"created_by": {"id": graphql_context.active_account_session.account_id}}
80
+ state = data.get("state", {}).get("value")
81
+ if state and state != ProposedChangeState.OPEN.value:
82
+ raise ValidationError(input_value="A proposed change has to be in the open state during creation")
83
+
56
84
  async with graphql_context.db.start_transaction() as dbt:
57
- proposed_change, result = await super().mutate_create(info=info, data=data, branch=branch, database=dbt)
85
+ proposed_change, result = await super().mutate_create(
86
+ info=info, data=data, branch=branch, database=dbt, override_data=override_data
87
+ )
58
88
  destination_branch = proposed_change.destination_branch.value
59
89
  source_branch = await _get_source_branch(db=dbt, name=proposed_change.source_branch.value)
60
90
  if destination_branch == source_branch.name:
@@ -100,7 +130,7 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
100
130
  include_source=True,
101
131
  )
102
132
  state = ProposedChangeState(obj.state.value.value)
103
- state.validate_editability()
133
+ state.validate_updatable()
104
134
 
105
135
  updated_state = None
106
136
  if state_update := data.get("state", {}).get("value"):
@@ -192,6 +222,165 @@ class ProposedChangeRequestRunCheck(Mutation):
192
222
  return {"ok": True}
193
223
 
194
224
 
225
+ class ProposedChangeReviewInput(InputObjectType):
226
+ id = String(required=True, description="The ID of the proposed change to review.")
227
+ decision = ProposedChangeApprovalDecisionInput(
228
+ required=True, description="The decision for the proposed change review."
229
+ )
230
+
231
+
232
+ class ProposedChangeReview(Mutation):
233
+ class Arguments:
234
+ data = ProposedChangeReviewInput(required=True)
235
+
236
+ ok = Boolean()
237
+
238
+ @classmethod
239
+ async def mutate(
240
+ cls,
241
+ root: dict, # noqa: ARG003
242
+ info: GraphQLResolveInfo,
243
+ data: ProposedChangeReviewInput,
244
+ ) -> dict[str, bool]:
245
+ """
246
+ This mutation is used to approve or reject a proposed change.
247
+ It can also be used to undo an approval or rejection.
248
+ """
249
+
250
+ graphql_context: GraphqlContext = info.context
251
+ graphql_context.active_permissions.raise_for_permission(
252
+ permission=GlobalPermission(
253
+ action=GlobalPermissions.REVIEW_PROPOSED_CHANGE.value, decision=PermissionDecision.ALLOW_ALL.value
254
+ )
255
+ )
256
+ pc_id = str(data.id)
257
+ lock_name = build_object_lock_name(pc_id)
258
+ async with InfrahubLock(name=lock_name, connection=lock.registry.connection):
259
+ proposed_change = await NodeManager.get_one_by_id_or_default_filter(
260
+ id=pc_id, kind=CoreProposedChange, db=graphql_context.db, prefetch_relationships=True
261
+ )
262
+ state = ProposedChangeState(proposed_change.state.value.value)
263
+ state.validate_reviewable()
264
+
265
+ created_by = await proposed_change.created_by.get_peer(db=graphql_context.db)
266
+ if created_by and created_by.id == graphql_context.active_account_session.account_id:
267
+ raise ValidationError(input_value="You cannot review your own proposed changes")
268
+
269
+ current_user = await NodeManager.get_one_by_id_or_default_filter(
270
+ id=graphql_context.active_account_session.account_id,
271
+ kind=InfrahubKind.GENERICACCOUNT,
272
+ db=graphql_context.db,
273
+ )
274
+
275
+ async with graphql_context.db.start_session() as db:
276
+ event = await cls._handle_decision(
277
+ db=db,
278
+ decision=data.decision,
279
+ proposed_change=proposed_change,
280
+ current_user=current_user,
281
+ context=graphql_context,
282
+ )
283
+ await proposed_change.save(db=db)
284
+
285
+ if event:
286
+ event_service = await get_event_service()
287
+ await event_service.send(event=event)
288
+
289
+ return {"ok": True}
290
+
291
+ @classmethod
292
+ async def _handle_decision(
293
+ cls,
294
+ db: InfrahubDatabase,
295
+ decision: ProposedChangeApprovalDecision,
296
+ proposed_change: CoreProposedChange,
297
+ current_user: Node,
298
+ context: GraphqlContext,
299
+ ) -> InfrahubEvent | None:
300
+ """Modify approved_by and rejected_by relationships of the prpoposed change based on the decision."""
301
+
302
+ approved_by = await proposed_change.approved_by.get_peers(db=db)
303
+ rejected_by = await proposed_change.rejected_by.get_peers(db=db)
304
+ approved_by_ids = [node.id for _, node in approved_by.items()]
305
+ rejected_by_ids = [node.id for _, node in rejected_by.items()]
306
+ event: InfrahubEvent | None = None
307
+ event_meta = EventMeta.from_context(context=context.get_context())
308
+
309
+ match decision:
310
+ case ProposedChangeApprovalDecision.APPROVE:
311
+ if current_user.id in approved_by_ids:
312
+ raise ValidationError(input_value="You have already approved this proposed change")
313
+ await proposed_change.approved_by.add(db=db, data=current_user)
314
+ if current_user.id in rejected_by_ids:
315
+ await proposed_change.rejected_by.remove_locally(db=db, peer_id=current_user.id)
316
+
317
+ event = ProposedChangeApprovedEvent(
318
+ proposed_change_id=proposed_change.id,
319
+ proposed_change_name=proposed_change.name.value,
320
+ proposed_change_state=proposed_change.state.value,
321
+ reviewer_account_id=current_user.id,
322
+ reviewer_account_name=current_user.name.value,
323
+ reviewer_decision=decision.value,
324
+ meta=event_meta,
325
+ )
326
+
327
+ case ProposedChangeApprovalDecision.CANCEL_APPROVE:
328
+ if current_user.id not in approved_by_ids:
329
+ raise ValidationError(
330
+ input_value="You did not approve this proposed change yet, it can't be un-approved"
331
+ )
332
+ await proposed_change.approved_by.remove_locally(db=db, peer_id=current_user.id)
333
+
334
+ event = ProposedChangeApprovalRevokedEvent(
335
+ proposed_change_id=proposed_change.id,
336
+ proposed_change_name=proposed_change.name.value,
337
+ proposed_change_state=proposed_change.state.value,
338
+ reviewer_account_id=current_user.id,
339
+ reviewer_account_name=current_user.name.value,
340
+ reviewer_former_decision=ProposedChangeApprovalDecision.APPROVE.value,
341
+ meta=event_meta,
342
+ )
343
+
344
+ case ProposedChangeApprovalDecision.REJECT:
345
+ if current_user.id in rejected_by_ids:
346
+ raise ValidationError(input_value="You have already rejected this proposed change")
347
+ await proposed_change.rejected_by.add(db=db, data=current_user)
348
+ if current_user.id in approved_by_ids:
349
+ await proposed_change.approved_by.remove_locally(db=db, peer_id=current_user.id)
350
+
351
+ event = ProposedChangeRejectedEvent(
352
+ proposed_change_id=proposed_change.id,
353
+ proposed_change_name=proposed_change.name.value,
354
+ proposed_change_state=proposed_change.state.value,
355
+ reviewer_account_id=current_user.id,
356
+ reviewer_account_name=current_user.name.value,
357
+ reviewer_decision=decision.value,
358
+ meta=event_meta,
359
+ )
360
+
361
+ case ProposedChangeApprovalDecision.CANCEL_REJECT:
362
+ if current_user.id not in rejected_by_ids:
363
+ raise ValidationError(
364
+ input_value="You did not reject this proposed change yet, it can't be un-rejected"
365
+ )
366
+ await proposed_change.rejected_by.remove_locally(db=db, peer_id=current_user.id)
367
+
368
+ event = ProposedChangeRejectionRevokedEvent(
369
+ proposed_change_id=proposed_change.id,
370
+ proposed_change_name=proposed_change.name.value,
371
+ proposed_change_state=proposed_change.state.value,
372
+ reviewer_account_id=current_user.id,
373
+ reviewer_account_name=current_user.name.value,
374
+ reviewer_former_decision=ProposedChangeApprovalDecision.REJECT.value,
375
+ meta=event_meta,
376
+ )
377
+
378
+ case _:
379
+ raise ValidationError(input_value=f"Invalid decision {decision}")
380
+
381
+ return event
382
+
383
+
195
384
  class ProposedChangeMergeInput(InputObjectType):
196
385
  id = String(required=True)
197
386
 
@@ -250,6 +439,34 @@ class ProposedChangeMerge(Mutation):
250
439
  return cls(ok=True, task=task)
251
440
 
252
441
 
442
+ class ProposedChangeCheckForApprovalRevokeInput(InputObjectType):
443
+ ids = Field(List(of_type=String, required=True), required=False)
444
+
445
+
446
+ class ProposedChangeCheckForApprovalRevoke(Mutation):
447
+ class Arguments:
448
+ data = ProposedChangeCheckForApprovalRevokeInput(required=True)
449
+
450
+ ok = Boolean()
451
+
452
+ @classmethod
453
+ async def mutate(
454
+ cls,
455
+ root: dict, # noqa: ARG003
456
+ info: GraphQLResolveInfo,
457
+ data: dict[str, Any],
458
+ ) -> dict[str, bool]:
459
+ db = info.context.db
460
+ ids: list[str] | None
461
+ try:
462
+ ids = data["ids"]
463
+ except KeyError:
464
+ ids = None
465
+
466
+ await do_revoke_approvals_on_updated_pcs(db=db, proposed_changes_ids=ids)
467
+ return cls(ok=True)
468
+
469
+
253
470
  async def _get_source_branch(db: InfrahubDatabase, name: str) -> Branch:
254
471
  try:
255
472
  return await Branch.get_by_name(name=name, db=db)
@@ -332,6 +332,11 @@ async def _validate_node(info: GraphQLResolveInfo, data: RelationshipNodesInput)
332
332
  if rel_schema.cardinality != RelationshipCardinality.MANY:
333
333
  raise ValidationError({"name": f"'{relationship_name}' must be a relationship of cardinality Many"})
334
334
 
335
+ if rel_schema.read_only:
336
+ # These mutations should never be allowed to update read-only relationships, as those typically
337
+ # have custom code tied to them such as the approved_by relationship of a CoreProposedChange.
338
+ raise ValidationError({source.get_kind(): f"'{relationship_name}' is a read-only relationship"})
339
+
335
340
  return source
336
341
 
337
342
 
@@ -64,12 +64,13 @@ class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
64
64
  data: InputObjectType,
65
65
  branch: Branch,
66
66
  database: InfrahubDatabase | None = None, # noqa: ARG003
67
+ override_data: dict[str, Any] | None = None,
67
68
  ) -> tuple[Node, Self]:
68
69
  graphql_context: GraphqlContext = info.context
69
70
 
70
71
  cleanup_payload(data)
71
72
  # Create the object in the database
72
- obj, result = await super().mutate_create(info, data, branch)
73
+ obj, result = await super().mutate_create(info, data, branch, override_data=override_data)
73
74
  obj = cast(CoreGenericRepository, obj)
74
75
 
75
76
  # First check the connectivity to the remote repository
@@ -15,16 +15,17 @@ from infrahub.core.validators.models.validate_migration import SchemaValidateMig
15
15
  from infrahub.core.validators.tasks import schema_validate_migrations
16
16
  from infrahub.dependencies.registry import get_component_registry
17
17
  from infrahub.exceptions import ValidationError
18
- from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
18
+ from infrahub.workers.dependencies import get_database, get_workflow
19
19
  from infrahub.workflows.catalogue import BRANCH_MERGE
20
20
  from infrahub.workflows.utils import add_tags
21
21
 
22
22
 
23
23
  @flow(name="merge-branch-mutation", flow_run_name="Merge branch graphQL mutation")
24
- async def merge_branch_mutation(branch: str, context: InfrahubContext, service: InfrahubServices) -> None:
24
+ async def merge_branch_mutation(branch: str, context: InfrahubContext) -> None:
25
25
  await add_tags(branches=[branch])
26
26
 
27
- async with service.database.start_session() as db:
27
+ database = await get_database()
28
+ async with database.start_session() as db:
28
29
  obj = await Branch.get_by_name(db=db, name=branch)
29
30
  base_branch = await Branch.get_by_name(db=db, name=registry.default_branch)
30
31
 
@@ -52,7 +53,7 @@ async def merge_branch_mutation(branch: str, context: InfrahubContext, service:
52
53
  diff_repository=diff_repository,
53
54
  source_branch=obj,
54
55
  diff_locker=DiffLocker(),
55
- service=service,
56
+ workflow=get_workflow(),
56
57
  )
57
58
  candidate_schema = merger.get_candidate_schema()
58
59
  determiner = ConstraintValidatorDeterminer(schema_branch=candidate_schema)
@@ -62,13 +63,10 @@ async def merge_branch_mutation(branch: str, context: InfrahubContext, service:
62
63
 
63
64
  if constraints:
64
65
  responses = await schema_validate_migrations(
65
- message=SchemaValidateMigrationData(
66
- branch=obj, schema_branch=candidate_schema, constraints=constraints
67
- ),
68
- service=service,
66
+ message=SchemaValidateMigrationData(branch=obj, schema_branch=candidate_schema, constraints=constraints)
69
67
  )
70
68
  error_messages = [violation.message for response in responses for violation in response.violations]
71
69
  if error_messages:
72
70
  raise ValidationError(",\n".join(error_messages))
73
71
 
74
- await service.workflow.execute_workflow(workflow=BRANCH_MERGE, context=context, parameters={"branch": obj.name})
72
+ await get_workflow().execute_workflow(workflow=BRANCH_MERGE, context=context, parameters={"branch": obj.name})
@@ -67,6 +67,7 @@ class InfrahubWebhookMutation(InfrahubMutationMixin, Mutation):
67
67
  data: InputObjectType,
68
68
  branch: Branch,
69
69
  database: InfrahubDatabase | None = None,
70
+ override_data: dict[str, Any] | None = None,
70
71
  ) -> tuple[Node, Self]:
71
72
  graphql_context: GraphqlContext = info.context
72
73
 
@@ -74,7 +75,9 @@ class InfrahubWebhookMutation(InfrahubMutationMixin, Mutation):
74
75
 
75
76
  _validate_input(graphql_context=graphql_context, branch=branch, input_data=input_data)
76
77
 
77
- obj, result = await super().mutate_create(info=info, data=data, branch=branch, database=database)
78
+ obj, result = await super().mutate_create(
79
+ info=info, data=data, branch=branch, database=database, override_data=override_data
80
+ )
78
81
 
79
82
  return obj, result
80
83
 
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
  from graphql.language import (
7
7
  DirectiveNode,
8
8
  FieldNode,
9
+ FragmentSpreadNode,
9
10
  InlineFragmentNode,
10
11
  ListValueNode,
11
12
  NameNode,
@@ -15,7 +16,9 @@ from graphql.language import (
15
16
  from infrahub_sdk.utils import deep_merge_dict
16
17
 
17
18
  if TYPE_CHECKING:
18
- from infrahub.core.schema import NodeSchema
19
+ from graphql import GraphQLResolveInfo
20
+
21
+ from infrahub.core.schema import GenericSchema, NodeSchema
19
22
 
20
23
 
21
24
  @dataclass
@@ -26,14 +29,14 @@ class FieldEnricher:
26
29
  fields: dict = field(default_factory=dict)
27
30
 
28
31
 
29
- async def extract_selection(field_node: FieldNode, schema: NodeSchema) -> dict:
30
- graphql_extractor = GraphQLExtractor(field_node=field_node, schema=schema)
32
+ async def extract_selection(info: GraphQLResolveInfo, schema: NodeSchema | GenericSchema) -> dict:
33
+ graphql_extractor = GraphQLExtractor(info=info, schema=schema)
31
34
  return await graphql_extractor.get_fields()
32
35
 
33
36
 
34
37
  class GraphQLExtractor:
35
- def __init__(self, field_node: FieldNode, schema: NodeSchema) -> None:
36
- self.field_node = field_node
38
+ def __init__(self, info: GraphQLResolveInfo, schema: NodeSchema | GenericSchema) -> None:
39
+ self.info = info
37
40
  self.schema = schema
38
41
  self.typename_paths: dict[str, list[FieldEnricher]] = {}
39
42
  self.node_path: dict[str, list[FieldEnricher]] = {}
@@ -43,7 +46,7 @@ class GraphQLExtractor:
43
46
  self.node_path[path] = []
44
47
 
45
48
  async def get_fields(self) -> dict:
46
- return await self.extract_fields(selection_set=self.field_node.selection_set) or {}
49
+ return await self.extract_fields(selection_set=self.info.field_nodes[0].selection_set) or {}
47
50
 
48
51
  def _process_expand_directive(self, path: str, directive: DirectiveNode) -> None:
49
52
  excluded_fields = []
@@ -204,6 +207,12 @@ class GraphQLExtractor:
204
207
  elif isinstance(fields[sub_node.name.value], dict) and isinstance(value, dict):
205
208
  fields[sub_node.name.value].update(value) # type: ignore[union-attr]
206
209
 
210
+ elif isinstance(node, FragmentSpreadNode):
211
+ if node.name.value in self.info.fragments:
212
+ fragment_fields = await self.extract_fields(self.info.fragments[node.name.value].selection_set)
213
+ if fragment_fields:
214
+ fields.update(fragment_fields)
215
+
207
216
  return self.apply_directives(selection_set=selection_set, fields=fields, path=path)
208
217
 
209
218
 
@@ -1,7 +1,13 @@
1
1
  from .account import AccountPermissions, AccountToken
2
2
  from .branch import BranchQueryList
3
3
  from .internal import InfrahubInfo
4
- from .ipam import InfrahubIPAddressGetNextAvailable, InfrahubIPPrefixGetNextAvailable
4
+ from .ipam import (
5
+ DeprecatedIPAddressGetNextAvailable,
6
+ DeprecatedIPPrefixGetNextAvailable,
7
+ InfrahubIPAddressGetNextAvailable,
8
+ InfrahubIPPrefixGetNextAvailable,
9
+ )
10
+ from .proposed_change import ProposedChangeAvailableActions
5
11
  from .relationship import Relationship
6
12
  from .resource_manager import InfrahubResourcePoolAllocated, InfrahubResourcePoolUtilization
7
13
  from .search import InfrahubSearchAnywhere
@@ -12,6 +18,8 @@ __all__ = [
12
18
  "AccountPermissions",
13
19
  "AccountToken",
14
20
  "BranchQueryList",
21
+ "DeprecatedIPAddressGetNextAvailable",
22
+ "DeprecatedIPPrefixGetNextAvailable",
15
23
  "InfrahubIPAddressGetNextAvailable",
16
24
  "InfrahubIPPrefixGetNextAvailable",
17
25
  "InfrahubInfo",
@@ -19,6 +27,7 @@ __all__ = [
19
27
  "InfrahubResourcePoolUtilization",
20
28
  "InfrahubSearchAnywhere",
21
29
  "InfrahubStatus",
30
+ "ProposedChangeAvailableActions",
22
31
  "Relationship",
23
32
  "Task",
24
33
  ]
@@ -3,11 +3,11 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from graphene import Field, Int, List, NonNull, ObjectType, String
6
- from infrahub_sdk.utils import extract_fields_first_node
7
6
 
8
7
  from infrahub.core.manager import NodeManager
9
8
  from infrahub.core.protocols import InternalAccountToken
10
9
  from infrahub.exceptions import PermissionDeniedError
10
+ from infrahub.graphql.field_extractor import extract_graphql_fields
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from graphql import GraphQLResolveInfo
@@ -44,7 +44,7 @@ async def resolve_account_tokens(
44
44
  if not graphql_context.account_session.authenticated_by_jwt:
45
45
  raise PermissionDeniedError("This operation requires authentication with a JWT token")
46
46
 
47
- fields = await extract_fields_first_node(info)
47
+ fields = extract_graphql_fields(info)
48
48
 
49
49
  filters = {"account__ids": [graphql_context.account_session.account_id]}
50
50
  response: dict[str, Any] = {}
@@ -121,7 +121,7 @@ async def resolve_account_permissions(
121
121
  if not graphql_context.account_session:
122
122
  raise ValueError("An account_session is mandatory to execute this query")
123
123
 
124
- fields = await extract_fields_first_node(info)
124
+ fields = extract_graphql_fields(info)
125
125
 
126
126
  response: dict[str, dict[str, Any]] = {}
127
127
  if "global_permissions" in fields:
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from graphene import ID, Field, List, NonNull, String
6
- from infrahub_sdk.utils import extract_fields_first_node
7
6
 
7
+ from infrahub.graphql.field_extractor import extract_graphql_fields
8
8
  from infrahub.graphql.types import BranchType
9
9
 
10
10
  if TYPE_CHECKING:
@@ -16,7 +16,7 @@ async def branch_resolver(
16
16
  info: GraphQLResolveInfo,
17
17
  **kwargs: Any,
18
18
  ) -> list[dict[str, Any]]:
19
- fields = await extract_fields_first_node(info)
19
+ fields = extract_graphql_fields(info)
20
20
  return await BranchType.get_list(graphql_context=info.context, fields=fields, **kwargs)
21
21
 
22
22
 
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from graphene import Argument, Boolean, DateTime, Field, InputObjectType, Int, List, NonNull, ObjectType, String
6
6
  from graphene import Enum as GrapheneEnum
7
- from infrahub_sdk.utils import extract_fields
8
7
 
9
8
  from infrahub.core import registry
10
9
  from infrahub.core.constants import DiffAction, RelationshipCardinality
@@ -16,6 +15,7 @@ from infrahub.core.query.diff import DiffCountChanges
16
15
  from infrahub.core.timestamp import Timestamp
17
16
  from infrahub.dependencies.registry import get_component_registry
18
17
  from infrahub.graphql.enums import ConflictSelection as GraphQLConflictSelection
18
+ from infrahub.graphql.field_extractor import extract_graphql_fields
19
19
 
20
20
  if TYPE_CHECKING:
21
21
  from datetime import datetime
@@ -438,7 +438,7 @@ class DiffTreeResolver:
438
438
  else:
439
439
  enriched_diff = enriched_diffs[0]
440
440
 
441
- full_fields = await extract_fields(info.field_nodes[0].selection_set)
441
+ full_fields = extract_graphql_fields(info=info)
442
442
  diff_tree = await self.to_diff_tree(enriched_diff_root=enriched_diff, graphql_context=graphql_context)
443
443
  need_base_changes = "num_untracked_base_changes" in full_fields
444
444
  need_branch_changes = "num_untracked_diff_changes" in full_fields
@@ -495,7 +495,7 @@ class DiffTreeResolver:
495
495
  to_time=summary.to_time.to_datetime(),
496
496
  **summary.model_dump(exclude={"from_time", "to_time"}),
497
497
  )
498
- full_fields = await extract_fields(info.field_nodes[0].selection_set)
498
+ full_fields = extract_graphql_fields(info=info)
499
499
  need_base_changes = "num_untracked_base_changes" in full_fields
500
500
  need_branch_changes = "num_untracked_diff_changes" in full_fields
501
501
  if need_base_changes or need_branch_changes:
@@ -2,10 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
- from graphene import Argument, Boolean, DateTime, Field, Int, List, NonNull, ObjectType, String
6
- from infrahub_sdk.utils import extract_fields_first_node
5
+ from graphene import Argument, Boolean, DateTime, Enum, Field, Int, List, NonNull, ObjectType, String
7
6
 
7
+ from infrahub.events.constants import EventSortOrder
8
8
  from infrahub.exceptions import ValidationError
9
+ from infrahub.graphql.field_extractor import extract_graphql_fields
9
10
  from infrahub.graphql.types.event import EventNodes, EventTypeFilter
10
11
  from infrahub.task_manager.event import PrefectEvent
11
12
  from infrahub.task_manager.models import InfrahubEventFilter
@@ -15,6 +16,8 @@ if TYPE_CHECKING:
15
16
 
16
17
  from graphql import GraphQLResolveInfo
17
18
 
19
+ InfrahubEventSortOrder = Enum.from_enum(EventSortOrder)
20
+
18
21
 
19
22
  class Events(ObjectType):
20
23
  edges = List(NonNull(EventNodes), required=True)
@@ -24,6 +27,7 @@ class Events(ObjectType):
24
27
  async def resolve(
25
28
  root: dict, # noqa: ARG004
26
29
  info: GraphQLResolveInfo,
30
+ order: EventSortOrder,
27
31
  limit: int = 10,
28
32
  has_children: bool | None = None,
29
33
  level: int | None = None,
@@ -57,6 +61,7 @@ class Events(ObjectType):
57
61
  since=since,
58
62
  until=until,
59
63
  level=level,
64
+ order=order,
60
65
  )
61
66
 
62
67
  return await Events.query(
@@ -74,7 +79,7 @@ class Events(ObjectType):
74
79
  limit: int,
75
80
  offset: int | None = None,
76
81
  ) -> dict[str, Any]:
77
- fields = await extract_fields_first_node(info)
82
+ fields = extract_graphql_fields(info)
78
83
 
79
84
  prefect_tasks = await PrefectEvent.query(
80
85
  fields=fields,
@@ -110,6 +115,11 @@ Event = Field(
110
115
  branches=List(NonNull(String), required=False, description="Filter the query to specific branches"),
111
116
  account__ids=List(NonNull(String), required=False, description="Filter the query to specific accounts"),
112
117
  ids=List(NonNull(String)),
118
+ order=InfrahubEventSortOrder(
119
+ required=False,
120
+ default_value=EventSortOrder.DESC,
121
+ description="Sort order of the events, defaults to descending order",
122
+ ),
113
123
  resolver=Events.resolve,
114
124
  required=True,
115
125
  )
@@ -74,7 +74,7 @@ class IPPrefixGetNextAvailable(ObjectType):
74
74
  root: dict, # noqa: ARG004
75
75
  info: GraphQLResolveInfo,
76
76
  prefix_id: str,
77
- prefix_length: int,
77
+ prefix_length: int | None = None,
78
78
  ) -> dict[str, str]:
79
79
  graphql_context: GraphqlContext = info.context
80
80
 
@@ -119,3 +119,25 @@ InfrahubIPPrefixGetNextAvailable = Field(
119
119
  resolver=IPPrefixGetNextAvailable.resolve,
120
120
  required=True,
121
121
  )
122
+
123
+ # The following two query fields must be removed once we are sure that people are not using the old queries anymore. Those fields only exist to
124
+ # expose a deprecation message.
125
+
126
+ DeprecatedIPAddressGetNextAvailable = Field(
127
+ IPAddressGetNextAvailable,
128
+ prefix_id=String(required=True),
129
+ prefix_length=Int(required=False),
130
+ resolver=IPAddressGetNextAvailable.resolve,
131
+ required=True,
132
+ deprecation_reason="This query has been renamed to 'InfrahubIPAddressGetNextAvailable'. It will be removed in the next version of Infrahub.",
133
+ )
134
+
135
+
136
+ DeprecatedIPPrefixGetNextAvailable = Field(
137
+ IPPrefixGetNextAvailable,
138
+ prefix_id=String(required=True),
139
+ prefix_length=Int(required=False),
140
+ resolver=IPPrefixGetNextAvailable.resolve,
141
+ required=True,
142
+ deprecation_reason="This query has been renamed to 'InfrahubIPPrefixGetNextAvailable'. It will be removed in the next version of Infrahub.",
143
+ )