infrahub-server 1.4.13__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/internal.py +2 -0
  5. infrahub/api/query.py +2 -0
  6. infrahub/api/schema.py +27 -3
  7. infrahub/auth.py +5 -5
  8. infrahub/cli/__init__.py +2 -0
  9. infrahub/cli/db.py +160 -157
  10. infrahub/cli/dev.py +118 -0
  11. infrahub/cli/upgrade.py +56 -9
  12. infrahub/computed_attribute/tasks.py +19 -7
  13. infrahub/config.py +7 -2
  14. infrahub/core/attribute.py +35 -24
  15. infrahub/core/branch/enums.py +1 -1
  16. infrahub/core/branch/models.py +9 -5
  17. infrahub/core/branch/needs_rebase_status.py +11 -0
  18. infrahub/core/branch/tasks.py +72 -10
  19. infrahub/core/changelog/models.py +2 -10
  20. infrahub/core/constants/__init__.py +4 -0
  21. infrahub/core/constants/infrahubkind.py +1 -0
  22. infrahub/core/convert_object_type/object_conversion.py +201 -0
  23. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  24. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  25. infrahub/core/diff/model/path.py +4 -0
  26. infrahub/core/diff/payload_builder.py +1 -1
  27. infrahub/core/diff/query/artifact.py +1 -0
  28. infrahub/core/diff/query/field_summary.py +1 -0
  29. infrahub/core/graph/__init__.py +1 -1
  30. infrahub/core/initialization.py +7 -4
  31. infrahub/core/manager.py +3 -81
  32. infrahub/core/migrations/__init__.py +3 -0
  33. infrahub/core/migrations/exceptions.py +4 -0
  34. infrahub/core/migrations/graph/__init__.py +11 -10
  35. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  37. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  38. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  39. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  40. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  41. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  42. infrahub/core/migrations/query/__init__.py +7 -8
  43. infrahub/core/migrations/query/attribute_add.py +8 -6
  44. infrahub/core/migrations/query/attribute_remove.py +134 -0
  45. infrahub/core/migrations/runner.py +54 -0
  46. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  47. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  48. infrahub/core/migrations/schema/node_attribute_add.py +26 -5
  49. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  50. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  51. infrahub/core/migrations/schema/node_remove.py +2 -1
  52. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  53. infrahub/core/migrations/shared.py +66 -19
  54. infrahub/core/models.py +2 -2
  55. infrahub/core/node/__init__.py +207 -54
  56. infrahub/core/node/create.py +53 -49
  57. infrahub/core/node/lock_utils.py +124 -0
  58. infrahub/core/node/node_property_attribute.py +230 -0
  59. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  60. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  61. infrahub/core/node/resource_manager/number_pool.py +2 -1
  62. infrahub/core/node/standard.py +1 -1
  63. infrahub/core/property.py +11 -0
  64. infrahub/core/protocols.py +8 -1
  65. infrahub/core/query/attribute.py +82 -15
  66. infrahub/core/query/ipam.py +16 -4
  67. infrahub/core/query/node.py +66 -188
  68. infrahub/core/query/relationship.py +44 -26
  69. infrahub/core/query/subquery.py +0 -8
  70. infrahub/core/relationship/model.py +69 -24
  71. infrahub/core/schema/__init__.py +56 -0
  72. infrahub/core/schema/attribute_schema.py +4 -2
  73. infrahub/core/schema/basenode_schema.py +42 -2
  74. infrahub/core/schema/definitions/core/__init__.py +2 -0
  75. infrahub/core/schema/definitions/core/check.py +1 -1
  76. infrahub/core/schema/definitions/core/generator.py +2 -0
  77. infrahub/core/schema/definitions/core/group.py +16 -2
  78. infrahub/core/schema/definitions/core/repository.py +7 -0
  79. infrahub/core/schema/definitions/core/transform.py +1 -1
  80. infrahub/core/schema/definitions/internal.py +12 -3
  81. infrahub/core/schema/generated/attribute_schema.py +2 -2
  82. infrahub/core/schema/generated/base_node_schema.py +6 -1
  83. infrahub/core/schema/manager.py +3 -0
  84. infrahub/core/schema/node_schema.py +1 -0
  85. infrahub/core/schema/relationship_schema.py +0 -1
  86. infrahub/core/schema/schema_branch.py +295 -10
  87. infrahub/core/schema/schema_branch_display.py +135 -0
  88. infrahub/core/schema/schema_branch_hfid.py +120 -0
  89. infrahub/core/validators/aggregated_checker.py +1 -1
  90. infrahub/database/graph.py +21 -0
  91. infrahub/display_labels/__init__.py +0 -0
  92. infrahub/display_labels/gather.py +48 -0
  93. infrahub/display_labels/models.py +240 -0
  94. infrahub/display_labels/tasks.py +192 -0
  95. infrahub/display_labels/triggers.py +22 -0
  96. infrahub/events/branch_action.py +27 -1
  97. infrahub/events/group_action.py +1 -1
  98. infrahub/events/node_action.py +1 -1
  99. infrahub/generators/constants.py +7 -0
  100. infrahub/generators/models.py +38 -12
  101. infrahub/generators/tasks.py +34 -16
  102. infrahub/git/base.py +38 -1
  103. infrahub/git/integrator.py +22 -14
  104. infrahub/graphql/api/dependencies.py +2 -4
  105. infrahub/graphql/api/endpoints.py +16 -6
  106. infrahub/graphql/app.py +2 -4
  107. infrahub/graphql/initialization.py +2 -3
  108. infrahub/graphql/manager.py +213 -137
  109. infrahub/graphql/middleware.py +12 -0
  110. infrahub/graphql/mutations/branch.py +16 -0
  111. infrahub/graphql/mutations/computed_attribute.py +110 -3
  112. infrahub/graphql/mutations/convert_object_type.py +44 -13
  113. infrahub/graphql/mutations/display_label.py +118 -0
  114. infrahub/graphql/mutations/generator.py +25 -7
  115. infrahub/graphql/mutations/hfid.py +125 -0
  116. infrahub/graphql/mutations/ipam.py +73 -41
  117. infrahub/graphql/mutations/main.py +61 -178
  118. infrahub/graphql/mutations/profile.py +195 -0
  119. infrahub/graphql/mutations/proposed_change.py +8 -1
  120. infrahub/graphql/mutations/relationship.py +2 -2
  121. infrahub/graphql/mutations/repository.py +22 -83
  122. infrahub/graphql/mutations/resource_manager.py +2 -2
  123. infrahub/graphql/mutations/webhook.py +1 -1
  124. infrahub/graphql/queries/resource_manager.py +1 -1
  125. infrahub/graphql/registry.py +173 -0
  126. infrahub/graphql/resolvers/resolver.py +2 -0
  127. infrahub/graphql/schema.py +8 -1
  128. infrahub/graphql/schema_sort.py +170 -0
  129. infrahub/graphql/types/branch.py +4 -1
  130. infrahub/graphql/types/enums.py +3 -0
  131. infrahub/groups/tasks.py +1 -1
  132. infrahub/hfid/__init__.py +0 -0
  133. infrahub/hfid/gather.py +48 -0
  134. infrahub/hfid/models.py +240 -0
  135. infrahub/hfid/tasks.py +191 -0
  136. infrahub/hfid/triggers.py +22 -0
  137. infrahub/lock.py +119 -42
  138. infrahub/locks/__init__.py +0 -0
  139. infrahub/locks/tasks.py +37 -0
  140. infrahub/patch/plan_writer.py +2 -2
  141. infrahub/permissions/constants.py +2 -0
  142. infrahub/profiles/__init__.py +0 -0
  143. infrahub/profiles/node_applier.py +101 -0
  144. infrahub/profiles/queries/__init__.py +0 -0
  145. infrahub/profiles/queries/get_profile_data.py +98 -0
  146. infrahub/profiles/tasks.py +63 -0
  147. infrahub/proposed_change/tasks.py +24 -5
  148. infrahub/repositories/__init__.py +0 -0
  149. infrahub/repositories/create_repository.py +113 -0
  150. infrahub/server.py +9 -1
  151. infrahub/services/__init__.py +8 -5
  152. infrahub/services/adapters/workflow/worker.py +5 -2
  153. infrahub/task_manager/event.py +5 -0
  154. infrahub/task_manager/models.py +7 -0
  155. infrahub/tasks/registry.py +6 -4
  156. infrahub/trigger/catalogue.py +4 -0
  157. infrahub/trigger/models.py +2 -0
  158. infrahub/trigger/setup.py +13 -4
  159. infrahub/trigger/tasks.py +6 -0
  160. infrahub/webhook/models.py +1 -1
  161. infrahub/workers/dependencies.py +3 -1
  162. infrahub/workers/infrahub_async.py +5 -1
  163. infrahub/workflows/catalogue.py +118 -3
  164. infrahub/workflows/initialization.py +21 -0
  165. infrahub/workflows/models.py +17 -2
  166. infrahub_sdk/branch.py +17 -8
  167. infrahub_sdk/checks.py +1 -1
  168. infrahub_sdk/client.py +376 -95
  169. infrahub_sdk/config.py +29 -2
  170. infrahub_sdk/convert_object_type.py +61 -0
  171. infrahub_sdk/ctl/branch.py +3 -0
  172. infrahub_sdk/ctl/check.py +2 -3
  173. infrahub_sdk/ctl/cli_commands.py +20 -12
  174. infrahub_sdk/ctl/config.py +8 -2
  175. infrahub_sdk/ctl/generator.py +6 -3
  176. infrahub_sdk/ctl/graphql.py +184 -0
  177. infrahub_sdk/ctl/repository.py +39 -1
  178. infrahub_sdk/ctl/schema.py +40 -10
  179. infrahub_sdk/ctl/task.py +110 -0
  180. infrahub_sdk/ctl/utils.py +4 -0
  181. infrahub_sdk/ctl/validate.py +5 -3
  182. infrahub_sdk/diff.py +4 -5
  183. infrahub_sdk/exceptions.py +2 -0
  184. infrahub_sdk/generator.py +7 -1
  185. infrahub_sdk/graphql/__init__.py +12 -0
  186. infrahub_sdk/graphql/constants.py +1 -0
  187. infrahub_sdk/graphql/plugin.py +85 -0
  188. infrahub_sdk/graphql/query.py +77 -0
  189. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  190. infrahub_sdk/graphql/utils.py +40 -0
  191. infrahub_sdk/node/attribute.py +2 -0
  192. infrahub_sdk/node/node.py +28 -20
  193. infrahub_sdk/node/relationship.py +1 -3
  194. infrahub_sdk/playback.py +1 -2
  195. infrahub_sdk/protocols.py +54 -6
  196. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  197. infrahub_sdk/pytest_plugin/utils.py +40 -0
  198. infrahub_sdk/repository.py +1 -2
  199. infrahub_sdk/schema/__init__.py +70 -4
  200. infrahub_sdk/schema/main.py +1 -0
  201. infrahub_sdk/schema/repository.py +8 -0
  202. infrahub_sdk/spec/models.py +7 -0
  203. infrahub_sdk/spec/object.py +54 -6
  204. infrahub_sdk/spec/processors/__init__.py +0 -0
  205. infrahub_sdk/spec/processors/data_processor.py +10 -0
  206. infrahub_sdk/spec/processors/factory.py +34 -0
  207. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  208. infrahub_sdk/spec/range_expansion.py +118 -0
  209. infrahub_sdk/task/models.py +6 -4
  210. infrahub_sdk/timestamp.py +18 -6
  211. infrahub_sdk/transforms.py +1 -1
  212. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
  213. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +221 -165
  214. infrahub_testcontainers/container.py +114 -2
  215. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  216. infrahub_testcontainers/docker-compose.test.yml +5 -0
  217. infrahub_testcontainers/models.py +2 -2
  218. infrahub_testcontainers/performance_test.py +4 -4
  219. infrahub/core/convert_object_type/conversion.py +0 -134
  220. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
  221. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
  222. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -16,10 +16,12 @@ from infrahub.core.node import Node
