infrahub-server 1.4.10__py3-none-any.whl → 1.5.0b1__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 (178) 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/query.py +2 -0
  5. infrahub/api/schema.py +3 -0
  6. infrahub/auth.py +5 -5
  7. infrahub/cli/db.py +26 -2
  8. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  9. infrahub/config.py +7 -2
  10. infrahub/core/attribute.py +25 -22
  11. infrahub/core/branch/models.py +2 -2
  12. infrahub/core/branch/needs_rebase_status.py +11 -0
  13. infrahub/core/branch/tasks.py +4 -3
  14. infrahub/core/changelog/models.py +4 -12
  15. infrahub/core/constants/__init__.py +1 -0
  16. infrahub/core/constants/infrahubkind.py +1 -0
  17. infrahub/core/convert_object_type/object_conversion.py +201 -0
  18. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  19. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  20. infrahub/core/diff/model/path.py +4 -0
  21. infrahub/core/diff/payload_builder.py +1 -1
  22. infrahub/core/diff/query/artifact.py +1 -1
  23. infrahub/core/graph/__init__.py +1 -1
  24. infrahub/core/initialization.py +2 -2
  25. infrahub/core/ipam/utilization.py +1 -1
  26. infrahub/core/manager.py +9 -84
  27. infrahub/core/migrations/graph/__init__.py +6 -0
  28. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  29. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
  30. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
  31. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  32. infrahub/core/migrations/shared.py +5 -6
  33. infrahub/core/node/__init__.py +165 -42
  34. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  35. infrahub/core/node/create.py +67 -35
  36. infrahub/core/node/lock_utils.py +98 -0
  37. infrahub/core/node/node_property_attribute.py +230 -0
  38. infrahub/core/node/standard.py +1 -1
  39. infrahub/core/property.py +11 -0
  40. infrahub/core/protocols.py +8 -1
  41. infrahub/core/query/attribute.py +27 -15
  42. infrahub/core/query/node.py +61 -185
  43. infrahub/core/query/relationship.py +43 -26
  44. infrahub/core/query/subquery.py +0 -8
  45. infrahub/core/registry.py +2 -2
  46. infrahub/core/relationship/constraints/count.py +1 -1
  47. infrahub/core/relationship/model.py +60 -20
  48. infrahub/core/schema/attribute_schema.py +0 -2
  49. infrahub/core/schema/basenode_schema.py +42 -2
  50. infrahub/core/schema/definitions/core/__init__.py +2 -0
  51. infrahub/core/schema/definitions/core/generator.py +2 -0
  52. infrahub/core/schema/definitions/core/group.py +16 -2
  53. infrahub/core/schema/definitions/core/repository.py +7 -0
  54. infrahub/core/schema/definitions/internal.py +14 -1
  55. infrahub/core/schema/generated/base_node_schema.py +6 -1
  56. infrahub/core/schema/node_schema.py +5 -2
  57. infrahub/core/schema/relationship_schema.py +0 -1
  58. infrahub/core/schema/schema_branch.py +137 -2
  59. infrahub/core/schema/schema_branch_display.py +123 -0
  60. infrahub/core/schema/schema_branch_hfid.py +114 -0
  61. infrahub/core/validators/aggregated_checker.py +1 -1
  62. infrahub/core/validators/determiner.py +12 -1
  63. infrahub/core/validators/relationship/peer.py +1 -1
  64. infrahub/core/validators/tasks.py +1 -1
  65. infrahub/display_labels/__init__.py +0 -0
  66. infrahub/display_labels/gather.py +48 -0
  67. infrahub/display_labels/models.py +240 -0
  68. infrahub/display_labels/tasks.py +186 -0
  69. infrahub/display_labels/triggers.py +22 -0
  70. infrahub/events/group_action.py +1 -1
  71. infrahub/events/node_action.py +1 -1
  72. infrahub/generators/constants.py +7 -0
  73. infrahub/generators/models.py +38 -12
  74. infrahub/generators/tasks.py +34 -16
  75. infrahub/git/base.py +38 -1
  76. infrahub/git/integrator.py +22 -14
  77. infrahub/graphql/analyzer.py +1 -1
  78. infrahub/graphql/api/dependencies.py +2 -4
  79. infrahub/graphql/api/endpoints.py +2 -2
  80. infrahub/graphql/app.py +2 -4
  81. infrahub/graphql/initialization.py +2 -3
  82. infrahub/graphql/manager.py +212 -137
  83. infrahub/graphql/middleware.py +12 -0
  84. infrahub/graphql/mutations/branch.py +11 -0
  85. infrahub/graphql/mutations/computed_attribute.py +110 -3
  86. infrahub/graphql/mutations/convert_object_type.py +34 -13
  87. infrahub/graphql/mutations/display_label.py +111 -0
  88. infrahub/graphql/mutations/generator.py +25 -7
  89. infrahub/graphql/mutations/hfid.py +118 -0
  90. infrahub/graphql/mutations/ipam.py +21 -8
  91. infrahub/graphql/mutations/main.py +37 -153
  92. infrahub/graphql/mutations/profile.py +195 -0
  93. infrahub/graphql/mutations/proposed_change.py +2 -1
  94. infrahub/graphql/mutations/relationship.py +2 -2
  95. infrahub/graphql/mutations/repository.py +22 -83
  96. infrahub/graphql/mutations/resource_manager.py +2 -2
  97. infrahub/graphql/mutations/schema.py +5 -5
  98. infrahub/graphql/mutations/webhook.py +1 -1
  99. infrahub/graphql/queries/resource_manager.py +1 -1
  100. infrahub/graphql/registry.py +173 -0
  101. infrahub/graphql/resolvers/resolver.py +2 -0
  102. infrahub/graphql/schema.py +8 -1
  103. infrahub/groups/tasks.py +1 -1
  104. infrahub/hfid/__init__.py +0 -0
  105. infrahub/hfid/gather.py +48 -0
  106. infrahub/hfid/models.py +240 -0
  107. infrahub/hfid/tasks.py +185 -0
  108. infrahub/hfid/triggers.py +22 -0
  109. infrahub/lock.py +67 -30
  110. infrahub/locks/__init__.py +0 -0
  111. infrahub/locks/tasks.py +37 -0
  112. infrahub/middleware.py +26 -1
  113. infrahub/patch/plan_writer.py +2 -2
  114. infrahub/profiles/__init__.py +0 -0
  115. infrahub/profiles/node_applier.py +101 -0
  116. infrahub/profiles/queries/__init__.py +0 -0
  117. infrahub/profiles/queries/get_profile_data.py +99 -0
  118. infrahub/profiles/tasks.py +63 -0
  119. infrahub/proposed_change/tasks.py +10 -1
  120. infrahub/repositories/__init__.py +0 -0
  121. infrahub/repositories/create_repository.py +113 -0
  122. infrahub/server.py +16 -3
  123. infrahub/services/__init__.py +8 -5
  124. infrahub/tasks/registry.py +6 -4
  125. infrahub/trigger/catalogue.py +4 -0
  126. infrahub/trigger/models.py +2 -0
  127. infrahub/trigger/tasks.py +3 -0
  128. infrahub/webhook/models.py +1 -1
  129. infrahub/workflows/catalogue.py +110 -3
  130. infrahub/workflows/initialization.py +16 -0
  131. infrahub/workflows/models.py +17 -2
  132. infrahub_sdk/branch.py +5 -8
  133. infrahub_sdk/checks.py +1 -1
  134. infrahub_sdk/client.py +364 -84
  135. infrahub_sdk/convert_object_type.py +61 -0
  136. infrahub_sdk/ctl/check.py +2 -3
  137. infrahub_sdk/ctl/cli_commands.py +18 -12
  138. infrahub_sdk/ctl/config.py +8 -2
  139. infrahub_sdk/ctl/generator.py +6 -3
  140. infrahub_sdk/ctl/graphql.py +184 -0
  141. infrahub_sdk/ctl/repository.py +39 -1
  142. infrahub_sdk/ctl/schema.py +18 -3
  143. infrahub_sdk/ctl/utils.py +4 -0
  144. infrahub_sdk/ctl/validate.py +5 -3
  145. infrahub_sdk/diff.py +4 -5
  146. infrahub_sdk/exceptions.py +2 -0
  147. infrahub_sdk/generator.py +7 -1
  148. infrahub_sdk/graphql/__init__.py +12 -0
  149. infrahub_sdk/graphql/constants.py +1 -0
  150. infrahub_sdk/graphql/plugin.py +85 -0
  151. infrahub_sdk/graphql/query.py +77 -0
  152. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  153. infrahub_sdk/graphql/utils.py +40 -0
  154. infrahub_sdk/node/attribute.py +2 -0
  155. infrahub_sdk/node/node.py +28 -20
  156. infrahub_sdk/playback.py +1 -2
  157. infrahub_sdk/protocols.py +54 -6
  158. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  159. infrahub_sdk/pytest_plugin/utils.py +40 -0
  160. infrahub_sdk/repository.py +1 -2
  161. infrahub_sdk/schema/__init__.py +38 -0
  162. infrahub_sdk/schema/main.py +1 -0
  163. infrahub_sdk/schema/repository.py +8 -0
  164. infrahub_sdk/spec/object.py +120 -7
  165. infrahub_sdk/spec/range_expansion.py +118 -0
  166. infrahub_sdk/timestamp.py +18 -6
  167. infrahub_sdk/transforms.py +1 -1
  168. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +9 -11
  169. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +177 -134
  170. infrahub_testcontainers/container.py +1 -1
  171. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  172. infrahub_testcontainers/docker-compose.test.yml +1 -1
  173. infrahub_testcontainers/models.py +2 -2
  174. infrahub_testcontainers/performance_test.py +4 -4
  175. infrahub/core/convert_object_type/conversion.py +0 -134
  176. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
  177. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
  178. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
