Skip to content

Commit 21d3c54

Browse files
authored
Merge pull request #6450 from opsmill/stable
Merge stable into develop
2 parents 71ac078 + 9b51d87 commit 21d3c54

File tree

10 files changed

+222
-36
lines changed

10 files changed

+222
-36
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ support_logs_*
2727
docs/build
2828

2929
storage/*
30+
sandbox/*
3031
.coverage.*
3132
python_sdk/dist/*
3233
.benchmarks/*

backend/infrahub/computed_attribute/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ class PythonTransformTarget:
118118
class ComputedAttrJinja2TriggerDefinition(TriggerBranchDefinition):
119119
type: TriggerType = TriggerType.COMPUTED_ATTR_JINJA2
120120
computed_attribute: ComputedAttributeTarget
121+
template_hash: str
122+
123+
def get_description(self) -> str:
124+
return f"{super().get_description()} | hash:{self.template_hash}"
121125

122126
@classmethod
123127
def from_computed_attribute(
@@ -139,6 +143,14 @@ def from_computed_attribute(
139143
# node creation events if the attribute is optional.
140144
event_trigger.events.add(NodeCreatedEvent.event_name)
141145

146+
if (
147+
computed_attribute.attribute.computed_attribute
148+
and computed_attribute.attribute.computed_attribute.jinja2_template is None
149+
) or not computed_attribute.attribute.computed_attribute:
150+
raise ValueError("Jinja2 template is required for computed attribute")
151+
152+
template_hash = computed_attribute.attribute.computed_attribute.get_hash()
153+
142154
event_trigger.match = {"infrahub.node.kind": trigger_node.kind}
143155
if branches_out_of_scope:
144156
event_trigger.match["infrahub.branch.name"] = [f"!{branch}" for branch in branches_out_of_scope]
@@ -177,6 +189,7 @@ def from_computed_attribute(
177189

178190
definition = cls(
179191
name=f"{computed_attribute.key_name}{NAME_SEPARATOR}kind{NAME_SEPARATOR}{trigger_node.kind}",
192+
template_hash=template_hash,
180193
branch=branch,
181194
computed_attribute=computed_attribute,
182195
trigger=event_trigger,

backend/infrahub/computed_attribute/tasks.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from infrahub.events import BranchDeletedEvent
1515
from infrahub.git.repository import get_initialized_repo
1616
from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
17-
from infrahub.trigger.models import TriggerType
17+
from infrahub.trigger.models import TriggerSetupReport, TriggerType
1818
from infrahub.trigger.setup import setup_triggers
1919
from infrahub.workflows.catalogue import (
2020
COMPUTED_ATTRIBUTE_PROCESS_JINJA2,
@@ -25,7 +25,11 @@
2525
from infrahub.workflows.utils import add_tags, wait_for_schema_to_converge
2626

2727
from .gather import gather_trigger_computed_attribute_jinja2, gather_trigger_computed_attribute_python
28-
from .models import ComputedAttrJinja2GraphQL, ComputedAttrJinja2GraphQLResponse, PythonTransformTarget
28+
from .models import (
29+
ComputedAttrJinja2GraphQL,
30+
ComputedAttrJinja2GraphQLResponse,
31+
PythonTransformTarget,
32+
)
2933

3034
if TYPE_CHECKING:
3135
from infrahub.core.schema.computed_attribute import ComputedAttribute
@@ -159,10 +163,10 @@ async def trigger_update_python_computed_attributes(
159163

160164

161165
@flow(
162-
name="process_computed_attribute_value_jinja2",
163-
flow_run_name="Update value for computed attribute {attribute_name}",
166+
name="computed-attribute-jinja2-update-value",
167+
flow_run_name="Update value for computed attribute {node_kind}:{attribute_name}",
164168
)
165-
async def update_computed_attribute_value_jinja2(
169+
async def computed_attribute_jinja2_update_value(
166170
branch_name: str,
167171
obj: ComputedAttrJinja2GraphQLResponse,
168172
node_kind: str,
@@ -246,7 +250,7 @@ async def process_jinja2(
246250
batch = await service.client.create_batch()
247251
for node in found:
248252
batch.add(
249-
task=update_computed_attribute_value_jinja2,
253+
task=computed_attribute_jinja2_update_value,
250254
branch_name=branch_name,
251255
obj=node,
252256
node_kind=node_schema.kind,
@@ -302,36 +306,33 @@ async def computed_attribute_setup_jinja2(
302306

303307
triggers = await gather_trigger_computed_attribute_jinja2()
304308

305-
# Since we can have multiple trigger per NodeKind
306-
# we need to extract the list of unique node that should be processed
307-
# also
308-
# Because the automation in Prefect doesn't capture all information about the computed attribute
309-
# we can't tell right now if a given computed attribute has changed and need to be updated
310-
unique_nodes: set[tuple[str, str, str]] = {
311-
(trigger.branch, trigger.computed_attribute.kind, trigger.computed_attribute.attribute.name)
312-
for trigger in triggers
313-
}
314-
for branch, kind, attribute_name in unique_nodes:
315-
if event_name != BranchDeletedEvent.event_name and branch == branch_name:
316-
await service.workflow.submit_workflow(
317-
workflow=TRIGGER_UPDATE_JINJA_COMPUTED_ATTRIBUTES,
318-
context=context,
319-
parameters={
320-
"branch_name": branch,
321-
"computed_attribute_name": attribute_name,
322-
"computed_attribute_kind": kind,
323-
},
324-
)
325-
326309
# Configure all ComputedAttrJinja2Trigger in Prefect
327310
async with get_client(sync_client=False) as prefect_client:
328-
await setup_triggers(
311+
report: TriggerSetupReport = await setup_triggers(
329312
client=prefect_client,
330313
triggers=triggers,
331314
trigger_type=TriggerType.COMPUTED_ATTR_JINJA2,
332315
force_update=False,
333316
) # type: ignore[misc]
334317

318+
# Since we can have multiple trigger per NodeKind
319+
# we need to extract the list of unique node that should be processed
320+
unique_nodes: set[tuple[str, str, str]] = {
321+
(trigger.branch, trigger.computed_attribute.kind, trigger.computed_attribute.attribute.name) # type: ignore[attr-defined]
322+
for trigger in report.updated + report.created
323+
}
324+
for branch, kind, attribute_name in unique_nodes:
325+
if event_name != BranchDeletedEvent.event_name and branch == branch_name:
326+
await service.workflow.submit_workflow(
327+
workflow=TRIGGER_UPDATE_JINJA_COMPUTED_ATTRIBUTES,
328+
context=context,
329+
parameters={
330+
"branch_name": branch,
331+
"computed_attribute_name": attribute_name,
332+
"computed_attribute_kind": kind,
333+
},
334+
)
335+
335336
log.info(f"{len(triggers)} Computed Attribute for Jinja2 automation configuration completed")
336337

337338

backend/infrahub/core/attribute.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,10 @@ def label(self) -> str:
782782

783783
return ""
784784

785+
@staticmethod
786+
def get_allowed_property_in_path() -> list[str]:
787+
return ["color", "description", "label", "value"]
788+
785789
@classmethod
786790
def validate_content(cls, value: Any, name: str, schema: AttributeSchema) -> None:
787791
"""Validate the content of the dropdown."""
@@ -817,7 +821,18 @@ class IPNetwork(BaseAttribute):
817821

818822
@staticmethod
819823
def get_allowed_property_in_path() -> list[str]:
820-
return ["value", "version", "binary_address", "prefixlen"]
824+
return [
825+
"binary_address",
826+
"broadcast_address",
827+
"hostmask",
828+
"netmask",
829+
"num_addresses",
830+
"prefixlen",
831+
"value",
832+
"version",
833+
"with_hostmask",
834+
"with_netmask",
835+
]
821836

822837
@property
823838
def obj(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network:
@@ -950,7 +965,17 @@ class IPHost(BaseAttribute):
950965

951966
@staticmethod
952967
def get_allowed_property_in_path() -> list[str]:
953-
return ["value", "version", "binary_address"]
968+
return [
969+
"binary_address",
970+
"hostmask",
971+
"ip",
972+
"netmask",
973+
"prefixlen",
974+
"value",
975+
"version",
976+
"with_hostmask",
977+
"with_netmask",
978+
]
954979

955980
@property
956981
def obj(self) -> ipaddress.IPv4Interface | ipaddress.IPv6Interface:
@@ -1170,6 +1195,22 @@ def serialize_value(self) -> str:
11701195
"""Serialize the value as standard EUI-48 or EUI-64 before storing it in the database."""
11711196
return str(netaddr.EUI(addr=self.value))
11721197

1198+
@staticmethod
1199+
def get_allowed_property_in_path() -> list[str]:
1200+
return [
1201+
"bare",
1202+
"binary",
1203+
"dot_notation",
1204+
"ei",
1205+
"eui48",
1206+
"eui64",
1207+
"oui",
1208+
"semicolon_notation",
1209+
"split_notation",
1210+
"value",
1211+
"version",
1212+
]
1213+
11731214

11741215
class MacAddressOptional(MacAddress):
11751216
value: str | None

backend/infrahub/trigger/setup.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ async def setup_triggers(
3939
report = TriggerSetupReport()
4040

4141
if trigger_type:
42-
log.info(f"Setting up triggers of type {trigger_type.value}")
42+
log.debug(f"Setting up triggers of type {trigger_type.value}")
4343
else:
44-
log.info("Setting up all triggers")
44+
log.debug("Setting up all triggers")
4545

4646
# -------------------------------------------------------------
4747
# Retrieve existing Deployments and Automation from the server
@@ -112,4 +112,15 @@ async def setup_triggers(
112112
await client.delete_automation(automation_id=existing_automation.id)
113113
log.info(f"{item_to_delete} Deleted")
114114

115+
if trigger_type:
116+
log.info(
117+
f"Processed triggers of type {trigger_type.value}: "
118+
f"{len(report.created)} created, {len(report.updated)} updated, {len(report.unchanged)} unchanged, {len(report.deleted)} deleted"
119+
)
120+
else:
121+
log.info(
122+
f"Processed all triggers: "
123+
f"{len(report.created)} created, {len(report.updated)} updated, {len(report.unchanged)} unchanged, {len(report.deleted)} deleted"
124+
)
125+
115126
return report

backend/infrahub/workflows/catalogue.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,14 @@
303303
tags=[WorkflowTag.DATABASE_CHANGE],
304304
)
305305

306+
COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE = WorkflowDefinition(
307+
name="computed-attribute-jinja2-update-value",
308+
type=WorkflowType.CORE,
309+
module="infrahub.computed_attribute.tasks",
310+
function="computed_attribute_jinja2_update_value",
311+
tags=[WorkflowTag.DATABASE_CHANGE],
312+
)
313+
306314
TRIGGER_UPDATE_JINJA_COMPUTED_ATTRIBUTES = WorkflowDefinition(
307315
name="trigger_update_jinja2_computed_attributes",
308316
type=WorkflowType.CORE,
@@ -513,6 +521,7 @@
513521
BRANCH_MERGE_POST_PROCESS,
514522
BRANCH_REBASE,
515523
BRANCH_VALIDATE,
524+
COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE,
516525
COMPUTED_ATTRIBUTE_PROCESS_JINJA2,
517526
COMPUTED_ATTRIBUTE_PROCESS_TRANSFORM,
518527
COMPUTED_ATTRIBUTE_SETUP_JINJA2,

backend/tests/integration_docker/test_computed_attributes.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,36 @@
44
import pytest
55
import yaml
66
from infrahub_sdk import InfrahubClient
7+
from infrahub_sdk.task.models import TaskFilter, TaskState
78
from infrahub_sdk.testing.docker import TestInfrahubDockerClient
89
from infrahub_sdk.testing.repository import GitRepo
910

11+
from infrahub.workflows.catalogue import COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE
12+
1013
CURRENT_DIRECTORY = Path(__file__).parent.resolve()
1114

1215

16+
async def wait_for_all_tasks_to_be_completed(client: InfrahubClient) -> None:
17+
while ( # noqa: ASYNC110
18+
await client.task.count(filters=TaskFilter(state=[TaskState.PENDING, TaskState.RUNNING, TaskState.SCHEDULED]))
19+
> 0
20+
):
21+
await sleep(1)
22+
23+
1324
class TestComputedAttributes(TestInfrahubDockerClient):
1425
@pytest.fixture(scope="class")
1526
def infrahub_version(self) -> str:
1627
return "local"
1728

18-
async def test_load_schema(self, client: InfrahubClient) -> None:
19-
"""Prepare the schema"""
29+
@pytest.fixture(scope="class")
30+
def schema_computed_tshirt(self) -> dict:
2031
with Path(CURRENT_DIRECTORY / "test_files/computed_tshirt.yml").open(encoding="utf-8") as file: # noqa: ASYNC230
21-
computed_tshirt = yaml.safe_load(file.read())
32+
return yaml.safe_load(file.read())
2233

23-
tshirt_schema = await client.schema.load(schemas=[computed_tshirt], wait_until_converged=True)
34+
async def test_load_schema(self, client: InfrahubClient, schema_computed_tshirt: dict) -> None:
35+
"""Prepare the schema"""
36+
tshirt_schema = await client.schema.load(schemas=[schema_computed_tshirt], wait_until_converged=True)
2437
assert tshirt_schema.schema_updated
2538
# Validate that the schema is in sync after loading the device and interface schema
2639
assert await client.schema.in_sync()
@@ -190,3 +203,47 @@ async def test_transform_based_computed_attribute(self, client: InfrahubClient,
190203
await sleep(1)
191204

192205
assert sth_router_1_swe.name.value == swe_name_router_1
206+
207+
async def test_update_schema_not_related_to_computed_attribute(
208+
self, client: InfrahubClient, schema_computed_tshirt: dict
209+
) -> None:
210+
nbr_task_before = await client.task.count(
211+
filters=TaskFilter(workflow=[COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE.name])
212+
)
213+
214+
# Update schema LocationSite with a change that IS NOT related to the computed attribute
215+
# The computed attribute of type Jinja2 should not be updated neither on the sites nor on the continents
216+
schema_computed_tshirt["nodes"][4]["description"] = "New Description that will trigger a new schema"
217+
tshirt_schema = await client.schema.load(schemas=[schema_computed_tshirt], wait_until_converged=True)
218+
assert tshirt_schema.schema_updated
219+
220+
# Validate that the schema is in sync after loading the device and interface schema
221+
assert await client.schema.in_sync()
222+
223+
await sleep(1)
224+
await wait_for_all_tasks_to_be_completed(client)
225+
226+
nbr_task_after_not_related = await client.task.count(
227+
filters=TaskFilter(workflow=[COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE.name])
228+
)
229+
assert nbr_task_after_not_related == nbr_task_before
230+
231+
# Update schema LocationSite with a change that IS related to the computed attribute
232+
# The computed attribute of type Jinja2 should be updated
233+
schema_computed_tshirt["nodes"][4]["attributes"][3]["computed_attribute"]["jinja2_template"] = (
234+
"WELCOME TO {{ name__value }}!"
235+
)
236+
tshirt_schema = await client.schema.load(schemas=[schema_computed_tshirt], wait_until_converged=True)
237+
assert tshirt_schema.schema_updated
238+
239+
# Validate that the schema is in sync after loading the device and interface schema
240+
assert await client.schema.in_sync()
241+
242+
await sleep(1)
243+
await wait_for_all_tasks_to_be_completed(client)
244+
245+
# The computed attribute of type Jinja2 should be updated on the sites but NOT on the continents
246+
nbr_task_after_related = await client.task.count(
247+
filters=TaskFilter(workflow=[COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE.name])
248+
)
249+
assert nbr_task_after_related == nbr_task_after_not_related + 2

0 commit comments

Comments
 (0)