16
16
  from infrahub.core.schema import NodeSchema
17
17
  from infrahub.database import InfrahubDatabase, retry_db_transaction
18
18
  from infrahub.exceptions import NodeNotFoundError, ValidationError
19
- from infrahub.lock import InfrahubMultiLock, build_object_lock_name
19
+ from infrahub.lock import InfrahubMultiLock
20
20
  from infrahub.log import get_logger
21
21
 
22
- from .main import DeleteResult, InfrahubMutationMixin, InfrahubMutationOptions
22
+ from ...core.node.create import create_node
23
+ from ...core.node.lock_utils import build_object_lock_name, get_lock_names_on_object_mutation
24
+ from .main import DeleteResult, InfrahubMutationMixin, InfrahubMutationOptions, build_graphql_response
23
25
  from .node_getter.by_default_filter import MutationNodeGetterByDefaultFilter
24
26
 
25
27
  if TYPE_CHECKING:
@@ -106,11 +108,11 @@ class InfrahubIPAddressMutation(InfrahubMutationMixin, Mutation):
106
108
  super().__init_subclass_with_meta__(_meta=_meta, **options)
107
109
 
108
110
  @staticmethod
109
- def _get_lock_name(namespace_id: str, branch: Branch) -> str | None:
111
+ def _get_lock_names(namespace_id: str, branch: Branch) -> list[str]:
110
112
  if not branch.is_default:
