infrahub-server 1.2.11__py3-none-any.whl → 1.3.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 (147) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/core/account.py +24 -47
  10. infrahub/core/attribute.py +13 -15
  11. infrahub/core/constants/__init__.py +5 -0
  12. infrahub/core/constants/infrahubkind.py +9 -0
  13. infrahub/core/convert_object_type/__init__.py +0 -0
  14. infrahub/core/convert_object_type/conversion.py +122 -0
  15. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  16. infrahub/core/diff/query/all_conflicts.py +1 -5
  17. infrahub/core/diff/query/artifact.py +10 -20
  18. infrahub/core/diff/query/diff_get.py +3 -6
  19. infrahub/core/diff/query/field_summary.py +2 -4
  20. infrahub/core/diff/query/merge.py +70 -123
  21. infrahub/core/diff/query/save.py +20 -32
  22. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  23. infrahub/core/manager.py +14 -11
  24. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  25. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  26. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  27. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  28. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  29. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  30. infrahub/core/migrations/query/attribute_add.py +1 -2
  31. infrahub/core/migrations/query/attribute_rename.py +5 -10
  32. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  33. infrahub/core/migrations/query/node_duplicate.py +19 -21
  34. infrahub/core/migrations/query/relationship_duplicate.py +19 -17
  35. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  36. infrahub/core/migrations/schema/node_remove.py +19 -19
  37. infrahub/core/models.py +29 -2
  38. infrahub/core/node/__init__.py +90 -18
  39. infrahub/core/node/create.py +211 -0
  40. infrahub/core/node/resource_manager/number_pool.py +31 -5
  41. infrahub/core/node/standard.py +6 -1
  42. infrahub/core/protocols.py +56 -0
  43. infrahub/core/protocols_base.py +3 -0
  44. infrahub/core/query/__init__.py +2 -2
  45. infrahub/core/query/diff.py +19 -32
  46. infrahub/core/query/ipam.py +10 -20
  47. infrahub/core/query/node.py +28 -46
  48. infrahub/core/query/relationship.py +53 -32
  49. infrahub/core/query/resource_manager.py +1 -2
  50. infrahub/core/query/subquery.py +2 -4
  51. infrahub/core/relationship/model.py +3 -0
  52. infrahub/core/schema/__init__.py +2 -1
  53. infrahub/core/schema/attribute_parameters.py +160 -0
  54. infrahub/core/schema/attribute_schema.py +111 -8
  55. infrahub/core/schema/basenode_schema.py +25 -1
  56. infrahub/core/schema/definitions/core/__init__.py +29 -1
  57. infrahub/core/schema/definitions/core/group.py +45 -0
  58. infrahub/core/schema/definitions/internal.py +27 -4
  59. infrahub/core/schema/generated/attribute_schema.py +16 -3
  60. infrahub/core/schema/manager.py +3 -0
  61. infrahub/core/schema/schema_branch.py +67 -7
  62. infrahub/core/validators/__init__.py +13 -1
  63. infrahub/core/validators/attribute/choices.py +1 -3
  64. infrahub/core/validators/attribute/enum.py +1 -3
  65. infrahub/core/validators/attribute/kind.py +1 -3
  66. infrahub/core/validators/attribute/length.py +13 -7
  67. infrahub/core/validators/attribute/min_max.py +118 -0
  68. infrahub/core/validators/attribute/number_pool.py +106 -0
  69. infrahub/core/validators/attribute/optional.py +1 -4
  70. infrahub/core/validators/attribute/regex.py +5 -6
  71. infrahub/core/validators/attribute/unique.py +1 -3
  72. infrahub/core/validators/determiner.py +18 -2
  73. infrahub/core/validators/enum.py +12 -0
  74. infrahub/core/validators/node/hierarchy.py +3 -6
  75. infrahub/core/validators/query.py +1 -3
  76. infrahub/core/validators/relationship/count.py +6 -12
  77. infrahub/core/validators/relationship/optional.py +2 -4
  78. infrahub/core/validators/relationship/peer.py +3 -8
  79. infrahub/core/validators/uniqueness/query.py +5 -9
  80. infrahub/database/__init__.py +11 -2
  81. infrahub/events/group_action.py +1 -0
  82. infrahub/git/base.py +5 -3
  83. infrahub/git/integrator.py +102 -3
  84. infrahub/graphql/analyzer.py +139 -18
  85. infrahub/graphql/manager.py +4 -0
  86. infrahub/graphql/mutations/action.py +164 -0
  87. infrahub/graphql/mutations/convert_object_type.py +62 -0
  88. infrahub/graphql/mutations/main.py +24 -175
  89. infrahub/graphql/mutations/proposed_change.py +20 -17
  90. infrahub/graphql/mutations/resource_manager.py +62 -6
  91. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  92. infrahub/graphql/queries/resource_manager.py +7 -1
  93. infrahub/graphql/schema.py +6 -0
  94. infrahub/menu/menu.py +31 -0
  95. infrahub/message_bus/messages/__init__.py +0 -10
  96. infrahub/message_bus/operations/__init__.py +0 -8
  97. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  98. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  99. infrahub/pools/number.py +5 -3
  100. infrahub/prefect_server/models.py +1 -19
  101. infrahub/proposed_change/models.py +68 -3
  102. infrahub/proposed_change/tasks.py +907 -30
  103. infrahub/task_manager/models.py +10 -6
  104. infrahub/trigger/catalogue.py +2 -0
  105. infrahub/trigger/models.py +18 -2
  106. infrahub/trigger/tasks.py +3 -1
  107. infrahub/types.py +6 -0
  108. infrahub/workflows/catalogue.py +76 -0
  109. infrahub_sdk/client.py +43 -10
  110. infrahub_sdk/node/__init__.py +39 -0
  111. infrahub_sdk/node/attribute.py +122 -0
  112. infrahub_sdk/node/constants.py +21 -0
  113. infrahub_sdk/{node.py → node/node.py} +50 -749
  114. infrahub_sdk/node/parsers.py +15 -0
  115. infrahub_sdk/node/property.py +24 -0
  116. infrahub_sdk/node/related_node.py +266 -0
  117. infrahub_sdk/node/relationship.py +302 -0
  118. infrahub_sdk/protocols.py +112 -0
  119. infrahub_sdk/protocols_base.py +34 -2
  120. infrahub_sdk/query_groups.py +13 -2
  121. infrahub_sdk/schema/main.py +1 -0
  122. infrahub_sdk/schema/repository.py +16 -0
  123. infrahub_sdk/spec/object.py +1 -1
  124. infrahub_sdk/store.py +1 -1
  125. infrahub_sdk/testing/schemas/car_person.py +1 -0
  126. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/METADATA +4 -4
  127. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/RECORD +134 -122
  128. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/WHEEL +1 -1
  129. infrahub_testcontainers/container.py +0 -1
  130. infrahub_testcontainers/docker-compose.test.yml +1 -1
  131. infrahub_testcontainers/helpers.py +8 -2
  132. infrahub/message_bus/messages/check_generator_run.py +0 -26
  133. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  134. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  135. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  136. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  137. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  138. infrahub/message_bus/operations/check/__init__.py +0 -3
  139. infrahub/message_bus/operations/check/generator.py +0 -156
  140. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  141. infrahub/message_bus/operations/finalize/validator.py +0 -133
  142. infrahub/message_bus/operations/requests/__init__.py +0 -9
  143. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  144. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  145. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  146. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/LICENSE.txt +0 -0
  147. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/entry_points.txt +0 -0
