Skip to content

Commit 9077db5

Browse files
committed
Additional tests and guardrails for triggers and actions
1 parent 8049814 commit 9077db5

File tree

5 files changed

+535
-13
lines changed

5 files changed

+535
-13
lines changed

backend/infrahub/actions/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class CoreNodeTriggerAttributeMatch(CoreNodeTriggerMatch):
7171
class CoreNodeTriggerRelationshipMatch(CoreNodeTriggerMatch):
7272
relationship_name: str
7373
added: bool
74-
peer: str
74+
peer: str | None
7575

7676

7777
class CoreNodeTriggerRule(CoreTriggerRule):
@@ -138,9 +138,10 @@ def _from_node_trigger(
138138
event_trigger.match_related = {
139139
"prefect.resource.role": "infrahub.node.relationship_update",
140140
"infrahub.field.name": match.relationship_name,
141-
"infrahub.relationship.peer_id": match.peer,
142141
"infrahub.relationship.peer_status": peer_status,
143142
}
143+
if isinstance(match.peer, str):
144+
event_trigger.match_related["infrahub.relationship.peer_id"] = match.peer
144145

145146
if isinstance(trigger_rule.action, CoreGeneratorAction):
146147
workflow = ExecuteWorkflow(

backend/infrahub/graphql/manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .directives import DIRECTIVES
2626
from .enums import generate_graphql_enum, get_enum_attribute_type_name
2727
from .metrics import SCHEMA_GENERATE_GRAPHQL_METRICS
28-
from .mutations.action import InfrahubTriggerRuleMutation
28+
from .mutations.action import InfrahubTriggerRuleMatchMutation, InfrahubTriggerRuleMutation
2929
from .mutations.artifact_definition import InfrahubArtifactDefinitionMutation
3030
from .mutations.ipam import (
3131
InfrahubIPAddressMutation,
@@ -526,6 +526,8 @@ def generate_mutation_mixin(self) -> type[object]:
526526
InfrahubKind.STANDARDWEBHOOK: InfrahubWebhookMutation,
527527
InfrahubKind.CUSTOMWEBHOOK: InfrahubWebhookMutation,
528528
InfrahubKind.NODETRIGGERRULE: InfrahubTriggerRuleMutation,
529+
InfrahubKind.NODETRIGGERATTRIBUTEMATCH: InfrahubTriggerRuleMatchMutation,
530+
InfrahubKind.NODETRIGGERRELATIONSHIPMATCH: InfrahubTriggerRuleMatchMutation,
529531
}
530532

531533
if isinstance(node_schema, NodeSchema) and node_schema.is_ip_prefix():

backend/infrahub/graphql/mutations/action.py

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any
3+
from typing import TYPE_CHECKING, Any, cast
44

55
from graphene import InputObjectType, Mutation
66
from typing_extensions import Self
77

8-
from infrahub.core.schema import NodeSchema
8+
from infrahub.core.protocols import CoreNodeTriggerAttributeMatch, CoreNodeTriggerRelationshipMatch, CoreNodeTriggerRule
9+
from infrahub.exceptions import SchemaNotFoundError, ValidationError
910
from infrahub.log import get_logger
1011

1112
from .main import InfrahubMutationMixin, InfrahubMutationOptions
@@ -15,8 +16,11 @@
1516

1617
from infrahub.core.branch import Branch
1718
from infrahub.core.node import Node
19+
from infrahub.core.schema import NodeSchema
1820
from infrahub.database import InfrahubDatabase
1921

22+
from ..initialization import GraphqlContext
23+
2024
log = get_logger()
2125

2226

@@ -28,10 +32,6 @@ def __init_subclass_with_meta__(
2832
_meta: Any | None = None,
2933
**options: dict[str, Any],
3034
) -> None:
31-
# Make sure schema is a valid NodeSchema Node Class
32-
if not isinstance(schema, NodeSchema):
33-
raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
34-
3535
if not _meta:
3636
_meta = InfrahubMutationOptions(cls)
3737

@@ -45,9 +45,12 @@ async def mutate_create(
4545
info: GraphQLResolveInfo,
4646
data: InputObjectType,
4747
branch: Branch,
48-
database: InfrahubDatabase | None = None, # noqa: ARG003
48+
database: InfrahubDatabase | None = None,
4949
) -> tuple[Node, Self]:
50-
trigger_rule_definition, result = await super().mutate_create(info=info, data=data, branch=branch)
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)
5154

5255
return trigger_rule_definition, result
5356

@@ -57,9 +60,105 @@ async def mutate_update(
5760
info: GraphQLResolveInfo,
5861
data: InputObjectType,
5962
branch: Branch,
60-
database: InfrahubDatabase | None = None, # noqa: ARG003
63+
database: InfrahubDatabase | None = None,
6164
node: Node | None = None, # noqa: ARG003
6265
) -> tuple[Node, Self]:
63-
trigger_rule_definition, result = await super().mutate_update(info=info, data=data, branch=branch)
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)
6470

