infrahub-server 1.2.1__py3-none-any.whl → 1.2.3__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/computed_attribute/tasks.py +71 -67
- infrahub/config.py +3 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/migrations/graph/__init__.py +4 -1
- infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +69 -0
- infrahub/core/models.py +6 -0
- infrahub/core/node/__init__.py +4 -4
- infrahub/core/node/constraints/grouped_uniqueness.py +24 -9
- infrahub/core/query/ipam.py +1 -1
- infrahub/core/query/node.py +16 -5
- infrahub/core/schema/schema_branch.py +14 -5
- infrahub/exceptions.py +30 -2
- infrahub/git/base.py +80 -29
- infrahub/git/integrator.py +9 -31
- infrahub/menu/repository.py +6 -6
- infrahub/trigger/tasks.py +19 -18
- infrahub/workflows/utils.py +5 -5
- infrahub_sdk/client.py +6 -6
- infrahub_sdk/ctl/cli_commands.py +32 -37
- infrahub_sdk/ctl/render.py +39 -0
- infrahub_sdk/exceptions.py +6 -2
- infrahub_sdk/generator.py +1 -1
- infrahub_sdk/node.py +41 -12
- infrahub_sdk/protocols_base.py +8 -1
- infrahub_sdk/pytest_plugin/items/jinja2_transform.py +22 -26
- infrahub_sdk/store.py +351 -75
- infrahub_sdk/template/__init__.py +209 -0
- infrahub_sdk/template/exceptions.py +38 -0
- infrahub_sdk/template/filters.py +151 -0
- infrahub_sdk/template/models.py +10 -0
- infrahub_sdk/utils.py +7 -0
- {infrahub_server-1.2.1.dist-info → infrahub_server-1.2.3.dist-info}/METADATA +2 -1
- {infrahub_server-1.2.1.dist-info → infrahub_server-1.2.3.dist-info}/RECORD +39 -36
- infrahub_testcontainers/container.py +2 -0
- infrahub_testcontainers/docker-compose.test.yml +1 -0
- infrahub_testcontainers/haproxy.cfg +3 -3
- infrahub/support/__init__.py +0 -0
- infrahub/support/macro.py +0 -69
- {infrahub_server-1.2.1.dist-info → infrahub_server-1.2.3.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.1.dist-info → infrahub_server-1.2.3.dist-info}/WHEEL +0 -0
- {infrahub_server-1.2.1.dist-info → infrahub_server-1.2.3.dist-info}/entry_points.txt +0 -0
infrahub_sdk/node.py
CHANGED
|
@@ -15,7 +15,7 @@ from .exceptions import (
|
|
|
15
15
|
)
|
|
16
16
|
from .graphql import Mutation, Query
|
|
17
17
|
from .schema import GenericSchemaAPI, RelationshipCardinality, RelationshipKind
|
|
18
|
-
from .utils import compare_lists, get_flat_value
|
|
18
|
+
from .utils import compare_lists, generate_short_id, get_flat_value
|
|
19
19
|
from .uuidt import UUIDT
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
@@ -43,6 +43,20 @@ ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
|
|
|
43
43
|
"calling generate is only supported for CoreArtifactDefinition nodes"
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
+
HFID_STR_SEPARATOR = "__"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_human_friendly_id(hfid: str | list[str]) -> tuple[str | None, list[str]]:
|
|
50
|
+
"""Parse a human friendly ID into a kind and an identifier."""
|
|
51
|
+
if isinstance(hfid, str):
|
|
52
|
+
hfid_parts = hfid.split(HFID_STR_SEPARATOR)
|
|
53
|
+
if len(hfid_parts) == 1:
|
|
54
|
+
return None, hfid_parts
|
|
55
|
+
return hfid_parts[0], hfid_parts[1:]
|
|
56
|
+
if isinstance(hfid, list):
|
|
57
|
+
return None, hfid
|
|
58
|
+
raise ValueError(f"Invalid human friendly ID: {hfid}")
|
|
59
|
+
|
|
46
60
|
|
|
47
61
|
class Attribute:
|
|
48
62
|
"""Represents an attribute of a Node, including its schema, value, and properties."""
|
|
@@ -340,10 +354,10 @@ class RelatedNode(RelatedNodeBase):
|
|
|
340
354
|
return self._peer # type: ignore[return-value]
|
|
341
355
|
|
|
342
356
|
if self.id and self.typename:
|
|
343
|
-
return self._client.store.get(key=self.id, kind=self.typename) # type: ignore[return-value]
|
|
357
|
+
return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
|
|
344
358
|
|
|
345
359
|
if self.hfid_str:
|
|
346
|
-
return self._client.store.
|
|
360
|
+
return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
|
|
347
361
|
|
|
348
362
|
raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
|
|
349
363
|
|
|
@@ -387,10 +401,10 @@ class RelatedNodeSync(RelatedNodeBase):
|
|
|
387
401
|
return self._peer # type: ignore[return-value]
|
|
388
402
|
|
|
389
403
|
if self.id and self.typename:
|
|
390
|
-
return self._client.store.get(key=self.id, kind=self.typename) # type: ignore[return-value]
|
|
404
|
+
return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
|
|
391
405
|
|
|
392
406
|
if self.hfid_str:
|
|
393
|
-
return self._client.store.
|
|
407
|
+
return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
|
|
394
408
|
|
|
395
409
|
raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
|
|
396
410
|
|
|
@@ -678,6 +692,11 @@ class InfrahubNodeBase:
|
|
|
678
692
|
self._branch = branch
|
|
679
693
|
self._existing: bool = True
|
|
680
694
|
|
|
695
|
+
# Generate a unique ID only to be used inside the SDK
|
|
696
|
+
# The format if this ID is purposely different from the ID used by the API
|
|
697
|
+
# This is done to avoid confusion and potential conflicts between the IDs
|
|
698
|
+
self._internal_id = generate_short_id()
|
|
699
|
+
|
|
681
700
|
self.id = data.get("id", None) if isinstance(data, dict) else None
|
|
682
701
|
self.display_label: str | None = data.get("display_label", None) if isinstance(data, dict) else None
|
|
683
702
|
self.typename: str | None = data.get("__typename", schema.kind) if isinstance(data, dict) else schema.kind
|
|
@@ -694,6 +713,9 @@ class InfrahubNodeBase:
|
|
|
694
713
|
self._init_attributes(data)
|
|
695
714
|
self._init_relationships(data)
|
|
696
715
|
|
|
716
|
+
def get_branch(self) -> str:
|
|
717
|
+
return self._branch
|
|
718
|
+
|
|
697
719
|
def get_path_value(self, path: str) -> Any:
|
|
698
720
|
path_parts = path.split("__")
|
|
699
721
|
return_value = None
|
|
@@ -794,6 +816,11 @@ class InfrahubNodeBase:
|
|
|
794
816
|
def get_kind(self) -> str:
|
|
795
817
|
return self._schema.kind
|
|
796
818
|
|
|
819
|
+
def get_all_kinds(self) -> list[str]:
|
|
820
|
+
if hasattr(self._schema, "inherit_from"):
|
|
821
|
+
return [self._schema.kind] + self._schema.inherit_from
|
|
822
|
+
return [self._schema.kind]
|
|
823
|
+
|
|
797
824
|
def is_ip_prefix(self) -> bool:
|
|
798
825
|
builtin_ipprefix_kind = "BuiltinIPPrefix"
|
|
799
826
|
return self.get_kind() == builtin_ipprefix_kind or builtin_ipprefix_kind in self._schema.inherit_from # type: ignore[union-attr]
|
|
@@ -1201,7 +1228,7 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
1201
1228
|
else:
|
|
1202
1229
|
await self._client.group_context.add_related_nodes(ids=[self.id], update_group_context=update_group_context)
|
|
1203
1230
|
|
|
1204
|
-
self._client.store.set(
|
|
1231
|
+
self._client.store.set(node=self)
|
|
1205
1232
|
|
|
1206
1233
|
async def generate_query_data(
|
|
1207
1234
|
self,
|
|
@@ -1418,8 +1445,10 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
1418
1445
|
) -> None:
|
|
1419
1446
|
mutation_query = self._generate_mutation_query()
|
|
1420
1447
|
|
|
1448
|
+
# Upserting means we may want to create, meaning payload contains all mandatory fields required for a creation,
|
|
1449
|
+
# so hfid is just redondant information. Currently, upsert mutation has performance overhead if `hfid` is filled.
|
|
1421
1450
|
if allow_upsert:
|
|
1422
|
-
input_data = self._generate_input_data(exclude_hfid=
|
|
1451
|
+
input_data = self._generate_input_data(exclude_hfid=True, request_context=request_context)
|
|
1423
1452
|
mutation_name = f"{self._schema.kind}Upsert"
|
|
1424
1453
|
tracker = f"mutation-{str(self._schema.kind).lower()}-upsert"
|
|
1425
1454
|
else:
|
|
@@ -1477,15 +1506,15 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
1477
1506
|
for rel_name in self._relationships:
|
|
1478
1507
|
rel = getattr(self, rel_name)
|
|
1479
1508
|
if rel and isinstance(rel, RelatedNode):
|
|
1480
|
-
relation = node_data["node"].get(rel_name)
|
|
1481
|
-
if relation.get("node", None):
|
|
1509
|
+
relation = node_data["node"].get(rel_name, None)
|
|
1510
|
+
if relation and relation.get("node", None):
|
|
1482
1511
|
related_node = await InfrahubNode.from_graphql(
|
|
1483
1512
|
client=self._client, branch=branch, data=relation, timeout=timeout
|
|
1484
1513
|
)
|
|
1485
1514
|
related_nodes.append(related_node)
|
|
1486
1515
|
elif rel and isinstance(rel, RelationshipManager):
|
|
1487
|
-
peers = node_data["node"].get(rel_name)
|
|
1488
|
-
if peers:
|
|
1516
|
+
peers = node_data["node"].get(rel_name, None)
|
|
1517
|
+
if peers and peers["edges"]:
|
|
1489
1518
|
for peer in peers["edges"]:
|
|
1490
1519
|
related_node = await InfrahubNode.from_graphql(
|
|
1491
1520
|
client=self._client, branch=branch, data=peer, timeout=timeout
|
|
@@ -1724,7 +1753,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1724
1753
|
else:
|
|
1725
1754
|
self._client.group_context.add_related_nodes(ids=[self.id], update_group_context=update_group_context)
|
|
1726
1755
|
|
|
1727
|
-
self._client.store.set(
|
|
1756
|
+
self._client.store.set(node=self)
|
|
1728
1757
|
|
|
1729
1758
|
def generate_query_data(
|
|
1730
1759
|
self,
|
infrahub_sdk/protocols_base.py
CHANGED
|
@@ -144,7 +144,8 @@ class AnyAttributeOptional(Attribute):
|
|
|
144
144
|
@runtime_checkable
|
|
145
145
|
class CoreNodeBase(Protocol):
|
|
146
146
|
_schema: MainSchemaTypes
|
|
147
|
-
|
|
147
|
+
_internal_id: str
|
|
148
|
+
id: str # NOTE this is incorrect, should be str | None
|
|
148
149
|
display_label: str | None
|
|
149
150
|
|
|
150
151
|
@property
|
|
@@ -153,10 +154,16 @@ class CoreNodeBase(Protocol):
|
|
|
153
154
|
@property
|
|
154
155
|
def hfid_str(self) -> str | None: ...
|
|
155
156
|
|
|
157
|
+
def get_human_friendly_id(self) -> list[str] | None: ...
|
|
158
|
+
|
|
156
159
|
def get_human_friendly_id_as_string(self, include_kind: bool = False) -> str | None: ...
|
|
157
160
|
|
|
158
161
|
def get_kind(self) -> str: ...
|
|
159
162
|
|
|
163
|
+
def get_all_kinds(self) -> list[str]: ...
|
|
164
|
+
|
|
165
|
+
def get_branch(self) -> str: ...
|
|
166
|
+
|
|
160
167
|
def is_ip_prefix(self) -> bool: ...
|
|
161
168
|
|
|
162
169
|
def is_ip_address(self) -> bool: ...
|
|
@@ -1,51 +1,47 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import difflib
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from typing import TYPE_CHECKING, Any
|
|
5
7
|
|
|
6
8
|
import jinja2
|
|
7
9
|
import ujson
|
|
8
10
|
from httpx import HTTPStatusError
|
|
9
|
-
from rich.console import Console
|
|
10
|
-
from rich.traceback import Traceback
|
|
11
11
|
|
|
12
|
-
from ...
|
|
13
|
-
from
|
|
12
|
+
from ...template import Jinja2Template
|
|
13
|
+
from ...template.exceptions import JinjaTemplateError
|
|
14
|
+
from ..exceptions import OutputMatchError
|
|
14
15
|
from ..models import InfrahubInputOutputTest, InfrahubTestExpectedResult
|
|
15
16
|
from .base import InfrahubItem
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
19
|
from pytest import ExceptionInfo
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class InfrahubJinja2Item(InfrahubItem):
|
|
23
|
+
def _get_jinja2(self) -> Jinja2Template:
|
|
24
|
+
return Jinja2Template(
|
|
25
|
+
template=Path(self.resource_config.template_path), # type: ignore[attr-defined]
|
|
26
|
+
template_directory=Path(self.session.infrahub_config_path.parent), # type: ignore[attr-defined]
|
|
27
|
+
)
|
|
28
|
+
|
|
24
29
|
def get_jinja2_environment(self) -> jinja2.Environment:
|
|
25
|
-
|
|
26
|
-
return
|
|
30
|
+
jinja2_template = self._get_jinja2()
|
|
31
|
+
return jinja2_template.get_environment()
|
|
27
32
|
|
|
28
33
|
def get_jinja2_template(self) -> jinja2.Template:
|
|
29
|
-
|
|
34
|
+
jinja2_template = self._get_jinja2()
|
|
35
|
+
return jinja2_template.get_template()
|
|
30
36
|
|
|
31
37
|
def render_jinja2_template(self, variables: dict[str, Any]) -> str | None:
|
|
38
|
+
jinja2_template = self._get_jinja2()
|
|
39
|
+
|
|
32
40
|
try:
|
|
33
|
-
return
|
|
34
|
-
except
|
|
35
|
-
traceback = Traceback(show_locals=False)
|
|
36
|
-
errors = identify_faulty_jinja_code(traceback=traceback)
|
|
37
|
-
console = Console()
|
|
38
|
-
with console.capture() as capture:
|
|
39
|
-
console.print(f"An error occurred while rendering Jinja2 transform:{self.name!r}\n", soft_wrap=True)
|
|
40
|
-
console.print(f"{exc.message}\n", soft_wrap=True)
|
|
41
|
-
for frame, syntax in errors:
|
|
42
|
-
console.print(f"{frame.filename} on line {frame.lineno}\n", soft_wrap=True)
|
|
43
|
-
console.print(syntax, soft_wrap=True)
|
|
44
|
-
str_output = capture.get()
|
|
41
|
+
return asyncio.run(jinja2_template.render(variables=variables))
|
|
42
|
+
except JinjaTemplateError as exc:
|
|
45
43
|
if self.test.expect == InfrahubTestExpectedResult.PASS:
|
|
46
|
-
raise
|
|
47
|
-
name=self.name, message=str_output, rtb=traceback, errors=errors
|
|
48
|
-
) from exc
|
|
44
|
+
raise exc
|
|
49
45
|
return None
|
|
50
46
|
|
|
51
47
|
def get_result_differences(self, computed: Any) -> str | None:
|
|
@@ -99,8 +95,8 @@ class InfrahubJinja2TransformUnitRenderItem(InfrahubJinja2Item):
|
|
|
99
95
|
raise OutputMatchError(name=self.name, differences=differences)
|
|
100
96
|
|
|
101
97
|
def repr_failure(self, excinfo: ExceptionInfo, style: str | None = None) -> str:
|
|
102
|
-
if isinstance(excinfo.value, (
|
|
103
|
-
return excinfo.value.message
|
|
98
|
+
if isinstance(excinfo.value, (JinjaTemplateError)):
|
|
99
|
+
return str(excinfo.value.message)
|
|
104
100
|
|
|
105
101
|
return super().repr_failure(excinfo, style=style)
|
|
106
102
|
|