@@ -29,10 +29,12 @@ from infrahub_sdk.schema.repository import (
29
29
  InfrahubPythonTransformConfig,
30
30
  InfrahubRepositoryConfig,
31
31
  )
32
+ from infrahub_sdk.spec.menu import MenuFile
33
+ from infrahub_sdk.spec.object import ObjectFile
32
34
  from infrahub_sdk.template import Jinja2Template
33
35
  from infrahub_sdk.template.exceptions import JinjaTemplateError
34
36
  from infrahub_sdk.utils import compare_lists
35
- from infrahub_sdk.yaml import SchemaFile
37
+ from infrahub_sdk.yaml import InfrahubFile, SchemaFile
36
38
  from prefect import flow, task
37
39
  from prefect.cache_policies import NONE
38
40
  from prefect.logging import get_run_logger
@@ -40,7 +42,7 @@ from pydantic import BaseModel, Field
40
42
  from pydantic import ValidationError as PydanticValidationError
41
43
  from typing_extensions import Self
42
44
 
43
- from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositorySyncStatus
45
+ from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositoryObjects, RepositorySyncStatus
44
46
  from infrahub.core.registry import registry
45
47
  from infrahub.events.artifact_action import ArtifactCreatedEvent, ArtifactUpdatedEvent
46
48
  from infrahub.events.models import EventMeta