111
113
  # Do not lock on other branches as reconciliation will be performed at least when merging in main branch.
112
- return None
113
- return build_object_lock_name(InfrahubKind.IPADDRESS + "_" + namespace_id)
114
+ return []
115
+ return [build_object_lock_name(InfrahubKind.IPADDRESS + "_" + namespace_id)]
114
116
 
115
117
  @classmethod
116
118
  async def _mutate_create_object_and_reconcile(
@@ -121,7 +123,13 @@ class InfrahubIPAddressMutation(InfrahubMutationMixin, Mutation):
121
123
  ip_address: IPv4Interface | ipaddress.IPv6Interface,
122
124
  namespace_id: str,
123
125
  ) -> Node:
124
- address = await cls.mutate_create_object(data=data, db=db, branch=branch)
126
+ address = await create_node(
127
+ data=dict(data),
128
+ db=db,
129
+ branch=branch,
130
+ schema=cls._meta.active_schema,
131
+ )
132
+
125
133
  reconciler = IpamReconciler(db=db, branch=branch)
126
134
  reconciled_address = await reconciler.reconcile(
127
135
  ip_value=ip_address, namespace=namespace_id, node_uuid=address.get_id()
@@ -142,19 +150,15 @@ class InfrahubIPAddressMutation(InfrahubMutationMixin, Mutation):
142
150
  ip_address = ipaddress.ip_interface(data["address"]["value"])
143
151
  namespace_id = await validate_namespace(db=db, branch=branch, data=data)
144
152
 
145
- async with db.start_transaction() as dbt:
146
- if lock_name := cls._get_lock_name(namespace_id, branch):
147
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
148
- reconciled_address = await cls._mutate_create_object_and_reconcile(
149
- data=data, branch=branch, db=dbt, ip_address=ip_address, namespace_id=namespace_id
150
- )
151
- else:
153
+ lock_names = cls._get_lock_names(namespace_id, branch)
154
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
155
+ async with db.start_transaction() as dbt:
152
156
  reconciled_address = await cls._mutate_create_object_and_reconcile(
153
157
  data=data, branch=branch, db=dbt, ip_address=ip_address, namespace_id=namespace_id
154
158
  )
155
- result = await cls.mutate_create_to_graphql(info=info, db=dbt, obj=reconciled_address)
159
+ graphql_response = await build_graphql_response(info=info, db=dbt, obj=reconciled_address)
156
160
 
157
- return reconciled_address, result
161
+ return reconciled_address, cls(**graphql_response)
158
162
 
159
163
  @classmethod
160
164
  async def _mutate_update_object_and_reconcile(
@@ -198,18 +202,28 @@ class InfrahubIPAddressMutation(InfrahubMutationMixin, Mutation):
198
202
  namespace = await address.ip_namespace.get_peer(db)
199
203
  namespace_id = await validate_namespace(db=db, branch=branch, data=data, existing_namespace_id=namespace.id)
200
204
 
201
- async with db.start_transaction() as dbt:
202
- if lock_name := cls._get_lock_name(namespace_id, branch):
203
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
205
+ # Prepare a clone to compute locks without triggering pool allocations
206
+ preview_obj = await NodeManager.get_one_by_id_or_default_filter(
207
+ db=db,
208
+ kind=address.get_kind(),
209
+ id=address.get_id(),
210
+ branch=branch,
211
+ )
212
+ await preview_obj.from_graphql(db=db, data=data, process_pools=False)
213
+
214
+ schema_branch = db.schema.get_schema_branch(name=branch.name)
215
+ lock_names = get_lock_names_on_object_mutation(node=preview_obj, schema_branch=schema_branch)
216
+
217
+ namespace_lock_names = cls._get_lock_names(namespace_id, branch)
218
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=namespace_lock_names):
219
+ # FIXME: do not lock when data does not contain uniqueness constraint fields or resource pool allocations
220
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names, metrics=False):
221
+ async with db.start_transaction() as dbt:
204
222
  reconciled_address = await cls._mutate_update_object_and_reconcile(
205
223
  info=info, data=data, branch=branch, address=address, namespace_id=namespace_id, db=dbt
206
224
  )