6571
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+
)

backend/tests/unit/actions/test_gather.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
CoreGroupAction,
1010
CoreGroupTriggerRule,
1111
CoreNodeTriggerAttributeMatch,
12+
CoreNodeTriggerRelationshipMatch,
1213
CoreNodeTriggerRule,
1314
CoreRepository,
1415
CoreStandardGroup,
@@ -144,6 +145,108 @@ async def test_gather_trigger_gather_trigger_action_rules_node_attribute(
144145
assert len(triggers) == 0
145146

146147

148+
async def test_gather_trigger_gather_trigger_action_rules_node_relationship(
149+
register_core_models_schema: SchemaBranch, car_person_schema: SchemaBranch, db: InfrahubDatabase
150+
) -> None:
151+
group_action_target = await Node.init(db=db, schema=CoreStandardGroup)
152+
await group_action_target.new(db=db, name="GroupActionTarget")
153+
await group_action_target.save(db=db)
154+
155+
group_action = await Node.init(db=db, schema=CoreGroupAction)
156+
await group_action.new(db=db, name="MainGroupAction", group=group_action_target)
157+
await group_action.save(db=db)
158+
159+
car_owner = await Node.init(db=db, schema="TestPerson")
160+
await car_owner.new(db=db, name="Bobby")
161+
await car_owner.save(db=db)
162+
163+
main_node_trigger_rule = await Node.init(db=db, schema=CoreNodeTriggerRule)
164+
await main_node_trigger_rule.new(
165+
db=db,
166+
name="main_node_trigger",
167+
node_kind="TestCar",
168+
mutation_action="created",
169+
action=group_action,
170+
branch_scope="all_branches",
171+
)
172+
await main_node_trigger_rule.save(db=db)
173+
174+
triggers = await gather_trigger_action_rules(db=db)
175+
assert len(triggers) == 1
176+
automation = triggers[0]
177+
178+
assert automation.trigger == EventTrigger(
179+
events={"infrahub.node.created"},
180+
match={"infrahub.node.kind": "TestCar"},
181+
match_related={},
182+
)
183+
184+
relationship_match = await Node.init(db=db, schema=CoreNodeTriggerRelationshipMatch)
185+
await relationship_match.new(
186+
db=db,
187+
relationship_name="owner",
188+
peer=car_owner.id,
189+
trigger=main_node_trigger_rule,
190+
)
191+
await relationship_match.save(db=db)
192+
193+
triggers = await gather_trigger_action_rules(db=db)
194+
assert len(triggers) == 1
195+
automation = triggers[0]
196+
assert automation.trigger == EventTrigger(
197+
events={"infrahub.node.created"},
198+
match={"infrahub.node.kind": "TestCar"},
199+
match_related={
200+
"prefect.resource.role": "infrahub.node.relationship_update",
201+
"infrahub.field.name": "owner",
202+
"infrahub.relationship.peer_id": car_owner.id,
203+
"infrahub.relationship.peer_status": "added",
204+
},
205+
)
206+
207+
relationship_match.added.value = False
208+
await relationship_match.save(db=db)
209+
210+
triggers = await gather_trigger_action_rules(db=db)
211+
assert len(triggers) == 1
212+
automation = triggers[0]
213+
assert automation.trigger == EventTrigger(
214+
events={"infrahub.node.created"},
215+
match={"infrahub.node.kind": "TestCar"},
216+
match_related={
217+
"prefect.resource.role": "infrahub.node.relationship_update",
218+
"infrahub.field.name": "owner",
219+
"infrahub.relationship.peer_id": car_owner.id,
220+
"infrahub.relationship.peer_status": "removed",
221+
},
222+
)
223+
224+
main_node_trigger_rule.branch_scope.value = "other_branches"
225+
main_node_trigger_rule.mutation_action.value = "deleted"
226+
await main_node_trigger_rule.save(db=db)
227+
228+
triggers = await gather_trigger_action_rules(db=db)
229+
assert len(triggers) == 1
230+
automation = triggers[0]
231+
232+
assert automation.trigger == EventTrigger(
233+
events={"infrahub.node.deleted"},
234+
match={"infrahub.node.kind": "TestCar", "infrahub.branch.name": "!main"},
235+
match_related={
236+
"prefect.resource.role": "infrahub.node.relationship_update",
237+
"infrahub.field.name": "owner",
238+
"infrahub.relationship.peer_id": car_owner.id,
239+
"infrahub.relationship.peer_status": "removed",
240+
},
241+
)
242+
243+
main_node_trigger_rule.active.value = False
244+
await main_node_trigger_rule.save(db=db)
245+
246+
triggers = await gather_trigger_action_rules(db=db)
247+
assert len(triggers) == 0
248+
249+
147250
async def test_gather_trigger_gather_trigger_action_rules_group_generators(
148251
register_core_models_schema: SchemaBranch, db: InfrahubDatabase
149252
) -> None:

0 commit comments

Comments
 (0)