@@ -54,6 +56,7 @@ if TYPE_CHECKING:
54
56
  import types
55
57
 
56
58
  from infrahub_sdk.checks import InfrahubCheck
59
+ from infrahub_sdk.ctl.utils import YamlFileVar
57
60
  from infrahub_sdk.schema.repository import InfrahubRepositoryArtifactDefinitionConfig
58
61
  from infrahub_sdk.transforms import InfrahubTransform
59
62
 
@@ -159,7 +162,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
159
162
  async def ensure_location_is_defined(self) -> None:
160
163
  if self.location:
161
164
  return
162
- client = self.get_client()
165
+ client = self.sdk
163
166
  repo = await client.get(
164
167
  kind=CoreGenericRepository, name__value=self.name, exclude=["tags", "credential"], raise_when_missing=True
165
168
  )
@@ -179,6 +182,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
179
182
 
180
183
  config_file = await self.get_repository_config(branch_name=infrahub_branch_name, commit=commit) # type: ignore[misc]
181
184
  sync_status = RepositorySyncStatus.IN_SYNC if config_file else RepositorySyncStatus.ERROR_IMPORT
185
+
182
186
  error: Exception | None = None
183
187
 
184
188
  try:
@@ -189,6 +193,17 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
189
193
  branch_name=infrahub_branch_name, commit=commit, config_file=config_file
190
194
  ) # type: ignore[misc]
191
195
 
196
+ await self.import_objects(
197
+ branch_name=infrahub_branch_name,
198
+ commit=commit,
199
+ config_file=config_file,
200
+ ) # type: ignore[misc]
201
+ await self.import_objects(
202
+ branch_name=infrahub_branch_name,
203
+ commit=commit,
204
+ config_file=config_file,
205
+ ) # type: ignore[misc]
206
+
192
207
  await self.import_all_python_files( # type: ignore[call-overload]
193
208
  branch_name=infrahub_branch_name, commit=commit, config_file=config_file
194
209
  ) # type: ignore[misc]
@@ -815,6 +830,80 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
815
830
  log.info(f"TransformPython {transform_name!r} not found locally, deleting")
816
831
  await transform_definition_in_graph[transform_name].delete()
817
832
 
833
+ async def _load_yamlfile_from_disk(self, paths: list[Path], file_type: type[YamlFileVar]) -> list[YamlFileVar]:
834
+ data_files = file_type.load_from_disk(paths=paths)
835
+
836
+ for data_file in data_files:
837
+ if not data_file.valid or not data_file.content:
838
+ raise ValueError(f"{data_file.error_message} ({data_file.location})")
839
+
840
+ return data_files
841
+
842
+ async def _load_objects(
843
+ self,
844
+ paths: list[Path],
845
+ branch: str,
846
+ file_type: type[InfrahubFile],
847
+ ) -> None:
848
+ """Load one or multiple objects files into Infrahub."""
849
+
850
+ log = get_run_logger()
851
+ files = await self._load_yamlfile_from_disk(paths=paths, file_type=file_type)
852
+
853
+ for file in files:
854
+ await file.validate_format(client=self.sdk, branch=branch)
855
+ schema = await self.sdk.schema.get(kind=file.spec.kind, branch=branch)
856
+ if not schema.human_friendly_id and not schema.default_filter:
857
+ raise ValueError(
858
+ f"Schemas of objects or menus defined within {file.location} "
859
+ "should have a `human_friendly_id` defined to avoid creating duplicated objects."
860
+ )
861
+
862
+ for file in files:
863
+ log.info(f"Loading objects defined in {file.location}")
864
+ await file.process(client=self.sdk, branch=branch)
865
+
866
+ async def _import_file_paths(
867
+ self, branch_name: str, commit: str, files_pathes: list[Path], object_type: RepositoryObjects
868
+ ) -> None:
869
+ branch_wt = self.get_worktree(identifier=commit or branch_name)
870
+ file_pathes = [branch_wt.directory / file_path for file_path in files_pathes]
871
+
872
+ # We currently assume there can't be concurrent imports, but if so, we might need to clone the client before tracking here.
873
+ async with self.sdk.start_tracking(
874
+ identifier=f"group-repo-{object_type.value}-{self.id}",
875
+ delete_unused_nodes=True,
876
+ branch=branch_name,
877
+ group_type="CoreRepositoryGroup",
878
+ group_params={"content": object_type.value, "repository": str(self.id)},
879
+ ):
880
+ file_type = repo_object_type_to_file_type(object_type)
881
+ await self._load_objects(
882
+ paths=file_pathes,
883
+ branch=branch_name,
884
+ file_type=file_type,
885
+ )
886
+
887
+ @task(name="import-objects", task_run_name="Import Objects", cache_policy=NONE) # type: ignore[arg-type]
888
+ async def import_objects(
889
+ self,
890
+ branch_name: str,
891
+ commit: str,
892
+ config_file: InfrahubRepositoryConfig,
893
+ ) -> None:
894
+ await self._import_file_paths(
895
+ branch_name=branch_name,
896
+ commit=commit,
897
+ files_pathes=config_file.objects,
898
+ object_type=RepositoryObjects.OBJECT,
899
+ )
900
+ await self._import_file_paths(
901
+ branch_name=branch_name,
902
+ commit=commit,
903
+ files_pathes=config_file.menus,
904
+ object_type=RepositoryObjects.MENU,
905
+ )
906
+
818
907
  @task(name="check-definition-get", task_run_name="Get Check Definition", cache_policy=NONE) # type: ignore[arg-type]