207
- else:
208
- reconciled_address = await cls._mutate_update_object_and_reconcile(
209
- info=info, data=data, branch=branch, address=address, namespace_id=namespace_id, db=dbt
210
- )
211
225
 
212
- result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=reconciled_address)
226
+ result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=reconciled_address)
213
227
 
214
228
  return address, result
215
229
 
@@ -261,11 +275,11 @@ class InfrahubIPPrefixMutation(InfrahubMutationMixin, Mutation):
261
275
  super().__init_subclass_with_meta__(_meta=_meta, **options)
262
276
 
263
277
  @staticmethod
264
- def _get_lock_name(namespace_id: str) -> str | None:
278
+ def _get_lock_names(namespace_id: str) -> list[str]:
265
279
  # IPPrefix has some cardinality-one relationships involved (parent/child/ip_address),
266
280
  # so we need to lock on any branch to avoid creating multiple peers for these relationships
267
281
  # during concurrent ipam reconciliations.
268
- return build_object_lock_name(InfrahubKind.IPPREFIX + "_" + namespace_id)
282
+ return [build_object_lock_name(InfrahubKind.IPPREFIX + "_" + namespace_id)]
269
283
 
270
284
  @classmethod
271
285
  async def _mutate_create_object_and_reconcile(
@@ -275,7 +289,12 @@ class InfrahubIPPrefixMutation(InfrahubMutationMixin, Mutation):
275
289
  db: InfrahubDatabase,
276
290
  namespace_id: str,
277
291
  ) -> Node:
