infrahub-server 1.2.6__py3-none-any.whl → 1.2.8__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/api/transformation.py +1 -0
- infrahub/artifacts/models.py +4 -0
- infrahub/cli/db.py +3 -1
- infrahub/cli/patch.py +153 -0
- infrahub/computed_attribute/models.py +81 -1
- infrahub/computed_attribute/tasks.py +35 -53
- infrahub/config.py +2 -1
- infrahub/constants/__init__.py +0 -0
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/graph/index.py +3 -1
- infrahub/core/manager.py +16 -5
- infrahub/core/migrations/graph/m014_remove_index_attr_value.py +7 -8
- infrahub/core/node/__init__.py +4 -1
- infrahub/core/protocols.py +1 -0
- infrahub/core/query/ipam.py +7 -5
- infrahub/core/query/node.py +96 -29
- infrahub/core/schema/definitions/core/builtin.py +2 -4
- infrahub/core/schema/definitions/core/transform.py +1 -0
- infrahub/core/validators/aggregated_checker.py +2 -2
- infrahub/core/validators/uniqueness/query.py +8 -3
- infrahub/database/__init__.py +2 -10
- infrahub/database/index.py +1 -1
- infrahub/database/memgraph.py +2 -1
- infrahub/database/neo4j.py +1 -1
- infrahub/git/integrator.py +27 -3
- infrahub/git/models.py +4 -0
- infrahub/git/tasks.py +3 -0
- infrahub/git_credential/helper.py +2 -2
- infrahub/message_bus/operations/requests/proposed_change.py +6 -0
- infrahub/message_bus/types.py +3 -0
- infrahub/patch/__init__.py +0 -0
- infrahub/patch/constants.py +13 -0
- infrahub/patch/edge_adder.py +64 -0
- infrahub/patch/edge_deleter.py +33 -0
- infrahub/patch/edge_updater.py +28 -0
- infrahub/patch/models.py +98 -0
- infrahub/patch/plan_reader.py +107 -0
- infrahub/patch/plan_writer.py +92 -0
- infrahub/patch/queries/__init__.py +0 -0
- infrahub/patch/queries/base.py +17 -0
- infrahub/patch/queries/consolidate_duplicated_nodes.py +109 -0
- infrahub/patch/queries/delete_duplicated_edges.py +138 -0
- infrahub/patch/runner.py +254 -0
- infrahub/patch/vertex_adder.py +61 -0
- infrahub/patch/vertex_deleter.py +33 -0
- infrahub/patch/vertex_updater.py +28 -0
- infrahub/proposed_change/tasks.py +1 -0
- infrahub/server.py +3 -1
- infrahub/transformations/models.py +3 -0
- infrahub/transformations/tasks.py +1 -0
- infrahub/webhook/models.py +3 -0
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/client.py +4 -4
- infrahub_sdk/config.py +17 -0
- infrahub_sdk/ctl/cli_commands.py +9 -3
- infrahub_sdk/ctl/generator.py +2 -2
- infrahub_sdk/ctl/menu.py +56 -13
- infrahub_sdk/ctl/object.py +55 -5
- infrahub_sdk/ctl/utils.py +22 -1
- infrahub_sdk/exceptions.py +19 -1
- infrahub_sdk/generator.py +12 -66
- infrahub_sdk/node.py +42 -26
- infrahub_sdk/operation.py +80 -0
- infrahub_sdk/protocols.py +12 -0
- infrahub_sdk/protocols_generator/__init__.py +0 -0
- infrahub_sdk/protocols_generator/constants.py +28 -0
- infrahub_sdk/{code_generator.py → protocols_generator/generator.py} +47 -34
- infrahub_sdk/protocols_generator/template.j2 +114 -0
- infrahub_sdk/recorder.py +3 -0
- infrahub_sdk/schema/__init__.py +110 -74
- infrahub_sdk/schema/main.py +36 -2
- infrahub_sdk/schema/repository.py +6 -0
- infrahub_sdk/spec/menu.py +3 -3
- infrahub_sdk/spec/object.py +522 -41
- infrahub_sdk/testing/docker.py +4 -5
- infrahub_sdk/testing/schemas/animal.py +7 -0
- infrahub_sdk/transforms.py +15 -27
- infrahub_sdk/yaml.py +63 -7
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/METADATA +2 -2
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/RECORD +85 -64
- infrahub_testcontainers/docker-compose.test.yml +2 -0
- infrahub_sdk/ctl/constants.py +0 -115
- /infrahub/{database/constants.py → constants/database.py} +0 -0
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/WHEEL +0 -0
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/entry_points.txt +0 -0
infrahub/api/transformation.py
CHANGED
|
@@ -88,6 +88,7 @@ async def transform_python(
|
|
|
88
88
|
branch=branch_params.branch.name,
|
|
89
89
|
transform_location=f"{transform.file_path.value}::{transform.class_name.value}",
|
|
90
90
|
timeout=transform.timeout.value,
|
|
91
|
+
convert_query_response=transform.convert_query_response.value or False,
|
|
91
92
|
data=data,
|
|
92
93
|
)
|
|
93
94
|
|
infrahub/artifacts/models.py
CHANGED
|
@@ -12,6 +12,10 @@ class CheckArtifactCreate(BaseModel):
|
|
|
12
12
|
content_type: str = Field(..., description="Content type of the artifact")
|
|
13
13
|
transform_type: str = Field(..., description="The type of transform associated with this artifact")
|
|
14
14
|
transform_location: str = Field(..., description="The transforms location within the repository")
|
|
15
|
+
convert_query_response: bool = Field(
|
|
16
|
+
default=False,
|
|
17
|
+
description="Indicate if the query response should be converted to InfrahubNode objects for Python transforms",
|
|
18
|
+
)
|
|
15
19
|
repository_id: str = Field(..., description="The unique ID of the Repository")
|
|
16
20
|
repository_name: str = Field(..., description="The name of the Repository")
|
|
17
21
|
repository_kind: str = Field(..., description="The kind of the Repository")
|
infrahub/cli/db.py
CHANGED
|
@@ -54,12 +54,14 @@ from infrahub.services.adapters.message_bus.local import BusSimulator
|
|
|
54
54
|
from infrahub.services.adapters.workflow.local import WorkflowLocalExecution
|
|
55
55
|
|
|
56
56
|
from .constants import ERROR_BADGE, FAILED_BADGE, SUCCESS_BADGE
|
|
57
|
+
from .patch import patch_app
|
|
57
58
|
|
|
58
59
|
if TYPE_CHECKING:
|
|
59
60
|
from infrahub.cli.context import CliContext
|
|
60
61
|
from infrahub.database import InfrahubDatabase
|
|
61
62
|
|
|
62
63
|
app = AsyncTyper()
|
|
64
|
+
app.add_typer(patch_app, name="patch")
|
|
63
65
|
|
|
64
66
|
PERMISSIONS_AVAILABLE = ["read", "write", "admin"]
|
|
65
67
|
|
|
@@ -410,7 +412,7 @@ async def update_core_schema(
|
|
|
410
412
|
update_db=True,
|
|
411
413
|
)
|
|
412
414
|
default_branch.update_schema_hash()
|
|
413
|
-
rprint("The Core Schema has been updated")
|
|
415
|
+
rprint("The Core Schema has been updated, make sure to rebase any open branches after the upgrade")
|
|
414
416
|
if debug:
|
|
415
417
|
rprint(f"New schema hash: {default_branch.active_schema_hash.main}")
|
|
416
418
|
await default_branch.save(db=dbt)
|
infrahub/cli/patch.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from infrahub_sdk.async_typer import AsyncTyper
|
|
11
|
+
from rich import print as rprint
|
|
12
|
+
|
|
13
|
+
from infrahub import config
|
|
14
|
+
from infrahub.patch.edge_adder import PatchPlanEdgeAdder
|
|
15
|
+
from infrahub.patch.edge_deleter import PatchPlanEdgeDeleter
|
|
16
|
+
from infrahub.patch.edge_updater import PatchPlanEdgeUpdater
|
|
17
|
+
from infrahub.patch.plan_reader import PatchPlanReader
|
|
18
|
+
from infrahub.patch.plan_writer import PatchPlanWriter
|
|
19
|
+
from infrahub.patch.queries.base import PatchQuery
|
|
20
|
+
from infrahub.patch.runner import (
|
|
21
|
+
PatchPlanEdgeDbIdTranslator,
|
|
22
|
+
PatchRunner,
|
|
23
|
+
)
|
|
24
|
+
from infrahub.patch.vertex_adder import PatchPlanVertexAdder
|
|
25
|
+
from infrahub.patch.vertex_deleter import PatchPlanVertexDeleter
|
|
26
|
+
from infrahub.patch.vertex_updater import PatchPlanVertexUpdater
|
|
27
|
+
|
|
28
|
+
from .constants import ERROR_BADGE, SUCCESS_BADGE
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from infrahub.cli.context import CliContext
|
|
32
|
+
from infrahub.database import InfrahubDatabase
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
patch_app = AsyncTyper(help="Commands for planning, applying, and reverting database patches")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_patch_runner(db: InfrahubDatabase) -> PatchRunner:
|
|
39
|
+
return PatchRunner(
|
|
40
|
+
plan_writer=PatchPlanWriter(),
|
|
41
|
+
plan_reader=PatchPlanReader(),
|
|
42
|
+
edge_db_id_translator=PatchPlanEdgeDbIdTranslator(),
|
|
43
|
+
vertex_adder=PatchPlanVertexAdder(db=db),
|
|
44
|
+
vertex_deleter=PatchPlanVertexDeleter(db=db),
|
|
45
|
+
vertex_updater=PatchPlanVertexUpdater(db=db),
|
|
46
|
+
edge_adder=PatchPlanEdgeAdder(db=db),
|
|
47
|
+
edge_deleter=PatchPlanEdgeDeleter(db=db),
|
|
48
|
+
edge_updater=PatchPlanEdgeUpdater(db=db),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@patch_app.command(name="plan")
|
|
53
|
+
async def plan_patch_cmd(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
patch_path: str = typer.Argument(
|
|
56
|
+
help="Path to the file containing the PatchQuery instance to run. Use Python-style dot paths, such as infrahub.cli.patch.queries.base"
|
|
57
|
+
),
|
|
58
|
+
patch_plans_dir: Path = typer.Option(Path("infrahub-patches"), help="Path to patch plans directory"), # noqa: B008
|
|
59
|
+
apply: bool = typer.Option(False, help="Apply the patch immediately after creating it"),
|
|
60
|
+
config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Create a plan for a given patch and save it in the patch plans directory to be applied/reverted"""
|
|
63
|
+
logging.getLogger("infrahub").setLevel(logging.WARNING)
|
|
64
|
+
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
|
65
|
+
logging.getLogger("prefect").setLevel(logging.ERROR)
|
|
66
|
+
|
|
67
|
+
patch_module = importlib.import_module(patch_path)
|
|
68
|
+
patch_query_class = None
|
|
69
|
+
patch_query_class_count = 0
|
|
70
|
+
for _, cls in inspect.getmembers(patch_module, inspect.isclass):
|
|
71
|
+
if issubclass(cls, PatchQuery) and cls is not PatchQuery:
|
|
72
|
+
patch_query_class = cls
|
|
73
|
+
patch_query_class_count += 1
|
|
74
|
+
|
|
75
|
+
patch_query_path = f"{PatchQuery.__module__}.{PatchQuery.__name__}"
|
|
76
|
+
if patch_query_class is None:
|
|
77
|
+
rprint(f"{ERROR_BADGE} No subclass of {patch_query_path} found in {patch_path}")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
if patch_query_class_count > 1:
|
|
80
|
+
rprint(
|
|
81
|
+
f"{ERROR_BADGE} Multiple subclasses of {patch_query_path} found in {patch_path}. Please only define one per file."
|
|
82
|
+
)
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
config.load_and_exit(config_file_name=config_file)
|
|
86
|
+
|
|
87
|
+
context: CliContext = ctx.obj
|
|
88
|
+
dbdriver = await context.init_db(retry=1)
|
|
89
|
+
|
|
90
|
+
patch_query_instance = patch_query_class(db=dbdriver)
|
|
91
|
+
async with dbdriver.start_session() as db:
|
|
92
|
+
patch_runner = get_patch_runner(db=db)
|
|
93
|
+
patch_plan_dir = await patch_runner.prepare_plan(patch_query_instance, directory=Path(patch_plans_dir))
|
|
94
|
+
rprint(f"{SUCCESS_BADGE} Patch plan created at {patch_plan_dir}")
|
|
95
|
+
if apply:
|
|
96
|
+
await patch_runner.apply(patch_plan_directory=patch_plan_dir)
|
|
97
|
+
rprint(f"{SUCCESS_BADGE} Patch plan successfully applied")
|
|
98
|
+
|
|
99
|
+
await dbdriver.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@patch_app.command(name="apply")
|
|
103
|
+
async def apply_patch_cmd(
|
|
104
|
+
ctx: typer.Context,
|
|
105
|
+
patch_plan_dir: Path = typer.Argument(help="Path to the directory containing a patch plan"),
|
|
106
|
+
config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Apply a given patch plan"""
|
|
109
|
+
logging.getLogger("infrahub").setLevel(logging.WARNING)
|
|
110
|
+
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
|
111
|
+
logging.getLogger("prefect").setLevel(logging.ERROR)
|
|
112
|
+
|
|
113
|
+
config.load_and_exit(config_file_name=config_file)
|
|
114
|
+
|
|
115
|
+
context: CliContext = ctx.obj
|
|
116
|
+
dbdriver = await context.init_db(retry=1)
|
|
117
|
+
|
|
118
|
+
if not patch_plan_dir.exists() or not patch_plan_dir.is_dir():
|
|
119
|
+
rprint(f"{ERROR_BADGE} patch_plan_dir must be an existing directory")
|
|
120
|
+
raise typer.Exit(1)
|
|
121
|
+
|
|
122
|
+
async with dbdriver.start_session() as db:
|
|
123
|
+
patch_runner = get_patch_runner(db=db)
|
|
124
|
+
await patch_runner.apply(patch_plan_directory=patch_plan_dir)
|
|
125
|
+
rprint(f"{SUCCESS_BADGE} Patch plan successfully applied")
|
|
126
|
+
|
|
127
|
+
await dbdriver.close()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@patch_app.command(name="revert")
|
|
131
|
+
async def revert_patch_cmd(
|
|
132
|
+
ctx: typer.Context,
|
|
133
|
+
patch_plan_dir: Path = typer.Argument(help="Path to the directory containing a patch plan"),
|
|
134
|
+
config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Revert a given patch plan"""
|
|
137
|
+
logging.getLogger("infrahub").setLevel(logging.WARNING)
|
|
138
|
+
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
|
139
|
+
logging.getLogger("prefect").setLevel(logging.ERROR)
|
|
140
|
+
config.load_and_exit(config_file_name=config_file)
|
|
141
|
+
|
|
142
|
+
context: CliContext = ctx.obj
|
|
143
|
+
db = await context.init_db(retry=1)
|
|
144
|
+
|
|
145
|
+
if not patch_plan_dir.exists() or not patch_plan_dir.is_dir():
|
|
146
|
+
rprint(f"{ERROR_BADGE} patch_plan_dir must be an existing directory")
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
|
|
149
|
+
patch_runner = get_patch_runner(db=db)
|
|
150
|
+
await patch_runner.revert(patch_plan_directory=patch_plan_dir)
|
|
151
|
+
rprint(f"{SUCCESS_BADGE} Patch plan successfully reverted")
|
|
152
|
+
|
|
153
|
+
await db.close()
|
|
@@ -2,13 +2,16 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
+
from infrahub_sdk.graphql import Query
|
|
7
8
|
from prefect.events.schemas.automations import Automation # noqa: TC002
|
|
8
9
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
9
10
|
from typing_extensions import Self
|
|
10
11
|
|
|
11
12
|
from infrahub.core import registry
|
|
13
|
+
from infrahub.core.constants import RelationshipCardinality
|
|
14
|
+
from infrahub.core.schema import AttributeSchema, NodeSchema # noqa: TC001
|
|
12
15
|
from infrahub.core.schema.schema_branch_computed import ( # noqa: TC001
|
|
13
16
|
ComputedAttributeTarget,
|
|
14
17
|
ComputedAttributeTriggerNode,
|
|
@@ -309,3 +312,80 @@ class ComputedAttrPythonQueryTriggerDefinition(TriggerBranchDefinition):
|
|
|
309
312
|
)
|
|
310
313
|
|
|
311
314
|
return definition
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class ComputedAttrJinja2GraphQLResponse(BaseModel):
|
|
318
|
+
node_id: str
|
|
319
|
+
computed_attribute_value: str | None
|
|
320
|
+
variables: dict[str, Any] = Field(default_factory=dict)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ComputedAttrJinja2GraphQL(BaseModel):
|
|
324
|
+
node_schema: NodeSchema = Field(..., description="The node kind where the computed attribute is defined")
|
|
325
|
+
attribute_schema: AttributeSchema = Field(..., description="The computed attribute")
|
|
326
|
+
variables: list[str] = Field(..., description="The list of variable names used within the computed attribute")
|
|
327
|
+
|
|
328
|
+
def render_graphql_query(self, query_filter: str, filter_id: str) -> str:
|
|
329
|
+
query_fields = self.query_fields
|
|
330
|
+
query_fields["id"] = None
|
|
331
|
+
query_fields[self.attribute_schema.name] = {"value": None}
|
|
332
|
+
query = Query(
|
|
333
|
+
name="ComputedAttributeFilter",
|
|
334
|
+
query={
|
|
335
|
+
self.node_schema.kind: {
|
|
336
|
+
"@filters": {query_filter: filter_id},
|
|
337
|
+
"edges": {"node": query_fields},
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return query.render()
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def query_fields(self) -> dict[str, Any]:
|
|
346
|
+
output: dict[str, Any] = {}
|
|
347
|
+
for variable in self.variables:
|
|
348
|
+
field_name, remainder = variable.split("__", maxsplit=1)
|
|
349
|
+
if field_name in self.node_schema.attribute_names:
|
|
350
|
+
output[field_name] = {remainder: None}
|
|
351
|
+
elif field_name in self.node_schema.relationship_names:
|
|
352
|
+
related_attribute, related_value = remainder.split("__", maxsplit=1)
|
|
353
|
+
relationship = self.node_schema.get_relationship(name=field_name)
|
|
354
|
+
if relationship.cardinality == RelationshipCardinality.ONE:
|
|
355
|
+
if field_name not in output:
|
|
356
|
+
output[field_name] = {"node": {}}
|
|
357
|
+
output[field_name]["node"][related_attribute] = {related_value: None}
|
|
358
|
+
return output
|
|
359
|
+
|
|
360
|
+
def parse_response(self, response: dict[str, Any]) -> list[ComputedAttrJinja2GraphQLResponse]:
|
|
361
|
+
rendered_response: list[ComputedAttrJinja2GraphQLResponse] = []
|
|
362
|
+
if kind_payload := response.get(self.node_schema.kind):
|
|
363
|
+
edges = kind_payload.get("edges", [])
|
|
364
|
+
for node in edges:
|
|
365
|
+
if node_response := self.to_node_response(node_dict=node):
|
|
366
|
+
rendered_response.append(node_response)
|
|
367
|
+
return rendered_response
|
|
368
|
+
|
|
369
|
+
def to_node_response(self, node_dict: dict[str, Any]) -> ComputedAttrJinja2GraphQLResponse | None:
|
|
370
|
+
if node := node_dict.get("node"):
|
|
371
|
+
node_id = node.get("id")
|
|
372
|
+
else:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
computed_attribute = node.get(self.attribute_schema.name, {}).get("value")
|
|
376
|
+
response = ComputedAttrJinja2GraphQLResponse(node_id=node_id, computed_attribute_value=computed_attribute)
|
|
377
|
+
for variable in self.variables:
|
|
378
|
+
field_name, remainder = variable.split("__", maxsplit=1)
|
|
379
|
+
response.variables[variable] = None
|
|
380
|
+
if field_content := node.get(field_name):
|
|
381
|
+
if field_name in self.node_schema.attribute_names:
|
|
382
|
+
response.variables[variable] = field_content.get(remainder)
|
|
383
|
+
elif field_name in self.node_schema.relationship_names:
|
|
384
|
+
relationship = self.node_schema.get_relationship(name=field_name)
|
|
385
|
+
if relationship.cardinality == RelationshipCardinality.ONE:
|
|
386
|
+
related_attribute, related_value = remainder.split("__", maxsplit=1)
|
|
387
|
+
node_content = field_content.get("node") or {}
|
|
388
|
+
related_attribute_content = node_content.get(related_attribute) or {}
|
|
389
|
+
response.variables[variable] = related_attribute_content.get(related_value)
|
|
390
|
+
|
|
391
|
+
return response
|
|
@@ -2,10 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from infrahub_sdk.protocols import
|
|
6
|
-
CoreNode, # noqa: TC002
|
|
7
|
-
CoreTransformPython,
|
|
8
|
-
)
|
|
5
|
+
from infrahub_sdk.protocols import CoreTransformPython
|
|
9
6
|
from infrahub_sdk.template import Jinja2Template
|
|
10
7
|
from prefect import flow
|
|
11
8
|
from prefect.client.orchestration import get_client
|
|
@@ -28,9 +25,7 @@ from infrahub.workflows.catalogue import (
|
|
|
28
25
|
from infrahub.workflows.utils import add_tags, wait_for_schema_to_converge
|
|
29
26
|
|
|
30
27
|
from .gather import gather_trigger_computed_attribute_jinja2, gather_trigger_computed_attribute_python
|
|
31
|
-
from .models import
|
|
32
|
-
PythonTransformTarget,
|
|
33
|
-
)
|
|
28
|
+
from .models import ComputedAttrJinja2GraphQL, ComputedAttrJinja2GraphQLResponse, PythonTransformTarget
|
|
34
29
|
|
|
35
30
|
if TYPE_CHECKING:
|
|
36
31
|
from infrahub.core.schema.computed_attribute import ComputedAttribute
|
|
@@ -118,6 +113,7 @@ async def process_transform(
|
|
|
118
113
|
location=f"{transform.file_path.value}::{transform.class_name.value}",
|
|
119
114
|
data=data,
|
|
120
115
|
client=service.client,
|
|
116
|
+
convert_query_response=transform.convert_query_response.value,
|
|
121
117
|
) # type: ignore[misc]
|
|
122
118
|
|
|
123
119
|
await service.client.execute_graphql(
|
|
@@ -167,49 +163,33 @@ async def trigger_update_python_computed_attributes(
|
|
|
167
163
|
flow_run_name="Update value for computed attribute {attribute_name}",
|
|
168
164
|
)
|
|
169
165
|
async def update_computed_attribute_value_jinja2(
|
|
170
|
-
branch_name: str,
|
|
166
|
+
branch_name: str,
|
|
167
|
+
obj: ComputedAttrJinja2GraphQLResponse,
|
|
168
|
+
node_kind: str,
|
|
169
|
+
attribute_name: str,
|
|
170
|
+
template: Jinja2Template,
|
|
171
|
+
service: InfrahubServices,
|
|
171
172
|
) -> None:
|
|
172
173
|
log = get_run_logger()
|
|
173
174
|
|
|
174
|
-
await add_tags(branches=[branch_name], nodes=[obj.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
for variable in jinja_template.get_variables():
|
|
179
|
-
components = variable.split("__")
|
|
180
|
-
if len(components) == 2:
|
|
181
|
-
property_name = components[0]
|
|
182
|
-
property_value = components[1]
|
|
183
|
-
attribute_property = getattr(obj, property_name)
|
|
184
|
-
variables[variable] = getattr(attribute_property, property_value)
|
|
185
|
-
elif len(components) == 3:
|
|
186
|
-
relationship_name = components[0]
|
|
187
|
-
property_name = components[1]
|
|
188
|
-
property_value = components[2]
|
|
189
|
-
relationship = getattr(obj, relationship_name)
|
|
190
|
-
try:
|
|
191
|
-
attribute_property = getattr(relationship.peer, property_name)
|
|
192
|
-
variables[variable] = getattr(attribute_property, property_value)
|
|
193
|
-
except ValueError:
|
|
194
|
-
variables[variable] = ""
|
|
195
|
-
|
|
196
|
-
value = await jinja_template.render(variables=variables)
|
|
197
|
-
existing_value = getattr(obj, attribute_name).value
|
|
198
|
-
if value == existing_value:
|
|
175
|
+
await add_tags(branches=[branch_name], nodes=[obj.node_id], db_change=True)
|
|
176
|
+
|
|
177
|
+
value = await template.render(variables=obj.variables)
|
|
178
|
+
if value == obj.computed_attribute_value:
|
|
199
179
|
log.debug(f"Ignoring to update {obj} with existing value on {attribute_name}={value}")
|
|
200
180
|
return
|
|
201
181
|
|
|
202
182
|
await service.client.execute_graphql(
|
|
203
183
|
query=UPDATE_ATTRIBUTE,
|
|
204
184
|
variables={
|
|
205
|
-
"id": obj.
|
|
206
|
-
"kind":
|
|
185
|
+
"id": obj.node_id,
|
|
186
|
+
"kind": node_kind,
|
|
207
187
|
"attribute": attribute_name,
|
|
208
188
|
"value": value,
|
|
209
189
|
},
|
|
210
190
|
branch_name=branch_name,
|
|
211
191
|
)
|
|
212
|
-
log.info(f"Updating computed attribute {
|
|
192
|
+
log.info(f"Updating computed attribute {node_kind}.{attribute_name}='{value}' ({obj.node_id})")
|
|
213
193
|
|
|
214
194
|
|
|
215
195
|
@flow(
|
|
@@ -235,41 +215,43 @@ async def process_jinja2(
|
|
|
235
215
|
branch_name if branch_name in registry.get_altered_schema_branches() else registry.default_branch
|
|
236
216
|
)
|
|
237
217
|
schema_branch = registry.schema.get_schema_branch(name=target_branch_schema)
|
|
238
|
-
|
|
239
|
-
|
|
218
|
+
node_schema = schema_branch.get_node(name=computed_attribute_kind, duplicate=False)
|
|
240
219
|
computed_macros = [
|
|
241
220
|
attrib
|
|
242
221
|
for attrib in schema_branch.computed_attributes.get_impacted_jinja2_targets(kind=node_kind, updates=updates)
|
|
243
222
|
if attrib.kind == computed_attribute_kind and attrib.attribute.name == computed_attribute_name
|
|
244
223
|
]
|
|
245
224
|
for computed_macro in computed_macros:
|
|
246
|
-
found: list[
|
|
225
|
+
found: list[ComputedAttrJinja2GraphQLResponse] = []
|
|
226
|
+
template_string = "n/a"
|
|
227
|
+
if computed_macro.attribute.computed_attribute and computed_macro.attribute.computed_attribute.jinja2_template:
|
|
228
|
+
template_string = computed_macro.attribute.computed_attribute.jinja2_template
|
|
229
|
+
|
|
230
|
+
jinja_template = Jinja2Template(template=template_string)
|
|
231
|
+
variables = jinja_template.get_variables()
|
|
232
|
+
|
|
233
|
+
attribute_graphql = ComputedAttrJinja2GraphQL(
|
|
234
|
+
node_schema=node_schema, attribute_schema=computed_macro.attribute, variables=variables
|
|
235
|
+
)
|
|
236
|
+
|
|
247
237
|
for id_filter in computed_macro.node_filters:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
prefetch_relationships=True,
|
|
253
|
-
populate_store=True,
|
|
254
|
-
**filters,
|
|
255
|
-
)
|
|
256
|
-
found.extend(nodes)
|
|
238
|
+
query = attribute_graphql.render_graphql_query(query_filter=id_filter, filter_id=object_id)
|
|
239
|
+
response = await service.client.execute_graphql(query=query, branch_name=branch_name)
|
|
240
|
+
output = attribute_graphql.parse_response(response=response)
|
|
241
|
+
found.extend(output)
|
|
257
242
|
|
|
258
243
|
if not found:
|
|
259
244
|
log.debug("No nodes found that requires updates")
|
|
260
245
|
|
|
261
|
-
template_string = "n/a"
|
|
262
|
-
if computed_macro.attribute.computed_attribute and computed_macro.attribute.computed_attribute.jinja2_template:
|
|
263
|
-
template_string = computed_macro.attribute.computed_attribute.jinja2_template
|
|
264
|
-
|
|
265
246
|
batch = await service.client.create_batch()
|
|
266
247
|
for node in found:
|
|
267
248
|
batch.add(
|
|
268
249
|
task=update_computed_attribute_value_jinja2,
|
|
269
250
|
branch_name=branch_name,
|
|
270
251
|
obj=node,
|
|
252
|
+
node_kind=node_schema.kind,
|
|
271
253
|
attribute_name=computed_macro.attribute.name,
|
|
272
|
-
|
|
254
|
+
template=jinja_template,
|
|
273
255
|
service=service,
|
|
274
256
|
)
|
|
275
257
|
|
infrahub/config.py
CHANGED
|
@@ -23,7 +23,7 @@ from pydantic import (
|
|
|
23
23
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
24
24
|
from typing_extensions import Self
|
|
25
25
|
|
|
26
|
-
from infrahub.database
|
|
26
|
+
from infrahub.constants.database import DatabaseType
|
|
27
27
|
from infrahub.exceptions import InitializationError, ProcessingError
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
@@ -629,6 +629,7 @@ class AnalyticsSettings(BaseSettings):
|
|
|
629
629
|
class ExperimentalFeaturesSettings(BaseSettings):
|
|
630
630
|
model_config = SettingsConfigDict(env_prefix="INFRAHUB_EXPERIMENTAL_")
|
|
631
631
|
graphql_enums: bool = False
|
|
632
|
+
value_db_index: bool = False
|
|
632
633
|
|
|
633
634
|
|
|
634
635
|
class SecuritySettings(BaseSettings):
|
|
File without changes
|
|
@@ -150,6 +150,7 @@ class ContentType(InfrahubStringEnum):
|
|
|
150
150
|
APPLICATION_JSON = "application/json"
|
|
151
151
|
APPLICATION_YAML = "application/yaml"
|
|
152
152
|
APPLICATION_XML = "application/xml"
|
|
153
|
+
APPLICATION_HCL = "application/hcl"
|
|
153
154
|
TEXT_PLAIN = "text/plain"
|
|
154
155
|
TEXT_MARKDOWN = "text/markdown"
|
|
155
156
|
TEXT_CSV = "text/csv"
|
infrahub/core/graph/index.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from infrahub.database
|
|
3
|
+
from infrahub.constants.database import IndexType
|
|
4
4
|
from infrahub.database.index import IndexItem
|
|
5
5
|
|
|
6
6
|
node_indexes: list[IndexItem] = [
|
|
@@ -17,6 +17,8 @@ node_indexes: list[IndexItem] = [
|
|
|
17
17
|
IndexItem(name="diff_node_uuid", label="DiffNode", properties=["uuid"], type=IndexType.TEXT),
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
+
attr_value_index = IndexItem(name="attr_value", label="AttributeValue", properties=["value"], type=IndexType.RANGE)
|
|
21
|
+
|
|
20
22
|
rel_indexes: list[IndexItem] = [
|
|
21
23
|
IndexItem(name="attr_from", label="HAS_ATTRIBUTE", properties=["from"], type=IndexType.RANGE),
|
|
22
24
|
IndexItem(name="attr_branch", label="HAS_ATTRIBUTE", properties=["branch"], type=IndexType.RANGE),
|
infrahub/core/manager.py
CHANGED
|
@@ -1229,20 +1229,31 @@ class NodeManager:
|
|
|
1229
1229
|
if not prefetch_relationships and not fields:
|
|
1230
1230
|
return
|
|
1231
1231
|
cardinality_one_identifiers_by_kind: dict[str, dict[str, RelationshipDirection]] | None = None
|
|
1232
|
-
|
|
1232
|
+
outbound_identifiers: set[str] | None = None
|
|
1233
|
+
inbound_identifiers: set[str] | None = None
|
|
1234
|
+
bidirectional_identifiers: set[str] | None = None
|
|
1233
1235
|
if not prefetch_relationships:
|
|
1234
1236
|
cardinality_one_identifiers_by_kind = _get_cardinality_one_identifiers_by_kind(
|
|
1235
1237
|
nodes=nodes_by_id.values(), fields=fields or {}
|
|
1236
1238
|
)
|
|
1237
|
-
|
|
1239
|
+
outbound_identifiers = set()
|
|
1240
|
+
inbound_identifiers = set()
|
|
1241
|
+
bidirectional_identifiers = set()
|
|
1238
1242
|
for identifier_direction_map in cardinality_one_identifiers_by_kind.values():
|
|
1239
|
-
|
|
1240
|
-
|
|
1243
|
+
for identifier, direction in identifier_direction_map.items():
|
|
1244
|
+
if direction is RelationshipDirection.OUTBOUND:
|
|
1245
|
+
outbound_identifiers.add(identifier)
|
|
1246
|
+
elif direction is RelationshipDirection.INBOUND:
|
|
1247
|
+
inbound_identifiers.add(identifier)
|
|
1248
|
+
elif direction is RelationshipDirection.BIDIR:
|
|
1249
|
+
bidirectional_identifiers.add(identifier)
|
|
1241
1250
|
|
|
1242
1251
|
query = await NodeListGetRelationshipsQuery.init(
|
|
1243
1252
|
db=db,
|
|
1244
1253
|
ids=list(nodes_by_id.keys()),
|
|
1245
|
-
|
|
1254
|
+
outbound_identifiers=None if outbound_identifiers is None else list(outbound_identifiers),
|
|
1255
|
+
inbound_identifiers=None if inbound_identifiers is None else list(inbound_identifiers),
|
|
1256
|
+
bidirectional_identifiers=None if bidirectional_identifiers is None else list(bidirectional_identifiers),
|
|
1246
1257
|
branch=branch,
|
|
1247
1258
|
at=at,
|
|
1248
1259
|
branch_agnostic=branch_agnostic,
|
|
@@ -2,10 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Sequence
|
|
4
4
|
|
|
5
|
+
from infrahub.constants.database import IndexType
|
|
5
6
|
from infrahub.core.migrations.shared import MigrationResult
|
|
6
7
|
from infrahub.core.query import Query # noqa: TC001
|
|
7
8
|
from infrahub.database import DatabaseType
|
|
8
|
-
from infrahub.database.constants import IndexType
|
|
9
9
|
from infrahub.database.index import IndexItem
|
|
10
10
|
|
|
11
11
|
from ..shared import GraphMigration
|
|
@@ -29,13 +29,12 @@ class Migration014(GraphMigration):
|
|
|
29
29
|
if db.db_type != DatabaseType.NEO4J:
|
|
30
30
|
return result
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return result
|
|
32
|
+
try:
|
|
33
|
+
db.manager.index.init(nodes=[INDEX_TO_DELETE], rels=[])
|
|
34
|
+
await db.manager.index.drop()
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
result.errors.append(str(exc))
|
|
37
|
+
return result
|
|
39
38
|
|
|
40
39
|
return result
|
|
41
40
|
|
infrahub/core/node/__init__.py
CHANGED
|
@@ -29,6 +29,7 @@ from infrahub.types import ATTRIBUTE_TYPES
|
|
|
29
29
|
|
|
30
30
|
from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
|
|
31
31
|
from ...graphql.models import OrderModel
|
|
32
|
+
from ...log import get_logger
|
|
32
33
|
from ..query.relationship import RelationshipDeleteAllQuery
|
|
33
34
|
from ..relationship import RelationshipManager
|
|
34
35
|
from ..utils import update_relationships_to
|
|
@@ -53,6 +54,8 @@ SchemaProtocol = TypeVar("SchemaProtocol")
|
|
|
53
54
|
# -
|
|
54
55
|
# ---------------------------------------------------------------------------------------
|
|
55
56
|
|
|
57
|
+
log = get_logger()
|
|
58
|
+
|
|
56
59
|
|
|
57
60
|
class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
58
61
|
@classmethod
|
|
@@ -348,7 +351,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
348
351
|
fields.pop("updated_at")
|
|
349
352
|
for field_name in fields.keys():
|
|
350
353
|
if field_name not in self._schema.valid_input_names:
|
|
351
|
-
|
|
354
|
+
log.error(f"{field_name} is not a valid input for {self.get_kind()}")
|
|
352
355
|
|
|
353
356
|
# Backfill fields with the ones from the template if there's one
|
|
354
357
|
await self.handle_object_template(fields=fields, db=db, errors=errors)
|
infrahub/core/protocols.py
CHANGED
infrahub/core/query/ipam.py
CHANGED
|
@@ -367,9 +367,11 @@ class IPPrefixReconcileQuery(Query):
|
|
|
367
367
|
possible_prefix = tmp_prefix.ljust(self.ip_value.max_prefixlen, "0")
|
|
368
368
|
if possible_prefix not in possible_prefix_map:
|
|
369
369
|
possible_prefix_map[possible_prefix] = max_prefix_len
|
|
370
|
-
self.params["possible_prefix_and_length_list"] = [
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
self.params["possible_prefix_and_length_list"] = []
|
|
371
|
+
self.params["possible_prefix_list"] = []
|
|
372
|
+
for possible_prefix, max_length in possible_prefix_map.items():
|
|
373
|
+
self.params["possible_prefix_and_length_list"].append([possible_prefix, max_length])
|
|
374
|
+
self.params["possible_prefix_list"].append(possible_prefix)
|
|
373
375
|
|
|
374
376
|
namespace_query = """
|
|
375
377
|
// ------------------
|
|
@@ -386,8 +388,7 @@ class IPPrefixReconcileQuery(Query):
|
|
|
386
388
|
// ------------------
|
|
387
389
|
// Get IP Prefix node by UUID
|
|
388
390
|
// ------------------
|
|
389
|
-
MATCH (ip_node {uuid: $node_uuid})
|
|
390
|
-
WHERE "%(ip_kind)s" IN labels(ip_node)
|
|
391
|
+
MATCH (ip_node:%(ip_kind)s {uuid: $node_uuid})
|
|
391
392
|
""" % {
|
|
392
393
|
"ip_kind": InfrahubKind.IPADDRESS
|
|
393
394
|
if isinstance(self.ip_value, IPAddressType)
|
|
@@ -487,6 +488,7 @@ class IPPrefixReconcileQuery(Query):
|
|
|
487
488
|
-[hvr:HAS_VALUE]->(av:%(ip_prefix_attribute_kind)s)
|
|
488
489
|
WHERE all(r IN relationships(parent_path) WHERE (%(branch_filter)s))
|
|
489
490
|
AND av.version = $ip_version
|
|
491
|
+
AND av.binary_address IN $possible_prefix_list
|
|
490
492
|
AND any(prefix_and_length IN $possible_prefix_and_length_list WHERE av.binary_address = prefix_and_length[0] AND av.prefixlen <= prefix_and_length[1])
|
|
491
493
|
WITH
|
|
492
494
|
maybe_new_parent,
|