819
908
  async def get_check_definition(
820
909
  self,
@@ -1342,3 +1431,13 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1342
1431
 
1343
1432
  await self.service.event.send(event=event)
1344
1433
  return ArtifactGenerateResult(changed=True, checksum=checksum, storage_id=storage_id, artifact_id=artifact.id)
1434
+
1435
+
1436
+ def repo_object_type_to_file_type(repo_object: RepositoryObjects) -> type[InfrahubFile]:
1437
+ match repo_object:
1438
+ case RepositoryObjects.OBJECT:
1439
+ return ObjectFile
1440
+ case RepositoryObjects.MENU:
1441
+ return MenuFile
1442
+ case _:
1443
+ raise ValueError(f"Unknown repository object type: {repo_object}")
@@ -13,11 +13,27 @@ from graphql import (
13
13
  FragmentSpreadNode,
14
14
  GraphQLSchema,
15
15
  InlineFragmentNode,
16
+ ListTypeNode,
16
17
  NamedTypeNode,
17
18
  NonNullTypeNode,
18
19
  OperationDefinitionNode,
19
20
  OperationType,
20
21
  SelectionSetNode,
22
+ TypeNode,
23
+ )
24
+ from graphql.language.ast import (
25
+ BooleanValueNode,
26
+ ConstListValueNode,
27
+ ConstObjectValueNode,
28
+ EnumValueNode,
29
+ FloatValueNode,
30
+ IntValueNode,
31
+ ListValueNode,
32
+ NullValueNode,
33
+ ObjectValueNode,
34
+ StringValueNode,
35
+ ValueNode,
36
+ VariableNode,
21
37
  )
22
38
  from infrahub_sdk.analyzer import GraphQLQueryAnalyzer
23
39
  from infrahub_sdk.utils import extract_fields
@@ -91,9 +107,24 @@ class GraphQLSelectionSet:
91
107
  @dataclass
92
108
  class GraphQLArgument:
93
109
  name: str
94
- value: str
110
+ value: Any
95
111
  kind: str
96
112
 
113
+ @property
114
+ def is_variable(self) -> bool:
115
+ return self.kind == "variable"
116
+
117
+ @property
118
+ def as_variable_name(self) -> str:
119
+ """Return the name without a $ prefix"""
120
+ return str(self.value).removeprefix("$")
121
+
122
+ @property
123
+ def fields(self) -> list[str]:
124
+ if self.kind != "object_value" or not isinstance(self.value, dict):
125
+ return []
126
+ return sorted(self.value.keys())
127
+
97
128
 
98
129
  @dataclass
99
130
  class ObjectAccess:
@@ -106,6 +137,9 @@ class GraphQLVariable:
106
137
  name: str
107
138
  type: str
108
139
  required: bool
140
+ is_list: bool = False
141
+ inner_required: bool = False
142
+ default: Any | None = None
109
143
 
110
144
 
111
145
  @dataclass
@@ -266,6 +300,28 @@ class GraphQLQueryReport:
266
300
 
267
301
  return fields
268
302
 
303
+ @cached_property
304
+ def variables(self) -> list[GraphQLVariable]:
305
+ """Return input variables defined on the query document
306
+
307
+ All subqueries will use the same document level queries,
308
+ so only the first entry is required
309
+ """
310
+ if self.queries:
311
+ return self.queries[0].variables
312
+ return []
313
+
314
+ def required_argument(self, argument: GraphQLArgument) -> bool:
315
+ if not argument.is_variable:
316
+ # If the argument isn't a variable it would have been
317
+ # statically defined in the input and as such required
318
+ return True
319
+ for variable in self.variables:
320
+ if variable.name == argument.as_variable_name and variable.required:
321
+ return True
322
+
323
+ return False
324
+
269
325
  @cached_property
270
326
  def top_level_kinds(self) -> list[str]:
271
327
  return [query.infrahub_model.kind for query in self.queries if query.infrahub_model]
@@ -298,6 +354,22 @@ class GraphQLQueryReport:
298
354
 
299
355
  return access
300
356
 
357
+ @property
358
+ def only_has_unique_targets(self) -> bool:
359
+ """Indicate if the query document is defined so that it will return a single root level object"""
360
+ for query in self.queries:
361
+ targets_single_query = False
362
+ if query.infrahub_model and query.infrahub_model.uniqueness_constraints:
363
+ for argument in query.arguments:
364
+ if [[argument.name]] == query.infrahub_model.uniqueness_constraints:
365
+ if self.required_argument(argument=argument):
366
+ targets_single_query = True
367
+
368
+ if not targets_single_query:
369
+ return False
370
+
371
+ return True
372
+
301
373
 
302
374
  class InfrahubGraphQLQueryAnalyzer(GraphQLQueryAnalyzer):
303
375
  def __init__(
@@ -603,31 +675,80 @@ class InfrahubGraphQLQueryAnalyzer(GraphQLQueryAnalyzer):
603
675
  ],
604
676
  )