278
- prefix = await cls.mutate_create_object(data=data, db=db, branch=branch)
292
+ prefix = await create_node(
293
+ data=dict(data),
294
+ db=db,
295
+ branch=branch,
296
+ schema=cls._meta.active_schema,
297
+ )
279
298
  return await cls._reconcile_prefix(
280
299
  branch=branch, db=db, prefix=prefix, namespace_id=namespace_id, is_delete=False
281
300
  )
@@ -293,16 +312,16 @@ class InfrahubIPPrefixMutation(InfrahubMutationMixin, Mutation):
293
312
  db = database or graphql_context.db
294
313
  namespace_id = await validate_namespace(db=db, branch=branch, data=data)
295
314
 
296
- async with db.start_transaction() as dbt:
297
- lock_name = cls._get_lock_name(namespace_id)
298
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
315
+ lock_names = cls._get_lock_names(namespace_id)
316
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
317
+ async with db.start_transaction() as dbt:
299
318
  reconciled_prefix = await cls._mutate_create_object_and_reconcile(
300
319
  data=data, branch=branch, db=dbt, namespace_id=namespace_id
301
320
  )
302
321
 
303
- result = await cls.mutate_create_to_graphql(info=info, db=dbt, obj=reconciled_prefix)
322
+ graphql_response = await build_graphql_response(info=info, db=dbt, obj=reconciled_prefix)
304
323
 
305
- return reconciled_prefix, result
324
+ return reconciled_prefix, cls(**graphql_response)
306
325
 
307
326
  @classmethod
308
327
  async def _mutate_update_object_and_reconcile(
@@ -343,13 +362,26 @@ class InfrahubIPPrefixMutation(InfrahubMutationMixin, Mutation):
343
362
  namespace = await prefix.ip_namespace.get_peer(db)
344
363
  namespace_id = await validate_namespace(db=db, branch=branch, data=data, existing_namespace_id=namespace.id)
345
364
 
346
- async with db.start_transaction() as dbt:
347
- lock_name = cls._get_lock_name(namespace_id)
348
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
349
- reconciled_prefix = await cls._mutate_update_object_and_reconcile(
350
- info=info, data=data, prefix=prefix, db=dbt, namespace_id=namespace_id, branch=branch
351
- )
352
- result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=reconciled_prefix)
365
+ # Prepare a clone to compute locks without triggering pool allocations
366
+ preview_obj = await NodeManager.get_one_by_id_or_default_filter(
367
+ db=db,
368
+ kind=prefix.get_kind(),
369
+ id=prefix.get_id(),
370
+ branch=branch,
371
+ )
372
+ await preview_obj.from_graphql(db=db, data=data, process_pools=False)
373
+
374
+ schema_branch = db.schema.get_schema_branch(name=branch.name)
375
+ lock_names = get_lock_names_on_object_mutation(node=preview_obj, schema_branch=schema_branch)
376
+
377
+ namespace_lock_names = cls._get_lock_names(namespace_id)
378
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=namespace_lock_names):
379
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names, metrics=False):
380
+ async with db.start_transaction() as dbt:
381
+ reconciled_prefix = await cls._mutate_update_object_and_reconcile(
382
+ info=info, data=data, prefix=prefix, db=dbt, namespace_id=namespace_id, branch=branch
383
+ )
384
+ result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=reconciled_prefix)
353
385
 
354
386
  return prefix, result
355
387
 
@@ -408,9 +440,9 @@ class InfrahubIPPrefixMutation(InfrahubMutationMixin, Mutation):
408
440
  namespace_rels = await prefix.ip_namespace.get_relationships(db=db)
409
441
  namespace_id = namespace_rels[0].peer_id
410
442
 
411
- async with graphql_context.db.start_transaction() as dbt:
412
- lock_name = cls._get_lock_name(namespace_id)
413
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
443
+ lock_names = cls._get_lock_names(namespace_id)
444
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
445
+ async with graphql_context.db.start_transaction() as dbt:
414
446
  reconciled_prefix = await cls._reconcile_prefix(
415
447
  branch=branch, db=dbt, prefix=prefix, namespace_id=namespace_id, is_delete=True
416
448
  )
@@ -1,22 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- import hashlib
4
3
  from dataclasses import dataclass, field
5
4
  from typing import TYPE_CHECKING, Any
6
5
 
7
6
  from graphene import InputObjectType, Mutation
8
7
  from graphene.types.mutation import MutationOptions
8
+ from infrahub_sdk.utils import extract_fields_first_node
9
9
  from typing_extensions import Self
10
10
 
11
11
  from infrahub import config, lock
12
- from infrahub.core.constants import InfrahubKind, MutationAction
12
+ from infrahub.core.constants import MutationAction
13
13
  from infrahub.core.constraint.node.runner import NodeConstraintRunner
14
14
  from infrahub.core.manager import NodeManager
