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.
- infrahub/actions/constants.py +86 -0
- infrahub/actions/gather.py +114 -0
- infrahub/actions/models.py +241 -0
- infrahub/actions/parsers.py +104 -0
- infrahub/actions/schema.py +382 -0
- infrahub/actions/tasks.py +126 -0
- infrahub/actions/triggers.py +21 -0
- infrahub/cli/db.py +1 -2
- infrahub/core/account.py +24 -47
- infrahub/core/attribute.py +13 -15
- infrahub/core/constants/__init__.py +5 -0
- infrahub/core/constants/infrahubkind.py +9 -0
- infrahub/core/convert_object_type/__init__.py +0 -0
- infrahub/core/convert_object_type/conversion.py +122 -0
- infrahub/core/convert_object_type/schema_mapping.py +56 -0
- infrahub/core/diff/query/all_conflicts.py +1 -5
- infrahub/core/diff/query/artifact.py +10 -20
- infrahub/core/diff/query/diff_get.py +3 -6
- infrahub/core/diff/query/field_summary.py +2 -4
- infrahub/core/diff/query/merge.py +70 -123
- infrahub/core/diff/query/save.py +20 -32
- infrahub/core/diff/query/summary_counts_enricher.py +34 -54
- infrahub/core/manager.py +14 -11
- infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
- infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
- infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
- infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
- infrahub/core/migrations/query/attribute_add.py +1 -2
- infrahub/core/migrations/query/attribute_rename.py +5 -10
- infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
- infrahub/core/migrations/query/node_duplicate.py +19 -21
- infrahub/core/migrations/query/relationship_duplicate.py +19 -17
- infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
- infrahub/core/migrations/schema/node_remove.py +19 -19
- infrahub/core/models.py +29 -2
- infrahub/core/node/__init__.py +90 -18
- infrahub/core/node/create.py +211 -0
- infrahub/core/node/resource_manager/number_pool.py +31 -5
- infrahub/core/node/standard.py +6 -1
- infrahub/core/protocols.py +56 -0
- infrahub/core/protocols_base.py +3 -0
- infrahub/core/query/__init__.py +2 -2
- infrahub/core/query/diff.py +19 -32
- infrahub/core/query/ipam.py +10 -20
- infrahub/core/query/node.py +28 -46
- infrahub/core/query/relationship.py +53 -32
- infrahub/core/query/resource_manager.py +1 -2
- infrahub/core/query/subquery.py +2 -4
- infrahub/core/relationship/model.py +3 -0
- infrahub/core/schema/__init__.py +2 -1
- infrahub/core/schema/attribute_parameters.py +160 -0
- infrahub/core/schema/attribute_schema.py +111 -8
- infrahub/core/schema/basenode_schema.py +25 -1
- infrahub/core/schema/definitions/core/__init__.py +29 -1
- infrahub/core/schema/definitions/core/group.py +45 -0
- infrahub/core/schema/definitions/internal.py +27 -4
- infrahub/core/schema/generated/attribute_schema.py +16 -3
- infrahub/core/schema/manager.py +3 -0
- infrahub/core/schema/schema_branch.py +67 -7
- infrahub/core/validators/__init__.py +13 -1
- infrahub/core/validators/attribute/choices.py +1 -3
- infrahub/core/validators/attribute/enum.py +1 -3
- infrahub/core/validators/attribute/kind.py +1 -3
- infrahub/core/validators/attribute/length.py +13 -7
- infrahub/core/validators/attribute/min_max.py +118 -0
- infrahub/core/validators/attribute/number_pool.py +106 -0
- infrahub/core/validators/attribute/optional.py +1 -4
- infrahub/core/validators/attribute/regex.py +5 -6
- infrahub/core/validators/attribute/unique.py +1 -3
- infrahub/core/validators/determiner.py +18 -2
- infrahub/core/validators/enum.py +12 -0
- infrahub/core/validators/node/hierarchy.py +3 -6
- infrahub/core/validators/query.py +1 -3
- infrahub/core/validators/relationship/count.py +6 -12
- infrahub/core/validators/relationship/optional.py +2 -4
- infrahub/core/validators/relationship/peer.py +3 -8
- infrahub/core/validators/uniqueness/query.py +5 -9
- infrahub/database/__init__.py +11 -2
- infrahub/events/group_action.py +1 -0
- infrahub/git/base.py +5 -3
- infrahub/git/integrator.py +102 -3
- infrahub/graphql/analyzer.py +139 -18
- infrahub/graphql/manager.py +4 -0
- infrahub/graphql/mutations/action.py +164 -0
- infrahub/graphql/mutations/convert_object_type.py +62 -0
- infrahub/graphql/mutations/main.py +24 -175
- infrahub/graphql/mutations/proposed_change.py +20 -17
- infrahub/graphql/mutations/resource_manager.py +62 -6
- infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
- infrahub/graphql/queries/resource_manager.py +7 -1
- infrahub/graphql/schema.py +6 -0
- infrahub/menu/menu.py +31 -0
- infrahub/message_bus/messages/__init__.py +0 -10
- infrahub/message_bus/operations/__init__.py +0 -8
- infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
- infrahub/patch/queries/delete_duplicated_edges.py +5 -10
- infrahub/pools/number.py +5 -3
- infrahub/prefect_server/models.py +1 -19
- infrahub/proposed_change/models.py +68 -3
- infrahub/proposed_change/tasks.py +907 -30
- infrahub/task_manager/models.py +10 -6
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +18 -2
- infrahub/trigger/tasks.py +3 -1
- infrahub/types.py +6 -0
- infrahub/workflows/catalogue.py +76 -0
- infrahub_sdk/client.py +43 -10
- infrahub_sdk/node/__init__.py +39 -0
- infrahub_sdk/node/attribute.py +122 -0
- infrahub_sdk/node/constants.py +21 -0
- infrahub_sdk/{node.py → node/node.py} +50 -749
- infrahub_sdk/node/parsers.py +15 -0
- infrahub_sdk/node/property.py +24 -0
- infrahub_sdk/node/related_node.py +266 -0
- infrahub_sdk/node/relationship.py +302 -0
- infrahub_sdk/protocols.py +112 -0
- infrahub_sdk/protocols_base.py +34 -2
- infrahub_sdk/query_groups.py +13 -2
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +16 -0
- infrahub_sdk/spec/object.py +1 -1
- infrahub_sdk/store.py +1 -1
- infrahub_sdk/testing/schemas/car_person.py +1 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/METADATA +4 -4
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/RECORD +134 -122
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/WHEEL +1 -1
- infrahub_testcontainers/container.py +0 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- infrahub_testcontainers/helpers.py +8 -2
- infrahub/message_bus/messages/check_generator_run.py +0 -26
- infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
- infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
- infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
- infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
- infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
- infrahub/message_bus/operations/check/__init__.py +0 -3
- infrahub/message_bus/operations/check/generator.py +0 -156
- infrahub/message_bus/operations/finalize/__init__.py +0 -3
- infrahub/message_bus/operations/finalize/validator.py +0 -133
- infrahub/message_bus/operations/requests/__init__.py +0 -9
- infrahub/message_bus/operations/requests/generator_definition.py +0 -140
- infrahub/message_bus/operations/requests/proposed_change.py +0 -629
- /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/entry_points.txt +0 -0
infrahub/git/integrator.py
CHANGED
|
@@ -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.
|
|
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}")
|
infrahub/graphql/analyzer.py
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
for variable in operation.variable_definitions:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
infrahub/graphql/manager.py
CHANGED
|
@@ -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)
|