605
677
 
606
- @staticmethod
607
- def _get_variables(operation: OperationDefinitionNode) -> list[GraphQLVariable]:
608
- variables = []
609
- for variable in operation.variable_definitions:
610
- if isinstance(variable.type, NamedTypeNode):
611
- variables.append(
612
- GraphQLVariable(name=variable.variable.name.value, type=variable.type.name.value, required=False)
678
+ def _get_variables(self, operation: OperationDefinitionNode) -> list[GraphQLVariable]:
679
+ variables: list[GraphQLVariable] = []
680
+
681
+ for variable in operation.variable_definitions or []:
682
+ type_node: TypeNode = variable.type
683
+ required = False
684
+ is_list = False
685
+ inner_required = False
686
+
687
+ if isinstance(type_node, NonNullTypeNode):
688
+ required = True
689
+ type_node = type_node.type
690
+
691
+ if isinstance(type_node, ListTypeNode):
692
+ is_list = True
693
+ inner_type = type_node.type
694
+
695
+ if isinstance(inner_type, NonNullTypeNode):
696
+ inner_required = True
697
+ inner_type = inner_type.type
698
+
699
+ if isinstance(inner_type, NamedTypeNode):
700
+ type_name = inner_type.name.value
701
+ else:
702
+ raise TypeError(f"Unsupported inner type node: {inner_type}")
703
+ elif isinstance(type_node, NamedTypeNode):
704
+ type_name = type_node.name.value
705
+ else:
706
+ raise TypeError(f"Unsupported type node: {type_node}")
707
+
708
+ variables.append(
709
+ GraphQLVariable(
710
+ name=variable.variable.name.value,
711
+ type=type_name,
712
+ required=required,
713
+ is_list=is_list,
714
+ inner_required=inner_required,
715
+ default=self._parse_value(variable.default_value) if variable.default_value else None,
613
716
  )
614
- elif isinstance(variable.type, NonNullTypeNode):
615
- if isinstance(variable.type.type, NamedTypeNode):
616
- variables.append(
617
- GraphQLVariable(
618
- name=variable.variable.name.value, type=variable.type.type.name.value, required=True
619
- )
620
- )
717
+ )
621
718
 
622
719
  return variables
623
720
 
624
- @staticmethod
625
- def _parse_arguments(field_node: FieldNode) -> list[GraphQLArgument]:
721
+ def _parse_arguments(self, field_node: FieldNode) -> list[GraphQLArgument]:
626
722
  return [
627
723
  GraphQLArgument(
628
724
  name=argument.name.value,
629
- value=getattr(argument.value, "value", ""),
725
+ value=self._parse_value(argument.value),
630
726
  kind=argument.value.kind,
631
727
  )
632
728
  for argument in field_node.arguments
633
729
  ]
730
+
731
+ def _parse_value(self, node: ValueNode) -> Any:
732
+ match node:
733
+ case VariableNode():
734
+ value: Any = f"${node.name.value}"
735
+ case IntValueNode():
736
+ value = int(node.value)
737
+ case FloatValueNode():
738
+ value = float(node.value)
739
+ case StringValueNode():
740
+ value = node.value
741
+ case BooleanValueNode():
742
+ value = node.value
743
+ case NullValueNode():
744
+ value = None
745
+ case EnumValueNode():
746
+ value = node.value
747
+ case ListValueNode() | ConstListValueNode():
748
+ value = [self._parse_value(item) for item in node.values]
749
+ case ObjectValueNode() | ConstObjectValueNode():
750
+ value = {field.name.value: self._parse_value(field.value) for field in node.fields}
751
+ case _:
752
+ raise TypeError(f"Unsupported value node: {node}")
753
+
754
+ return value
@@ -25,6 +25,7 @@ from infrahub.types import ATTRIBUTE_TYPES, InfrahubDataType, get_attribute_type
25
25
  from .directives import DIRECTIVES
26
26
  from .enums import generate_graphql_enum, get_enum_attribute_type_name
27
27
  from .metrics import SCHEMA_GENERATE_GRAPHQL_METRICS
28
+ from .mutations.action import InfrahubTriggerRuleMatchMutation, InfrahubTriggerRuleMutation
28
29
  from .mutations.artifact_definition import InfrahubArtifactDefinitionMutation
29
30
  from .mutations.ipam import (
30
31
  InfrahubIPAddressMutation,
@@ -524,6 +525,9 @@ class GraphQLSchemaManager:
524
525
  InfrahubKind.MENUITEM: InfrahubCoreMenuMutation,
525
526
  InfrahubKind.STANDARDWEBHOOK: InfrahubWebhookMutation,
526
527
  InfrahubKind.CUSTOMWEBHOOK: InfrahubWebhookMutation,
528
+ InfrahubKind.NODETRIGGERRULE: InfrahubTriggerRuleMutation,
529
+ InfrahubKind.NODETRIGGERATTRIBUTEMATCH: InfrahubTriggerRuleMatchMutation,
530
+ InfrahubKind.NODETRIGGERRELATIONSHIPMATCH: InfrahubTriggerRuleMatchMutation,
527
531
  }
528
532
 
529
533
  if isinstance(node_schema, NodeSchema) and node_schema.is_ip_prefix():
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, cast
4
+
5
+ from graphene import InputObjectType, Mutation
6
+ from typing_extensions import Self
7
+
8
+ from infrahub.core.protocols import CoreNodeTriggerAttributeMatch, CoreNodeTriggerRelationshipMatch, CoreNodeTriggerRule
9
+ from infrahub.exceptions import SchemaNotFoundError, ValidationError
10
+ from infrahub.log import get_logger
11
+
12
+ from .main import InfrahubMutationMixin, InfrahubMutationOptions
13
+
14
+ if TYPE_CHECKING:
15
+ from graphql import GraphQLResolveInfo
16
+
17
+ from infrahub.core.branch import Branch
18
+ from infrahub.core.node import Node
19
+ from infrahub.core.schema import NodeSchema
20
+ from infrahub.database import InfrahubDatabase
21
+
22
+ from ..initialization import GraphqlContext
23
+
24
+ log = get_logger()
25
+
26
+
27
+ class InfrahubTriggerRuleMutation(InfrahubMutationMixin, Mutation):
28
+ @classmethod
29
+ def __init_subclass_with_meta__(
30
+ cls,
31
+ schema: NodeSchema,
32
+ _meta: Any | None = None,
33
+ **options: dict[str, Any],
34
+ ) -> None:
35
+ if not _meta:
36
+ _meta = InfrahubMutationOptions(cls)
37
+
38
+ _meta.schema = schema
39
+
40
+ super().__init_subclass_with_meta__(_meta=_meta, **options)
41
+
42
+ @classmethod
43
+ async def mutate_create(
44
+ cls,
45
+ info: GraphQLResolveInfo,
46
+ data: InputObjectType,
47
+ branch: Branch,
48
+ database: InfrahubDatabase | None = None,
49
+ ) -> tuple[Node, Self]:
50
+ graphql_context: GraphqlContext = info.context
51
+ db = database or graphql_context.db
52
+ _validate_node_kind(data=data, db=db)
53
+ trigger_rule_definition, result = await super().mutate_create(info=info, data=data, branch=branch, database=db)
54
+
55
+ return trigger_rule_definition, result
56
+
57
+ @classmethod
58
+ async def mutate_update(
59
+ cls,
60
+ info: GraphQLResolveInfo,
61
+ data: InputObjectType,
62
+ branch: Branch,
63
+ database: InfrahubDatabase | None = None,
64
+ node: Node | None = None, # noqa: ARG003
65
+ ) -> tuple[Node, Self]:
66
+ graphql_context: GraphqlContext = info.context
67
+ db = database or graphql_context.db
68
+ _validate_node_kind(data=data, db=db)
69
+ trigger_rule_definition, result = await super().mutate_update(info=info, data=data, branch=branch, database=db)
70
+
71
+ return trigger_rule_definition, result
72
+
73
+
74
+ class InfrahubTriggerRuleMatchMutation(InfrahubMutationMixin, Mutation):
75
+ @classmethod
76
+ def __init_subclass_with_meta__(
77
+ cls,
78
+ schema: NodeSchema,
79
+ _meta: Any | None = None,
80
+ **options: dict[str, Any],
81
+ ) -> None:
82
+ if not _meta:
83
+ _meta = InfrahubMutationOptions(cls)
84
+
85
+ _meta.schema = schema
86
+
87
+ super().__init_subclass_with_meta__(_meta=_meta, **options)
88
+
89
+ @classmethod
90
+ async def mutate_create(
91
+ cls,
92
+ info: GraphQLResolveInfo,
93
+ data: InputObjectType,
94
+ branch: Branch,
95
+ database: InfrahubDatabase | None = None, # noqa: ARG003
96
+ ) -> tuple[Node, Self]:
97
+ graphql_context: GraphqlContext = info.context
98
+
99
+ async with graphql_context.db.start_transaction() as dbt:
100
+ trigger_match, result = await super().mutate_create(info=info, data=data, branch=branch, database=dbt)
101
+ trigger_match_model = cast(CoreNodeTriggerAttributeMatch | CoreNodeTriggerRelationshipMatch, trigger_match)
102
+ node_trigger_rule = await trigger_match_model.trigger.get_peer(db=dbt, raise_on_error=True)
103
+ node_trigger_rule_model = cast(CoreNodeTriggerRule, node_trigger_rule)
104
+ node_schema = dbt.schema.get_node_schema(name=node_trigger_rule_model.node_kind.value, duplicate=False)
105
+ _validate_node_kind_field(data=data, node_schema=node_schema)
106
+
107
+ return trigger_match, result
108
+
109
+ @classmethod
110
+ async def mutate_update(
111
+ cls,
112
+ info: GraphQLResolveInfo,
113
+ data: InputObjectType,
114
+ branch: Branch,
115
+ database: InfrahubDatabase | None = None, # noqa: ARG003
116
+ node: Node | None = None, # noqa: ARG003
117
+ ) -> tuple[Node, Self]:
118
+ graphql_context: GraphqlContext = info.context
119
+ async with graphql_context.db.start_transaction() as dbt:
120
+ trigger_match, result = await super().mutate_update(info=info, data=data, branch=branch, database=dbt)
121
+ trigger_match_model = cast(CoreNodeTriggerAttributeMatch | CoreNodeTriggerRelationshipMatch, trigger_match)
122
+ node_trigger_rule = await trigger_match_model.trigger.get_peer(db=dbt, raise_on_error=True)
123
+ node_trigger_rule_model = cast(CoreNodeTriggerRule, node_trigger_rule)
124
+ node_schema = dbt.schema.get_node_schema(name=node_trigger_rule_model.node_kind.value, duplicate=False)
125
+ _validate_node_kind_field(data=data, node_schema=node_schema)
126
+
127
+ return trigger_match, result
128
+
129
+
130
+ def _validate_node_kind(data: InputObjectType, db: InfrahubDatabase) -> None:
131
+ input_data = cast(dict[str, dict[str, Any]], data)
132
+ if node_kind := input_data.get("node_kind"):
133
+ value = node_kind.get("value")
134
+ if isinstance(value, str):
135
+ try:
136
+ db.schema.get_node_schema(name=value, duplicate=False)
137
+ except SchemaNotFoundError as exc:
138
+ raise ValidationError(
139
+ input_value={"node_kind": "The requested node_kind schema was not found"}
140
+ ) from exc
141
+ except ValueError as exc:
142
+ raise ValidationError(input_value={"node_kind": "The requested node_kind is not a valid node"}) from exc
143
+
144
+
145
+ def _validate_node_kind_field(data: InputObjectType, node_schema: NodeSchema) -> None:
146
+ input_data = cast(dict[str, dict[str, Any]], data)
147
+ if attribute_name := input_data.get("attribute_name"):
148
+ value = attribute_name.get("value")
149
+ if isinstance(value, str):
150
+ if value not in node_schema.attribute_names:
151
+ raise ValidationError(
152
+ input_value={
153
+ "attribute_name": f"The attribute {value} doesn't exist on related node trigger using {node_schema.kind}"
154
+ }
155
+ )
156
+ if relationship_name := input_data.get("relationship_name"):
157
+ value = relationship_name.get("value")
158
+ if isinstance(value, str):
159
+ if value not in node_schema.relationship_names:
160
+ raise ValidationError(
161
+ input_value={
162
+ "relationship_name": f"The relationship {value} doesn't exist on related node trigger using {node_schema.kind}"
163
+ }
164
+ )
@@ -0,0 +1,62 @@
1
+ from typing import TYPE_CHECKING, Any, Self
2
+
3
+ from graphene import Boolean, InputObjectType, Mutation, String
4
+ from graphene.types.generic import GenericScalar
5
+ from graphql import GraphQLResolveInfo
6
+
7
+ from infrahub.core import registry
8
+ from infrahub.core.convert_object_type.conversion import InputForDestField, convert_object_type
9
+ from infrahub.core.manager import NodeManager
10
+
11
+ if TYPE_CHECKING:
12
+ from infrahub.graphql.initialization import GraphqlContext
13
+
14
+
15
+ class ConvertObjectTypeInput(InputObjectType):
16
+ node_id = String(required=True)
17
+ target_kind = String(required=True)
18
+ fields_mapping = GenericScalar(required=True) # keys are destination attributes/relationships names.
19
+ branch = String(required=True)
20
+
21
+
22
+ class ConvertObjectType(Mutation):
23
+ class Arguments:
24
+ data = ConvertObjectTypeInput(required=True)
25
+
26
+ ok = Boolean()
27
+ node = GenericScalar()
28
+
29
+ @classmethod
30
+ async def mutate(
31
+ cls,
32
+ root: dict, # noqa: ARG003
33
+ info: GraphQLResolveInfo,
34
+ data: ConvertObjectTypeInput,
35
+ ) -> Self:
36
+ """Convert an input node to a given compatible kind."""
37
+
38
+ graphql_context: GraphqlContext = info.context
39
+
40
+ fields_mapping: dict[str, InputForDestField] = {}
41
+ if not isinstance(data.fields_mapping, dict):
42
+ raise ValueError(f"Expected `fields_mapping` to be a `dict`, got {type(fields_mapping)}")
43
+
44
+ for field, input_for_dest_field_str in data.fields_mapping.items():
45
+ fields_mapping[field] = InputForDestField(**input_for_dest_field_str)
46
+
47
+ node_to_convert = await NodeManager.get_one(
48
+ id=str(data.node_id), db=graphql_context.db, branch=str(data.branch)
49
+ )
50
+ target_schema = registry.get_node_schema(name=str(data.target_kind), branch=data.branch)
51
+ new_node = await convert_object_type(
52
+ node=node_to_convert,
53
+ target_schema=target_schema,
54
+ mapping=fields_mapping,
55
+ branch=graphql_context.branch,
56
+ db=graphql_context.db,
57
+ )
58
+
59
+ dict_node = await new_node.to_graphql(db=graphql_context.db, fields={})
60
+ result: dict[str, Any] = {"ok": True, "node": dict_node}
61
+
62
+ return cls(**result)