15
- from infrahub.core.node.create import (
16
- create_node,
17
- get_profile_ids,
18
- refresh_for_profile_update,
19
- )
15
+ from infrahub.core.node.create import create_node, get_profile_ids
20
16
  from infrahub.core.schema import MainSchemaTypes, NodeSchema
21
17
  from infrahub.core.schema.generic_schema import GenericSchema
22
18
  from infrahub.core.schema.profile_schema import ProfileSchema
@@ -28,9 +24,11 @@ from infrahub.events.generator import generate_node_mutation_events
28
24
  from infrahub.exceptions import HFIDViolatedError, InitializationError, NodeNotFoundError
29
25
  from infrahub.graphql.context import apply_external_context
30
26
  from infrahub.graphql.field_extractor import extract_graphql_fields
31
- from infrahub.lock import InfrahubMultiLock, build_object_lock_name
27
+ from infrahub.lock import InfrahubMultiLock
32
28
  from infrahub.log import get_log_data, get_logger
29
+ from infrahub.profiles.node_applier import NodeProfilesApplier
33
30
 
31
+ from ...core.node.lock_utils import get_lock_names_on_object_mutation
34
32
  from .node_getter.by_default_filter import MutationNodeGetterByDefaultFilter
35
33
 
36
34
  if TYPE_CHECKING:
@@ -38,7 +36,6 @@ if TYPE_CHECKING:
38
36
 
39
37
  from infrahub.core.branch import Branch
40
38
  from infrahub.core.node import Node
41
- from infrahub.core.schema.schema_branch import SchemaBranch
42
39
  from infrahub.database import InfrahubDatabase
43
40
  from infrahub.graphql.types.context import ContextInput
44
41
 
@@ -47,8 +44,6 @@ if TYPE_CHECKING:
47
44
 
48
45
  log = get_logger()
49
46
 
50
- KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED = [InfrahubKind.GENERICGROUP]
51
-
52
47
 
53
48
  @dataclass
54
49
  class DeleteResult:
@@ -146,23 +141,6 @@ class InfrahubMutationMixin:
146
141
 
147
142
  return mutation
148
143
 
149
- @classmethod
150
- async def _call_mutate_create_object(
151
- cls, data: InputObjectType, db: InfrahubDatabase, branch: Branch, override_data: dict[str, Any] | None = None
152
- ) -> Node:
153
- """
154
- Wrapper around mutate_create_object to potentially activate locking.
155
- """
156
- schema_branch = db.schema.get_schema_branch(name=branch.name)
157
- lock_names = _get_kind_lock_names_on_object_mutation(
158
- kind=cls._meta.active_schema.kind, branch=branch, schema_branch=schema_branch, data=data
159
- )
160
- if lock_names:
161
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
162
- return await cls.mutate_create_object(data=data, db=db, branch=branch, override_data=override_data)
163
-
164
- return await cls.mutate_create_object(data=data, db=db, branch=branch, override_data=override_data)
165
-
166
144
  @classmethod
167
145
  async def mutate_create(
168
146
  cls,
@@ -172,40 +150,21 @@ class InfrahubMutationMixin:
172
150
  database: InfrahubDatabase | None = None,
173
151
  override_data: dict[str, Any] | None = None,
174
152
  ) -> tuple[Node, Self]:
175
- graphql_context: GraphqlContext = info.context
176
- db = database or graphql_context.db
177
- obj = await cls._call_mutate_create_object(data=data, db=db, branch=branch, override_data=override_data)
178
- result = await cls.mutate_create_to_graphql(info=info, db=db, obj=obj)
179
- return obj, result
180
-
181
- @classmethod
182
- @retry_db_transaction(name="object_create")
183
- async def mutate_create_object(
184
- cls,
185
- data: InputObjectType,
186
- db: InfrahubDatabase,
187
- branch: Branch,
188
- override_data: dict[str, Any] | None = None,
189
- ) -> Node:
153
+ db = database or info.context.db
190
154
  schema = cls._meta.active_schema
191
- if isinstance(schema, GenericSchema):
192
- raise ValueError(f"Node of generic schema `{schema.name=}` can not be instantiated.")
155
+
193
156
  create_data = dict(data)
194
157
  create_data.update(override_data or {})