@@ -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_kind_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(
@@ -222,8 +181,8 @@ class InfrahubMutationMixin:
222
181
  """
223
182
 
224
183
  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
184
+ lock_names = get_kind_lock_names_on_object_mutation(
185
+ kind=cls._meta.active_schema.kind, branch=branch, schema_branch=schema_branch, data=dict(data)
227
186
  )
228
187
 
229
188
  if db.is_transaction:
@@ -290,7 +249,6 @@ class InfrahubMutationMixin:
290
249
  component_registry = get_component_registry()
291
250
  node_constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
292
251
 
293
- before_mutate_profile_ids = await get_profile_ids(db=db, obj=obj)
294
252
  await obj.from_graphql(db=db, data=data)
295
253
  fields_to_validate = list(data)
296
254
  await node_constraint_runner.check(
@@ -302,15 +260,13 @@ class InfrahubMutationMixin:
302
260
  if field_to_remove in fields:
303
261
  fields.remove(field_to_remove)
304
262
 
263
+ after_mutate_profile_ids = await get_profile_ids(db=db, obj=obj)
264
+ if after_mutate_profile_ids or (not after_mutate_profile_ids and obj.uses_profiles()):
265
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
266
+ updated_field_names = await node_profiles_applier.apply_profiles(node=obj)
267
+ fields += updated_field_names
305
268
  await obj.save(db=db, fields=fields)
306
269
 
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
270
  return obj
315
271
 
316
272
  @classmethod
@@ -422,6 +378,15 @@ class InfrahubMutationMixin:
422
378
  )
423
379
  return updated_obj, mutation, False
424
380
 
381
+ @classmethod
382
+ async def _delete_obj(cls, graphql_context: GraphqlContext, branch: Branch, obj: Node) -> list[Node]:
383
+ db = graphql_context.db
384
+ async with db.start_transaction() as dbt:
385
+ deleted = await NodeManager.delete(db=dbt, branch=branch, nodes=[obj])
386
+ deleted_str = ", ".join([f"{d.get_kind()}({d.get_id()})" for d in deleted])
387
+ log.info(f"nodes deleted: {deleted_str}")
388
+ return deleted
389
+
425
390
  @classmethod
426
391
  @retry_db_transaction(name="object_delete")
427
392
  async def mutate_delete(
@@ -440,11 +405,7 @@ class InfrahubMutationMixin:
440
405
  branch=branch,
441
406
  )
442
407
 
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}")
408
+ deleted = await cls._delete_obj(graphql_context=graphql_context, branch=branch, obj=obj)
448
409
 
449
410
  ok = True
450
411
 
@@ -471,90 +432,13 @@ class InfrahubMutation(InfrahubMutationMixin, Mutation):
471
432
  super().__init_subclass_with_meta__(_meta=_meta, **options)
472
433
 
473
434
 
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)
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).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)
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
435
  def _get_data_fields(data: InputObjectType) -> list[str]:
560
436
  return [field for field in data.keys() if field not in ["id", "hfid"]]
437
+
438
+
439
+ async def build_graphql_response(info: GraphQLResolveInfo, db: InfrahubDatabase, obj: Node) -> dict:
440
+ fields = await extract_fields_first_node(info)
441
+ result: dict[str, Any] = {"ok": True}
442
+ if "object" in fields:
443
+ result["object"] = await obj.to_graphql(db=db, fields=fields.get("object", {}))
444
+ return result
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from graphene import Boolean, InputObjectType, Mutation, String
6
+ from graphql import GraphQLResolveInfo
7
+ from opentelemetry import trace
8
+ from typing_extensions import Self
9
+
10
+ from infrahub.core.manager import NodeManager
11
+ from infrahub.core.schema import ProfileSchema
12
+ from infrahub.graphql.types.context import ContextInput
13
+ from infrahub.log import get_logger
14
+ from infrahub.profiles.node_applier import NodeProfilesApplier
15
+ from infrahub.workflows.catalogue import PROFILE_REFRESH_MULTIPLE
16
+
17
+ from .main import InfrahubMutationMixin, InfrahubMutationOptions
18
+
19
+ if TYPE_CHECKING:
20
+ from graphql import GraphQLResolveInfo
21
+
22
+ from infrahub.core.branch import Branch
23
+ from infrahub.core.node import Node
24
+ from infrahub.database import InfrahubDatabase
25
+ from infrahub.graphql.initialization import GraphqlContext
26
+ from infrahub.services.adapters.workflow import InfrahubWorkflow
27
+
28
+ log = get_logger()
29
+
30
+
31
+ class InfrahubProfileMutation(InfrahubMutationMixin, Mutation):
32
+ @classmethod
33
+ def __init_subclass_with_meta__(
34
+ cls,
35
+ schema: ProfileSchema,
36
+ _meta: InfrahubMutationOptions | None = None,
37
+ **options: dict[str, Any],
38
+ ) -> None:
39
+ # Make sure schema is a valid NodeSchema Node Class
40
+ if not isinstance(schema, ProfileSchema):
41
+ raise ValueError(f"You need to pass a valid ProfileSchema in '{cls.__name__}.Meta', received '{schema}'")
42
+
43
+ if not _meta:
44
+ _meta = InfrahubMutationOptions(cls)
45
+ _meta.schema = schema
46
+
47
+ super().__init_subclass_with_meta__(_meta=_meta, **options)
48
+
49
+ @classmethod
50
+ async def _send_profile_refresh_workflows(
51
+ cls,
52
+ db: InfrahubDatabase,
53
+ workflow_service: InfrahubWorkflow,
54
+ branch_name: str,
55
+ obj: Node,
56
+ node_ids: list[str] | None = None,
57
+ ) -> None:
58
+ if not node_ids:
59
+ related_nodes = await obj.related_nodes.get_relationships(db=db) # type: ignore[attr-defined]
60
+ node_ids = [rel.peer_id for rel in related_nodes]
61
+ if node_ids:
62
+ await workflow_service.submit_workflow(
63
+ workflow=PROFILE_REFRESH_MULTIPLE,
64
+ parameters={
65
+ "branch_name": branch_name,
66
+ "node_ids": node_ids,
67
+ },
68
+ )
69
+
70
+ @classmethod
71
+ def _get_profile_attr_values_map(cls, obj: Node) -> dict[str, Any]:
72
+ attr_values_map = {}
73
+ for attr_schema in obj.get_schema().attributes:
74
+ # profile name update can be ignored
75
+ if attr_schema.name == "profile_name":
76
+ continue
77
+ attr_values_map[attr_schema.name] = getattr(obj, attr_schema.name).value
78
+ return attr_values_map
79
+
80
+ @classmethod
81
+ async def _get_profile_related_node_ids(cls, db: InfrahubDatabase, obj: Node) -> set[str]:
82
+ related_nodes = await obj.related_nodes.get_relationships(db=db) # type: ignore[attr-defined]
83
+ if related_nodes:
84
+ related_node_ids = {rel.peer_id for rel in related_nodes}
85
+ else:
86
+ related_node_ids = set()
87
+ return related_node_ids
88
+
89
+ @classmethod
90
+ async def mutate_create(
91
+ cls,
92
+ info: GraphQLResolveInfo,
93
+ data: InputObjectType,
94
+ branch: Branch,
95
+ database: InfrahubDatabase | None = None,
96
+ override_data: dict[str, Any] | None = None,
97
+ ) -> tuple[Node, Self]:
98
+ graphql_context: GraphqlContext = info.context
99
+ db = database or graphql_context.db
100
+ workflow_service = graphql_context.active_service.workflow
101
+
102
+ obj, mutation = await super().mutate_create(
103
+ info=info, data=data, branch=branch, database=database, override_data=override_data
104
+ )
105
+ await cls._send_profile_refresh_workflows(
106
+ db=db, workflow_service=workflow_service, branch_name=branch.name, obj=obj
107
+ )
108
+
109
+ return obj, mutation
110
+
111
+ @classmethod
112
+ async def _call_mutate_update(
113
+ cls,
114
+ info: GraphQLResolveInfo,
115
+ data: InputObjectType,
116
+ branch: Branch,
117
+ db: InfrahubDatabase,
118
+ obj: Node,
119
+ skip_uniqueness_check: bool = False,
120
+ ) -> tuple[Node, Self]:
121
+ workflow_service = info.context.active_service.workflow
122
+ original_attr_values = cls._get_profile_attr_values_map(obj=obj)
123
+ original_related_node_ids = await cls._get_profile_related_node_ids(db=db, obj=obj)
124
+
125
+ obj, mutation = await super()._call_mutate_update(
126
+ info=info, data=data, branch=branch, db=db, obj=obj, skip_uniqueness_check=skip_uniqueness_check
127
+ )
128
+
129
+ updated_attr_values = cls._get_profile_attr_values_map(obj=obj)
130
+ updated_related_node_ids = await cls._get_profile_related_node_ids(db=db, obj=obj)
131
+
132
+ if original_attr_values != updated_attr_values:
133
+ await cls._send_profile_refresh_workflows(
134
+ db=db, workflow_service=workflow_service, branch_name=branch.name, obj=obj
135
+ )
136
+ elif updated_related_node_ids != original_related_node_ids:
137
+ removed_node_ids = original_related_node_ids - updated_related_node_ids
138
+ added_node_ids = updated_related_node_ids - original_related_node_ids
139
+ await cls._send_profile_refresh_workflows(
140
+ db=db,
141
+ workflow_service=workflow_service,
142
+ branch_name=branch.name,
143
+ obj=obj,
144
+ node_ids=list(removed_node_ids) + list(added_node_ids),
145
+ )
146
+
147
+ return obj, mutation
148
+
149
+ @classmethod
150
+ async def _delete_obj(cls, graphql_context: GraphqlContext, branch: Branch, obj: Node) -> list[Node]:
151
+ db = graphql_context.db
152
+ workflow_service = graphql_context.active_service.workflow
153
+ related_node_ids = await cls._get_profile_related_node_ids(db=db, obj=obj)
154
+ deleted = await super()._delete_obj(graphql_context=graphql_context, branch=branch, obj=obj)
155
+ await cls._send_profile_refresh_workflows(
156
+ db=db, workflow_service=workflow_service, branch_name=branch.name, obj=obj, node_ids=list(related_node_ids)
157
+ )
158
+ return deleted
159
+
160
+
161
+ class ProfilesRefreshInput(InputObjectType):
162
+ id = String(required=False)
163
+
164
+
165
+ class InfrahubProfilesRefresh(Mutation):
166
+ class Arguments:
167
+ data = ProfilesRefreshInput(required=True)
168
+ context = ContextInput(required=False)
169
+
170
+ ok = Boolean()
171
+
172
+ @classmethod
173
+ @trace.get_tracer(__name__).start_as_current_span("profiles_refresh")
174
+ async def mutate(
175
+ cls,
176
+ root: dict, # noqa: ARG003
177
+ info: GraphQLResolveInfo,
178
+ data: ProfilesRefreshInput,
179
+ context: ContextInput | None = None, # noqa: ARG003
180
+ ) -> Self:
181
+ graphql_context: GraphqlContext = info.context
182
+ db = graphql_context.db
183
+ branch = graphql_context.branch
184
+ obj = await NodeManager.get_one(
185
+ db=db,
186
+ branch=branch,
187
+ id=str(data.id),
188
+ include_source=True,
189
+ )
190
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
191
+ updated_fields = await node_profiles_applier.apply_profiles(node=obj)
192
+ if updated_fields:
193
+ await obj.save(db=db, fields=updated_fields)
194
+
195
+ return cls(ok=True)
@@ -29,13 +29,14 @@ from infrahub.exceptions import BranchNotFoundError, PermissionDeniedError, Vali
29
29
  from infrahub.graphql.mutations.main import InfrahubMutationMixin
30
30
  from infrahub.graphql.types.enums import CheckType as GraphQLCheckType
31
31
  from infrahub.graphql.types.task import TaskInfo
32
- from infrahub.lock import InfrahubLock, build_object_lock_name
32
+ from infrahub.lock import InfrahubLock
33
33
  from infrahub.proposed_change.approval_revoker import do_revoke_approvals_on_updated_pcs
34
34
  from infrahub.proposed_change.constants import ProposedChangeApprovalDecision, ProposedChangeState
35
35
  from infrahub.proposed_change.models import RequestProposedChangePipeline
36
36
  from infrahub.workers.dependencies import get_event_service
37
37
  from infrahub.workflows.catalogue import PROPOSED_CHANGE_MERGE, REQUEST_PROPOSED_CHANGE_PIPELINE
38
38
 
39
+ from ...core.node.lock_utils import build_object_lock_name
39
40
  from .main import InfrahubMutationOptions
40
41
 
41
42
  if TYPE_CHECKING:
@@ -91,7 +91,7 @@ class RelationshipAdd(Mutation):
91
91
  await apply_external_context(graphql_context=graphql_context, context_input=context)
92
92
 
93
93
  rel_schema = source.get_schema().get_relationship(name=relationship_name)
94
- display_label: str = await source.render_display_label(db=graphql_context.db)
94
+ display_label: str = await source.get_display_label(db=graphql_context.db) or ""
95
95
  node_changelog = NodeChangelog(
96
96
  node_id=source.get_id(), node_kind=source.get_kind(), display_label=display_label
97
97
  )
@@ -214,7 +214,7 @@ class RelationshipRemove(Mutation):
214
214
  await apply_external_context(graphql_context=graphql_context, context_input=context)
215
215
 
216
216
  rel_schema = source.get_schema().get_relationship(name=relationship_name)
217
- display_label: str = await source.render_display_label(db=graphql_context.db)
217
+ display_label: str = await source.get_display_label(db=graphql_context.db) or ""
218
218
  node_changelog = NodeChangelog(
219
219
  node_id=source.get_id(), node_kind=source.get_kind(), display_label=display_label
220
220
  )
@@ -7,14 +7,11 @@ import httpx
7
7
  from graphene import Boolean, Field, InputObjectType, Mutation, String
8
8
 
9
9
  from infrahub import config
10
- from infrahub.core.constants import InfrahubKind, RepositoryInternalStatus
10
+ from infrahub.core.constants import InfrahubKind
11
11
  from infrahub.core.manager import NodeManager
12
- from infrahub.core.protocols import CoreGenericRepository, CoreReadOnlyRepository, CoreRepository
12
+ from infrahub.core.protocols import CoreReadOnlyRepository, CoreRepository
13
13
  from infrahub.core.schema import NodeSchema
14
- from infrahub.exceptions import ValidationError
15
14
  from infrahub.git.models import (
16
- GitRepositoryAdd,
17
- GitRepositoryAddReadOnly,
18
15
  GitRepositoryImportObjects,
19
16
  GitRepositoryPullReadOnly,
20
17
  )
@@ -22,15 +19,15 @@ from infrahub.graphql.types.common import IdentifierInput
22
19
  from infrahub.log import get_logger
23
20
  from infrahub.message_bus import messages
24
21
  from infrahub.message_bus.messages.git_repository_connectivity import GitRepositoryConnectivityResponse
22
+ from infrahub.repositories.create_repository import RepositoryFinalizer
25
23
  from infrahub.workflows.catalogue import (
26
24
  GIT_REPOSITORIES_IMPORT_OBJECTS,
27
25
  GIT_REPOSITORIES_PULL_READ_ONLY,
28
- GIT_REPOSITORY_ADD,
29
- GIT_REPOSITORY_ADD_READ_ONLY,
30
26
  )
31
27
 
28
+ from ...core.node.create import create_node
32
29
  from ..types.task import TaskInfo
33
- from .main import InfrahubMutationMixin, InfrahubMutationOptions
30
+ from .main import InfrahubMutationMixin, InfrahubMutationOptions, build_graphql_response
34
31
 
35
32
  if TYPE_CHECKING:
36
33
  from graphql import GraphQLResolveInfo
@@ -63,86 +60,28 @@ class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
63
60
  info: GraphQLResolveInfo,
64
61
  data: InputObjectType,
65
62
  branch: Branch,
66
- database: InfrahubDatabase | None = None, # noqa: ARG003
63
+ database: InfrahubDatabase | None = None,
67
64
  override_data: dict[str, Any] | None = None,
68
65
  ) -> tuple[Node, Self]:
69
66
  graphql_context: GraphqlContext = info.context
70
-
71
67
  cleanup_payload(data)
72
- # Create the object in the database
73
- obj, result = await super().mutate_create(info, data, branch, override_data=override_data)
74
- obj = cast(CoreGenericRepository, obj)
75
-
76
- # First check the connectivity to the remote repository
77
- # If the connectivity is not good, we remove the repository to allow the user to add a new one
78
- if graphql_context.service:
79
- message = messages.GitRepositoryConnectivity(
80
- repository_name=obj.name.value,
81
- repository_location=obj.location.value,
82
- )
83
- response = await graphql_context.service.message_bus.rpc(
84
- message=message, response_class=GitRepositoryConnectivityResponse
85
- )
86
-
87
- if response.data.success is False:
88
- await obj.delete(db=graphql_context.db)
89
- raise ValidationError(response.data.message)
90
-
91
- # If we are in the default branch, we set the sync status to Active
92
- # If we are in another branch, we set the sync status to Staging
93
- if branch.is_default:
94
- obj.internal_status.value = RepositoryInternalStatus.ACTIVE.value
95
- else:
96
- obj.internal_status.value = RepositoryInternalStatus.STAGING.value
97
- await obj.save(db=graphql_context.db)
98
-
99
- # Create the new repository in the filesystem.
100
- log.info("create_repository", name=obj.name.value)
101
- authenticated_user = None
102
- if graphql_context.account_session and graphql_context.account_session.authenticated:
103
- authenticated_user = graphql_context.account_session.account_id
104
- if obj.get_kind() == InfrahubKind.READONLYREPOSITORY:
105
- obj = cast(CoreReadOnlyRepository, obj)
106
- model = GitRepositoryAddReadOnly(
107
- repository_id=obj.id,
108
- repository_name=obj.name.value,
109
- location=obj.location.value,
110
- ref=obj.ref.value,
111
- infrahub_branch_name=branch.name,
112
- infrahub_branch_id=str(branch.get_uuid()),
113
- internal_status=obj.internal_status.value,
114
- created_by=authenticated_user,
115
- )
116
- if graphql_context.service:
117
- await graphql_context.service.workflow.submit_workflow(
118
- workflow=GIT_REPOSITORY_ADD_READ_ONLY,
119
- context=graphql_context.get_context(),
120
- parameters={"model": model},
121
- )
122
-
123
- else:
124
- obj = cast(CoreRepository, obj)
125
- git_repo_add_model = GitRepositoryAdd(
126
- repository_id=obj.id,
127
- repository_name=obj.name.value,
128
- location=obj.location.value,
129
- default_branch_name=obj.default_branch.value,
130
- infrahub_branch_name=branch.name,
131
- infrahub_branch_id=str(branch.get_uuid()),
132
- internal_status=obj.internal_status.value,
133
- created_by=authenticated_user,
134
- )
135
-
136
- if graphql_context.service:
137
- await graphql_context.service.workflow.submit_workflow(
138
- workflow=GIT_REPOSITORY_ADD,
139
- context=graphql_context.get_context(),
140
- parameters={"model": git_repo_add_model},
141
- )
142
-
143
- # TODO Validate that the creation of the repository went as expected
68
+ db = database or graphql_context.db
69
+ create_data = dict(data)
70
+ create_data.update(override_data or {})
71
+ obj = await create_node(data=create_data, db=db, branch=branch, schema=cls._meta.active_schema)
72
+
73
+ await RepositoryFinalizer(
74
+ account_session=graphql_context.active_account_session,
75
+ services=graphql_context.active_service,
76
+ context=graphql_context.get_context(),
77
+ ).post_create(
78
+ obj=obj, # type: ignore
79
+ branch=branch,
80
+ db=db,
81
+ )
144
82
 
145
- return obj, result
83
+ graphql_response = await build_graphql_response(info=info, db=db, obj=obj)
84
+ return obj, cls(**graphql_response)
146
85
 
147
86
  @classmethod
148
87
  async def mutate_update(
@@ -98,7 +98,7 @@ class IPPrefixPoolGetResource(Mutation):
98
98
  "id": resource.id,
99
99
  "kind": resource.get_kind(),
100
100
  "identifier": data.get("identifier", None),
101
- "display_label": await resource.render_display_label(db=graphql_context.db),
101
+ "display_label": await resource.get_display_label(db=graphql_context.db),
102
102
  "branch": graphql_context.branch.name,
103
103
  },
104
104
  }
@@ -144,7 +144,7 @@ class IPAddressPoolGetResource(Mutation):
144
144
  "id": resource.id,
145
145
  "kind": resource.get_kind(),
146
146
  "identifier": data.get("identifier"),
147
- "display_label": await resource.render_display_label(db=graphql_context.db),
147
+ "display_label": await resource.get_display_label(db=graphql_context.db),
148
148
  "branch": graphql_context.branch.name,
149
149
  },
150
150
  }