Skip to content

Commit 9b51d87

Browse files
authored
Merge pull request #6440 from opsmill/dga-20250511-jinja2-hash
Ensure Jinja2 computed attribute recalculate only when the template changes
2 parents e3642cd + 9c7d6e7 commit 9b51d87

File tree

7 files changed

+127
-34
lines changed

7 files changed

+127
-34
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/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
@@ -251,6 +251,14 @@
251251
tags=[WorkflowTag.DATABASE_CHANGE],
252252
)
253253

254+
COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE = WorkflowDefinition(
255+
name="computed-attribute-jinja2-update-value",
256+
type=WorkflowType.CORE,
257+
module="infrahub.computed_attribute.tasks",
258+
function="computed_attribute_jinja2_update_value",
259+
tags=[WorkflowTag.DATABASE_CHANGE],
260+
)
261+
254262
TRIGGER_UPDATE_JINJA_COMPUTED_ATTRIBUTES = WorkflowDefinition(
255263
name="trigger_update_jinja2_computed_attributes",
256264
type=WorkflowType.CORE,
@@ -443,6 +451,7 @@
443451
BRANCH_MERGE_POST_PROCESS,
444452
BRANCH_REBASE,
445453
BRANCH_VALIDATE,
454+
COMPUTED_ATTRIBUTE_JINJA2_UPDATE_VALUE,
446455
COMPUTED_ATTRIBUTE_PROCESS_JINJA2,
447456
COMPUTED_ATTRIBUTE_PROCESS_TRANSFORM,
448457
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
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Computed Attribute of kind Jinja will only be recalculated during a schema update if the template itself has been updated.

0 commit comments

Comments
 (0)