195
- return await create_node(
158
+
159
+ obj = await create_node(
196
160
  data=create_data,
197
161
  db=db,
198
162
  branch=branch,
199
163
  schema=schema,
200
164
  )
201
165
 
202
- @classmethod
203
- async def mutate_create_to_graphql(cls, info: GraphQLResolveInfo, db: InfrahubDatabase, obj: Node) -> Self:
204
- fields = extract_graphql_fields(info=info)
205
- result: dict[str, Any] = {"ok": True}
206
- if "object" in fields:
207
- result["object"] = await obj.to_graphql(db=db, fields=fields.get("object", {}))
208
- return cls(**result)
166
+ graphql_response = await build_graphql_response(info=info, db=db, obj=obj)
167
+ return obj, cls(**graphql_response)
209
168
 
210
169
  @classmethod
211
170
  async def _call_mutate_update(
@@ -221,41 +180,40 @@ class InfrahubMutationMixin:
221
180
  Wrapper around mutate_update to potentially activate locking and call it within a database transaction.
222
181
  """
223
182
 
224
- schema_branch = db.schema.get_schema_branch(name=branch.name)
225
- lock_names = _get_kind_lock_names_on_object_mutation(
226
- kind=cls._meta.active_schema.kind, branch=branch, schema_branch=schema_branch, data=data
183
+ # Prepare a clone to compute locks without triggering pool allocations
184
+ preview_obj = await NodeManager.get_one_by_id_or_default_filter(
185
+ db=db,
186
+ kind=obj.get_kind(),
187
+ id=obj.get_id(),
188
+ branch=branch,
227
189
  )
190
+ await preview_obj.from_graphql(db=db, data=data, process_pools=False)
228
191
 
229
- if db.is_transaction:
230
- if lock_names:
231
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
232
- obj = await cls.mutate_update_object(
233
- db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
234
- )
235
- else:
192
+ schema_branch = db.schema.get_schema_branch(name=branch.name)
193
+ lock_names = get_lock_names_on_object_mutation(node=preview_obj, schema_branch=schema_branch)
194
+
195
+ # FIXME: do not lock when data does not contain uniqueness constraint fields or resource pool allocations
196
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names, metrics=False):
197
+ if db.is_transaction:
236
198
  obj = await cls.mutate_update_object(
237
199
  db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
238
200
  )
239
- result = await cls.mutate_update_to_graphql(db=db, info=info, obj=obj)
240
- return obj, result
241
201
 
242
- async with db.start_transaction() as dbt:
243
- if lock_names:
244
- async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
245
- obj = await cls.mutate_update_object(
246
- db=dbt,
247
- info=info,
248
- data=data,
249
- branch=branch,
250
- obj=obj,
251
- skip_uniqueness_check=skip_uniqueness_check,
252
- )
253
- else:
202
+ result = await cls.mutate_update_to_graphql(db=db, info=info, obj=obj)
203
+ return obj, result
204
+
205
+ async with db.start_transaction() as dbt:
254
206
  obj = await cls.mutate_update_object(
255
- db=dbt, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
207
+ db=dbt,
208
+ info=info,
209
+ data=data,
210
+ branch=branch,
211
+ obj=obj,
212
+ skip_uniqueness_check=skip_uniqueness_check,
256
213
  )
257
- result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=obj)
258
- return obj, result
214
+
215
+ result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=obj)
216
+ return obj, result
259
217
 
260
218
  @classmethod
261
219
  @retry_db_transaction(name="object_update")
@@ -290,7 +248,6 @@ class InfrahubMutationMixin:
290
248
  component_registry = get_component_registry()
291
249
  node_constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
292
250
 
293
- before_mutate_profile_ids = await get_profile_ids(db=db, obj=obj)
294
251
  await obj.from_graphql(db=db, data=data)
295
252
  fields_to_validate = list(data)
296
253
  await node_constraint_runner.check(
@@ -302,15 +259,13 @@ class InfrahubMutationMixin:
302
259
  if field_to_remove in fields:
303
260
  fields.remove(field_to_remove)
304
261
 
262
+ after_mutate_profile_ids = await get_profile_ids(db=db, obj=obj)
263
+ if after_mutate_profile_ids or (not after_mutate_profile_ids and obj.uses_profiles()):
264
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
265
+ updated_field_names = await node_profiles_applier.apply_profiles(node=obj)
266
+ fields += updated_field_names
305
267
  await obj.save(db=db, fields=fields)
306
268
 
307
- obj = await refresh_for_profile_update(
308
- db=db,
309
- branch=branch,
310
- obj=obj,
311
- previous_profile_ids=before_mutate_profile_ids,
312
- schema=cls._meta.active_schema,
313
- )
314
269
  return obj
315
270
 
316
271
  @classmethod
@@ -422,6 +377,15 @@ class InfrahubMutationMixin:
422
377
  )
423
378
  return updated_obj, mutation, False
424
379
 
380
+ @classmethod
381
+ async def _delete_obj(cls, graphql_context: GraphqlContext, branch: Branch, obj: Node) -> list[Node]:
382
+ db = graphql_context.db
383
+ async with db.start_transaction() as dbt:
384
+ deleted = await NodeManager.delete(db=dbt, branch=branch, nodes=[obj])
385
+ deleted_str = ", ".join([f"{d.get_kind()}({d.get_id()})" for d in deleted])
386
+ log.info(f"nodes deleted: {deleted_str}")
387
+ return deleted
388
+
425
389
  @classmethod
426
390
  @retry_db_transaction(name="object_delete")
427
391
  async def mutate_delete(
@@ -440,11 +404,7 @@ class InfrahubMutationMixin:
440
404
  branch=branch,
441
405
  )
442
406
 
443
- async with graphql_context.db.start_transaction() as db:
444
- deleted = await NodeManager.delete(db=db, branch=branch, nodes=[obj])
445
-
446
- deleted_str = ", ".join([f"{d.get_kind()}({d.get_id()})" for d in deleted])
447
- log.info(f"nodes deleted: {deleted_str}")
407
+ deleted = await cls._delete_obj(graphql_context=graphql_context, branch=branch, obj=obj)
448
408
 
449
409
  ok = True
450
410
 
@@ -471,90 +431,13 @@ class InfrahubMutation(InfrahubMutationMixin, Mutation):
471
431
  super().__init_subclass_with_meta__(_meta=_meta, **options)
472
432
 
473
433
 
474
- def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch) -> list[str]:
475
- """
476
- Return kinds for which we want to lock during creating / updating an object of a given schema node.
477
- Lock should be performed on schema kind and its generics having a uniqueness_constraint defined.
478
- If a generic uniqueness constraint is the same as the node schema one,
479
- it means node schema overrided this constraint, in which case we only need to lock on the generic.
480
- """
481
-
482
- node_schema = schema_branch.get(name=kind, duplicate=False)
483
-
484
- schema_uc = None
485
- kinds = []
486
- if node_schema.uniqueness_constraints:
487
- kinds.append(node_schema.kind)
488
- schema_uc = node_schema.uniqueness_constraints
489
-
490
- if node_schema.is_generic_schema:
491
- return kinds
492
-
493
- generics_kinds = node_schema.inherit_from
494
-
495
- node_schema_kind_removed = False
496
- for generic_kind in generics_kinds:
497
- generic_uc = schema_branch.get(name=generic_kind, duplicate=False).uniqueness_constraints
498
- if generic_uc:
499
- kinds.append(generic_kind)
500
- if not node_schema_kind_removed and generic_uc == schema_uc:
501
- # Check whether we should remove original schema kind as it simply overrides uniqueness_constraint
502
- # of a generic
503
- kinds.pop(0)
504
- node_schema_kind_removed = True
505
- return kinds
506
-
507
-
508
- def _should_kind_be_locked_on_any_branch(kind: str, schema_branch: SchemaBranch) -> bool:
509
- """
510
- Check whether kind or any kind generic is in KINDS_TO_LOCK_ON_ANY_BRANCH.
511
- """
512
-
513
- if kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
514
- return True
515
-
516
- node_schema = schema_branch.get(name=kind, duplicate=False)
517
- if node_schema.is_generic_schema:
518
- return False
519
-
520
- for generic_kind in node_schema.inherit_from:
521
- if generic_kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
522
- return True
523
- return False
524
-
525
-
526
- def _hash(value: str) -> str:
527
- # Do not use builtin `hash` for lock names as due to randomization results would differ between
528
- # different processes.
529
- return hashlib.sha256(value.encode()).hexdigest()
530
-
531
-
532
- def _get_kind_lock_names_on_object_mutation(
533
- kind: str, branch: Branch, schema_branch: SchemaBranch, data: InputObjectType
534
- ) -> list[str]:
535
- """
536
- Return objects kind for which we want to avoid concurrent mutation (create/update). Except for some specific kinds,
537
- concurrent mutations are only allowed on non-main branch as objects validations will be performed at least when merging in main branch.
538
- """
539
-
540
- if not branch.is_default and not _should_kind_be_locked_on_any_branch(kind=kind, schema_branch=schema_branch):
541
- return []
542
-
543
- if kind == InfrahubKind.GRAPHQLQUERYGROUP:
544
- # Lock on name as well to improve performances
545
- try:
546
- name = data.name.value
547
- return [build_object_lock_name(kind + "." + _hash(name))]
548
- except AttributeError:
549
- # We might reach here if we are updating a CoreGraphQLQueryGroup without updating the name,
550
- # in which case we would not need to lock. This is not supposed to happen as current `update`
551
- # logic first fetches the node with its name.
552
- return []
553
-
554
- lock_kinds = _get_kinds_to_lock_on_object_mutation(kind, schema_branch)
555
- lock_names = [build_object_lock_name(kind) for kind in lock_kinds]
556
- return lock_names
557
-
558
-
559
434
  def _get_data_fields(data: InputObjectType) -> list[str]:
560
435
  return [field for field in data.keys() if field not in ["id", "hfid"]]
436
+
437
+
438
+ async def build_graphql_response(info: GraphQLResolveInfo, db: InfrahubDatabase, obj: Node) -> dict:
439
+ fields = await extract_fields_first_node(info)
440
+ result: dict[str, Any] = {"ok": True}
441
+ if "object" in fields:
442
+ result["object"] = await obj.to_graphql(db=db, fields=fields.get("object", {}))
443
+ return result