infrahub-server 1.4.13__py3-none-any.whl → 1.5.0__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.
Files changed (222) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/internal.py +2 -0
  5. infrahub/api/query.py +2 -0
  6. infrahub/api/schema.py +27 -3
  7. infrahub/auth.py +5 -5
  8. infrahub/cli/__init__.py +2 -0
  9. infrahub/cli/db.py +160 -157
  10. infrahub/cli/dev.py +118 -0
  11. infrahub/cli/upgrade.py +56 -9
  12. infrahub/computed_attribute/tasks.py +19 -7
  13. infrahub/config.py +7 -2
  14. infrahub/core/attribute.py +35 -24
  15. infrahub/core/branch/enums.py +1 -1
  16. infrahub/core/branch/models.py +9 -5
  17. infrahub/core/branch/needs_rebase_status.py +11 -0
  18. infrahub/core/branch/tasks.py +72 -10
  19. infrahub/core/changelog/models.py +2 -10
  20. infrahub/core/constants/__init__.py +4 -0
  21. infrahub/core/constants/infrahubkind.py +1 -0
  22. infrahub/core/convert_object_type/object_conversion.py +201 -0
  23. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  24. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  25. infrahub/core/diff/model/path.py +4 -0
  26. infrahub/core/diff/payload_builder.py +1 -1
  27. infrahub/core/diff/query/artifact.py +1 -0
  28. infrahub/core/diff/query/field_summary.py +1 -0
  29. infrahub/core/graph/__init__.py +1 -1
  30. infrahub/core/initialization.py +7 -4
  31. infrahub/core/manager.py +3 -81
  32. infrahub/core/migrations/__init__.py +3 -0
  33. infrahub/core/migrations/exceptions.py +4 -0
  34. infrahub/core/migrations/graph/__init__.py +11 -10
  35. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  37. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  38. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  39. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  40. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  41. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  42. infrahub/core/migrations/query/__init__.py +7 -8
  43. infrahub/core/migrations/query/attribute_add.py +8 -6
  44. infrahub/core/migrations/query/attribute_remove.py +134 -0
  45. infrahub/core/migrations/runner.py +54 -0
  46. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  47. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  48. infrahub/core/migrations/schema/node_attribute_add.py +26 -5
  49. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  50. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  51. infrahub/core/migrations/schema/node_remove.py +2 -1
  52. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  53. infrahub/core/migrations/shared.py +66 -19
  54. infrahub/core/models.py +2 -2
  55. infrahub/core/node/__init__.py +207 -54
  56. infrahub/core/node/create.py +53 -49
  57. infrahub/core/node/lock_utils.py +124 -0
  58. infrahub/core/node/node_property_attribute.py +230 -0
  59. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  60. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  61. infrahub/core/node/resource_manager/number_pool.py +2 -1
  62. infrahub/core/node/standard.py +1 -1
  63. infrahub/core/property.py +11 -0
  64. infrahub/core/protocols.py +8 -1
  65. infrahub/core/query/attribute.py +82 -15
  66. infrahub/core/query/ipam.py +16 -4
  67. infrahub/core/query/node.py +66 -188
  68. infrahub/core/query/relationship.py +44 -26
  69. infrahub/core/query/subquery.py +0 -8
  70. infrahub/core/relationship/model.py +69 -24
  71. infrahub/core/schema/__init__.py +56 -0
  72. infrahub/core/schema/attribute_schema.py +4 -2
  73. infrahub/core/schema/basenode_schema.py +42 -2
  74. infrahub/core/schema/definitions/core/__init__.py +2 -0
  75. infrahub/core/schema/definitions/core/check.py +1 -1
  76. infrahub/core/schema/definitions/core/generator.py +2 -0
  77. infrahub/core/schema/definitions/core/group.py +16 -2
  78. infrahub/core/schema/definitions/core/repository.py +7 -0
  79. infrahub/core/schema/definitions/core/transform.py +1 -1
  80. infrahub/core/schema/definitions/internal.py +12 -3
  81. infrahub/core/schema/generated/attribute_schema.py +2 -2
  82. infrahub/core/schema/generated/base_node_schema.py +6 -1
  83. infrahub/core/schema/manager.py +3 -0
  84. infrahub/core/schema/node_schema.py +1 -0
  85. infrahub/core/schema/relationship_schema.py +0 -1
  86. infrahub/core/schema/schema_branch.py +295 -10
  87. infrahub/core/schema/schema_branch_display.py +135 -0
  88. infrahub/core/schema/schema_branch_hfid.py +120 -0
  89. infrahub/core/validators/aggregated_checker.py +1 -1
  90. infrahub/database/graph.py +21 -0
  91. infrahub/display_labels/__init__.py +0 -0
  92. infrahub/display_labels/gather.py +48 -0
  93. infrahub/display_labels/models.py +240 -0
  94. infrahub/display_labels/tasks.py +192 -0
  95. infrahub/display_labels/triggers.py +22 -0
  96. infrahub/events/branch_action.py +27 -1
  97. infrahub/events/group_action.py +1 -1
  98. infrahub/events/node_action.py +1 -1
  99. infrahub/generators/constants.py +7 -0
  100. infrahub/generators/models.py +38 -12
  101. infrahub/generators/tasks.py +34 -16
  102. infrahub/git/base.py +38 -1
  103. infrahub/git/integrator.py +22 -14
  104. infrahub/graphql/api/dependencies.py +2 -4
  105. infrahub/graphql/api/endpoints.py +16 -6
  106. infrahub/graphql/app.py +2 -4
  107. infrahub/graphql/initialization.py +2 -3
  108. infrahub/graphql/manager.py +213 -137
  109. infrahub/graphql/middleware.py +12 -0
  110. infrahub/graphql/mutations/branch.py +16 -0
  111. infrahub/graphql/mutations/computed_attribute.py +110 -3
  112. infrahub/graphql/mutations/convert_object_type.py +44 -13
  113. infrahub/graphql/mutations/display_label.py +118 -0
  114. infrahub/graphql/mutations/generator.py +25 -7
  115. infrahub/graphql/mutations/hfid.py +125 -0
  116. infrahub/graphql/mutations/ipam.py +73 -41
  117. infrahub/graphql/mutations/main.py +61 -178
  118. infrahub/graphql/mutations/profile.py +195 -0
  119. infrahub/graphql/mutations/proposed_change.py +8 -1
  120. infrahub/graphql/mutations/relationship.py +2 -2
  121. infrahub/graphql/mutations/repository.py +22 -83
  122. infrahub/graphql/mutations/resource_manager.py +2 -2
  123. infrahub/graphql/mutations/webhook.py +1 -1
  124. infrahub/graphql/queries/resource_manager.py +1 -1
  125. infrahub/graphql/registry.py +173 -0
  126. infrahub/graphql/resolvers/resolver.py +2 -0
  127. infrahub/graphql/schema.py +8 -1
  128. infrahub/graphql/schema_sort.py +170 -0
  129. infrahub/graphql/types/branch.py +4 -1
  130. infrahub/graphql/types/enums.py +3 -0
  131. infrahub/groups/tasks.py +1 -1
  132. infrahub/hfid/__init__.py +0 -0
  133. infrahub/hfid/gather.py +48 -0
  134. infrahub/hfid/models.py +240 -0
  135. infrahub/hfid/tasks.py +191 -0
  136. infrahub/hfid/triggers.py +22 -0
  137. infrahub/lock.py +119 -42
  138. infrahub/locks/__init__.py +0 -0
  139. infrahub/locks/tasks.py +37 -0
  140. infrahub/patch/plan_writer.py +2 -2
  141. infrahub/permissions/constants.py +2 -0
  142. infrahub/profiles/__init__.py +0 -0
  143. infrahub/profiles/node_applier.py +101 -0
  144. infrahub/profiles/queries/__init__.py +0 -0
  145. infrahub/profiles/queries/get_profile_data.py +98 -0
  146. infrahub/profiles/tasks.py +63 -0
  147. infrahub/proposed_change/tasks.py +24 -5
  148. infrahub/repositories/__init__.py +0 -0
  149. infrahub/repositories/create_repository.py +113 -0
  150. infrahub/server.py +9 -1
  151. infrahub/services/__init__.py +8 -5
  152. infrahub/services/adapters/workflow/worker.py +5 -2
  153. infrahub/task_manager/event.py +5 -0
  154. infrahub/task_manager/models.py +7 -0
  155. infrahub/tasks/registry.py +6 -4
  156. infrahub/trigger/catalogue.py +4 -0
  157. infrahub/trigger/models.py +2 -0
  158. infrahub/trigger/setup.py +13 -4
  159. infrahub/trigger/tasks.py +6 -0
  160. infrahub/webhook/models.py +1 -1
  161. infrahub/workers/dependencies.py +3 -1
  162. infrahub/workers/infrahub_async.py +5 -1
  163. infrahub/workflows/catalogue.py +118 -3
  164. infrahub/workflows/initialization.py +21 -0
  165. infrahub/workflows/models.py +17 -2
  166. infrahub_sdk/branch.py +17 -8
  167. infrahub_sdk/checks.py +1 -1
  168. infrahub_sdk/client.py +376 -95
  169. infrahub_sdk/config.py +29 -2
  170. infrahub_sdk/convert_object_type.py +61 -0
  171. infrahub_sdk/ctl/branch.py +3 -0
  172. infrahub_sdk/ctl/check.py +2 -3
  173. infrahub_sdk/ctl/cli_commands.py +20 -12
  174. infrahub_sdk/ctl/config.py +8 -2
  175. infrahub_sdk/ctl/generator.py +6 -3
  176. infrahub_sdk/ctl/graphql.py +184 -0
  177. infrahub_sdk/ctl/repository.py +39 -1
  178. infrahub_sdk/ctl/schema.py +40 -10
  179. infrahub_sdk/ctl/task.py +110 -0
  180. infrahub_sdk/ctl/utils.py +4 -0
  181. infrahub_sdk/ctl/validate.py +5 -3
  182. infrahub_sdk/diff.py +4 -5
  183. infrahub_sdk/exceptions.py +2 -0
  184. infrahub_sdk/generator.py +7 -1
  185. infrahub_sdk/graphql/__init__.py +12 -0
  186. infrahub_sdk/graphql/constants.py +1 -0
  187. infrahub_sdk/graphql/plugin.py +85 -0
  188. infrahub_sdk/graphql/query.py +77 -0
  189. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  190. infrahub_sdk/graphql/utils.py +40 -0
  191. infrahub_sdk/node/attribute.py +2 -0
  192. infrahub_sdk/node/node.py +28 -20
  193. infrahub_sdk/node/relationship.py +1 -3
  194. infrahub_sdk/playback.py +1 -2
  195. infrahub_sdk/protocols.py +54 -6
  196. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  197. infrahub_sdk/pytest_plugin/utils.py +40 -0
  198. infrahub_sdk/repository.py +1 -2
  199. infrahub_sdk/schema/__init__.py +70 -4
  200. infrahub_sdk/schema/main.py +1 -0
  201. infrahub_sdk/schema/repository.py +8 -0
  202. infrahub_sdk/spec/models.py +7 -0
  203. infrahub_sdk/spec/object.py +54 -6
  204. infrahub_sdk/spec/processors/__init__.py +0 -0
  205. infrahub_sdk/spec/processors/data_processor.py +10 -0
  206. infrahub_sdk/spec/processors/factory.py +34 -0
  207. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  208. infrahub_sdk/spec/range_expansion.py +118 -0
  209. infrahub_sdk/task/models.py +6 -4
  210. infrahub_sdk/timestamp.py +18 -6
  211. infrahub_sdk/transforms.py +1 -1
  212. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
  213. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +221 -165
  214. infrahub_testcontainers/container.py +114 -2
  215. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  216. infrahub_testcontainers/docker-compose.test.yml +5 -0
  217. infrahub_testcontainers/models.py +2 -2
  218. infrahub_testcontainers/performance_test.py +4 -4
  219. infrahub/core/convert_object_type/conversion.py +0 -134
  220. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
  221. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
  222. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from infrahub.core.schema import SchemaAttributePath
10
+
11
+
12
+ @dataclass
13
+ class HFIDDefinition:
14
+ hfid: list[str]
15
+ attributes: set[str] = field(default_factory=set)
16
+ relationships: set[str] = field(default_factory=set)
17
+ relationship_fields: dict[str, set[str]] = field(default_factory=dict)
18
+ filter_key: str = "ids"
19
+
20
+ @property
21
+ def fields(self) -> list[str]:
22
+ return sorted(list(self.attributes) + list(self.relationships))
23
+
24
+ @property
25
+ def has_related_components(self) -> bool:
26
+ """Indicate if the associated template use variables from relationships"""
27
+ return len(self.relationships) > 0
28
+
29
+ def get_hash(self) -> str:
30
+ return hashlib.md5("::".join(self.hfid).encode(), usedforsecurity=False).hexdigest()
31
+
32
+
33
+ @dataclass
34
+ class RelationshipIdentifier:
35
+ kind: str
36
+ hfid: list[str]
37
+ filter_key: str
38
+
39
+ def __hash__(self) -> int:
40
+ return hash(f"{self.kind}::{'::'.join(self.hfid)}::{self.filter_key}")
41
+
42
+
43
+ @dataclass
44
+ class RelationshipTriggers:
45
+ attributes: dict[str, set[RelationshipIdentifier]] = field(default_factory=dict)
46
+
47
+
48
+ class HFIDs:
49
+ def __init__(
50
+ self,
51
+ node_level_hfids: dict[str, HFIDDefinition] | None = None,
52
+ relationship_triggers: dict[str, RelationshipTriggers] | None = None,
53
+ ) -> None:
54
+ self._node_level_hfids: dict[str, HFIDDefinition] = node_level_hfids or {}
55
+ self._relationship_triggers: dict[str, RelationshipTriggers] = relationship_triggers or {}
56
+
57
+ def duplicate(self) -> HFIDs:
58
+ return self.__class__(
59
+ node_level_hfids=deepcopy(self._node_level_hfids),
60
+ relationship_triggers=deepcopy(self._relationship_triggers),
61
+ )
62
+
63
+ def register_hfid_schema_path(self, kind: str, schema_path: SchemaAttributePath, hfid: list[str]) -> None:
64
+ """Register HFID using the schema path of each impacted schema path in use."""
65
+ if kind not in self._node_level_hfids:
66
+ self._node_level_hfids[kind] = HFIDDefinition(hfid=hfid)
67
+ if schema_path.is_type_attribute:
68
+ self._node_level_hfids[kind].attributes.add(schema_path.active_attribute_schema.name)
69
+ elif schema_path.is_type_relationship and schema_path.related_schema:
70
+ self._node_level_hfids[kind].relationships.add(schema_path.active_relationship_schema.name)
71
+ if schema_path.active_relationship_schema.name not in self._node_level_hfids[kind].relationship_fields:
72
+ self._node_level_hfids[kind].relationship_fields[schema_path.active_relationship_schema.name] = set()
73
+ self._node_level_hfids[kind].relationship_fields[schema_path.active_relationship_schema.name].add(
74
+ schema_path.active_attribute_schema.name
75
+ )
76
+ if schema_path.related_schema.kind not in self._relationship_triggers:
77
+ self._relationship_triggers[schema_path.related_schema.kind] = RelationshipTriggers()
78
+ if (
79
+ schema_path.active_attribute_schema.name
80
+ not in self._relationship_triggers[schema_path.related_schema.kind].attributes
81
+ ):
82
+ self._relationship_triggers[schema_path.related_schema.kind].attributes[
83
+ schema_path.active_attribute_schema.name
84
+ ] = set()
85
+ self._relationship_triggers[schema_path.related_schema.kind].attributes[
86
+ schema_path.active_attribute_schema.name
87
+ ].add(
88
+ RelationshipIdentifier(
89
+ kind=kind, filter_key=f"{schema_path.active_relationship_schema.name}__ids", hfid=hfid
90
+ )
91
+ )
92
+
93
+ def targets_node(self, kind: str) -> bool:
94
+ """Indicates if there is a human_friendly_id defined for the targeted node"""
95
+ return kind in self._node_level_hfids
96
+
97
+ def get_node_definition(self, kind: str) -> HFIDDefinition:
98
+ """Return node kinds together with their template definitions."""
99
+ return self._node_level_hfids[kind]
100
+
101
+ def get_template_nodes(self) -> dict[str, HFIDDefinition]:
102
+ """Return node kinds together with their template definitions."""
103
+ return self._node_level_hfids
104
+
105
+ def get_related_trigger_nodes(self) -> dict[str, RelationshipTriggers]:
106
+ """Return node kinds that other nodes use within their templates for display_labels."""
107
+ return self._relationship_triggers
108
+
109
+ def get_related_definition(self, related_kind: str, target_kind: str) -> HFIDDefinition:
110
+ relationship_trigger = self._relationship_triggers[related_kind]
111
+ for applicable_kinds in relationship_trigger.attributes.values():
112
+ for relationship_identifier in applicable_kinds:
113
+ if target_kind == relationship_identifier.kind:
114
+ template_label = self.get_node_definition(kind=target_kind)
115
+ template_label.filter_key = relationship_identifier.filter_key
116
+ return template_label
117
+
118
+ raise ValueError(
119
+ f"Unable to find registered template for {target_kind} registered on related node {related_kind}"
120
+ )
@@ -49,7 +49,7 @@ class AggregatedConstraintChecker:
49
49
  node_display_label = None
50
50
  display_label = None
51
51
  if node:
52
- node_display_label = await node.render_display_label(db=self.db)
52
+ node_display_label = await node.get_display_label(db=self.db)
53
53
  if node_display_label:
54
54
  if request.node_schema.display_labels and node:
55
55
  display_label = f"Node {node_display_label} ({node.get_kind()}: {path.node_id})"
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from infrahub.core.graph import GRAPH_VERSION
6
+ from infrahub.core.initialization import get_root_node
7
+ from infrahub.log import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from infrahub.database import InfrahubDatabase
11
+
12
+
13
+ log = get_logger()
14
+
15
+
16
+ async def validate_graph_version(db: InfrahubDatabase) -> None:
17
+ root = await get_root_node(db=db)
18
+ if root.graph_version != GRAPH_VERSION:
19
+ log.warning(
20
+ f"Expected database graph version {GRAPH_VERSION} but got {root.graph_version}, possibly 'infrahub upgrade' has not been executed"
21
+ )
File without changes
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from prefect import task
6
+ from prefect.cache_policies import NONE
7
+ from prefect.logging import get_run_logger
8
+
9
+ from infrahub.core.registry import registry
10
+ from infrahub.database import InfrahubDatabase # noqa: TC001 needed for prefect flow
11
+
12
+ from .models import DisplayLabelTriggerDefinition
13
+
14
+
15
+ @dataclass
16
+ class BranchScope:
17
+ name: str
18
+ out_of_scope: list[str] = field(default_factory=list)
19
+
20
+
21
+ @task(
22
+ name="gather-trigger-display-labels-jinja2",
23
+ cache_policy=NONE,
24
+ )
25
+ async def gather_trigger_display_labels_jinja2(
26
+ db: InfrahubDatabase | None = None, # noqa: ARG001 Needed to have a common function signature for gathering functions
27
+ ) -> list[DisplayLabelTriggerDefinition]:
28
+ log = get_run_logger()
29
+
30
+ # Build a list of all branches to process based on which branch is different from main
31
+ branches_with_diff_from_main = registry.get_altered_schema_branches()
32
+ branches_to_process: list[BranchScope] = [BranchScope(name=branch) for branch in branches_with_diff_from_main]
33
+ branches_to_process.append(BranchScope(name=registry.default_branch, out_of_scope=branches_with_diff_from_main))
34
+
35
+ triggers: list[DisplayLabelTriggerDefinition] = []
36
+
37
+ for branch in branches_to_process:
38
+ schema_branch = registry.schema.get_schema_branch(name=branch.name)
39
+ branch_triggers = DisplayLabelTriggerDefinition.from_schema_display_labels(
40
+ branch=branch.name,
41
+ display_labels=schema_branch.display_labels,
42
+ branches_out_of_scope=branch.out_of_scope,
43
+ )
44
+ log.info(f"Generating {len(branch_triggers)} Jinja2 trigger for {branch.name} (except {branch.out_of_scope})")
45
+
46
+ triggers.extend(branch_triggers)
47
+
48
+ return triggers
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, Self
5
+
6
+ from infrahub_sdk.graphql import Query
7
+ from pydantic import BaseModel, Field
8
+
9
+ from infrahub.core.constants import RelationshipCardinality
10
+ from infrahub.core.registry import registry
11
+ from infrahub.core.schema import NodeSchema # noqa: TC001
12
+ from infrahub.events import NodeUpdatedEvent
13
+ from infrahub.trigger.constants import NAME_SEPARATOR
14
+ from infrahub.trigger.models import (
15
+ EventTrigger,
16
+ ExecuteWorkflow,
17
+ TriggerBranchDefinition,
18
+ TriggerType,
19
+ )
20
+ from infrahub.workflows.catalogue import DISPLAY_LABELS_PROCESS_JINJA2
21
+
22
+ if TYPE_CHECKING:
23
+ from infrahub.core.schema.schema_branch_display import DisplayLabels, RelationshipTriggers
24
+
25
+
26
+ @dataclass
27
+ class AttributeTarget:
28
+ hash: str
29
+ fields: set[str]
30
+
31
+
32
+ class DisplayLabelTriggerDefinition(TriggerBranchDefinition):
33
+ type: TriggerType = TriggerType.DISPLAY_LABEL_JINJA2
34
+ template_hash: str
35
+ target_kind: str | None = Field(default=None)
36
+
37
+ def get_description(self) -> str:
38
+ return f"{super().get_description()} | hash:{self.template_hash}"
39
+
40
+ @classmethod
41
+ def from_schema_display_labels(
42
+ cls,
43
+ branch: str,
44
+ display_labels: DisplayLabels,
45
+ branches_out_of_scope: list[str] | None = None,
46
+ ) -> list[DisplayLabelTriggerDefinition]:
47
+ """
48
+ This function is used to create a trigger definition for a display labels of type Jinja2.
49
+ """
50
+
51
+ definitions: list[DisplayLabelTriggerDefinition] = []
52
+
53
+ for node_kind, template_label in display_labels.get_template_nodes().items():
54
+ definitions.append(
55
+ cls.new(
56
+ branch=branch,
57
+ node_kind=node_kind,
58
+ target_kind=node_kind,
59
+ fields=[
60
+ "_trigger_placeholder"
61
+ ], # Triggers for the nodes themselves are only used to determine if all nodes should be regenerated
62
+ template_hash=template_label.get_hash(),
63
+ branches_out_of_scope=branches_out_of_scope,
64
+ )
65
+ )
66
+
67
+ for related_kind, relationship_trigger in display_labels.get_related_trigger_nodes().items():
68
+ definitions.extend(
69
+ cls.from_related_node(
70
+ branch=branch,
71
+ related_kind=related_kind,
72
+ relationship_trigger=relationship_trigger,
73
+ display_labels=display_labels,
74
+ branches_out_of_scope=branches_out_of_scope,
75
+ )
76
+ )
77
+
78
+ return definitions
79
+
80
+ @classmethod
81
+ def from_related_node(
82
+ cls,
83
+ branch: str,
84
+ related_kind: str,
85
+ relationship_trigger: RelationshipTriggers,
86
+ display_labels: DisplayLabels,
87
+ branches_out_of_scope: list[str] | None = None,
88
+ ) -> list[DisplayLabelTriggerDefinition]:
89
+ targets_by_attribute: dict[str, AttributeTarget] = {}
90
+ definitions: list[DisplayLabelTriggerDefinition] = []
91
+ for attribute, relationship_identifiers in relationship_trigger.attributes.items():
92
+ for relationship_identifier in relationship_identifiers:
93
+ actual_node = display_labels.get_template_node(kind=relationship_identifier.kind)
94
+ if relationship_identifier.kind not in targets_by_attribute:
95
+ targets_by_attribute[relationship_identifier.kind] = AttributeTarget(
96
+ actual_node.get_hash(), fields=set()
97
+ )
98
+ targets_by_attribute[relationship_identifier.kind].fields.add(attribute)
99
+
100
+ for target_kind, attribute_target in targets_by_attribute.items():
101
+ definitions.append(
102
+ cls.new(
103
+ branch=branch,
104
+ node_kind=related_kind,
105
+ target_kind=target_kind,
106
+ fields=sorted(attribute_target.fields),
107
+ template_hash=attribute_target.hash,
108
+ branches_out_of_scope=branches_out_of_scope,
109
+ )
110
+ )
111
+
112
+ return definitions
113
+
114
+ @classmethod
115
+ def new(
116
+ cls,
117
+ branch: str,
118
+ node_kind: str,
119
+ target_kind: str,
120
+ template_hash: str,
121
+ fields: list[str],
122
+ branches_out_of_scope: list[str] | None = None,
123
+ ) -> Self:
124
+ event_trigger = EventTrigger()
125
+ event_trigger.events.add(NodeUpdatedEvent.event_name)
126
+ event_trigger.match = {"infrahub.node.kind": node_kind}
127
+ if branches_out_of_scope:
128
+ event_trigger.match["infrahub.branch.name"] = [f"!{branch}" for branch in branches_out_of_scope]
129
+ elif not branches_out_of_scope and branch != registry.default_branch:
130
+ event_trigger.match["infrahub.branch.name"] = branch
131
+
132
+ event_trigger.match_related = {
133
+ "prefect.resource.role": ["infrahub.node.attribute_update", "infrahub.node.relationship_update"],
134
+ "infrahub.field.name": fields,
135
+ }
136
+
137
+ workflow = ExecuteWorkflow(
138
+ workflow=DISPLAY_LABELS_PROCESS_JINJA2,
139
+ parameters={
140
+ "branch_name": "{{ event.resource['infrahub.branch.name'] }}",
141
+ "node_kind": node_kind,
142
+ "object_id": "{{ event.resource['infrahub.node.id'] }}",
143
+ "target_kind": target_kind,
144
+ "context": {
145
+ "__prefect_kind": "json",
146
+ "value": {
147
+ "__prefect_kind": "jinja",
148
+ "template": "{{ event.payload['context'] | tojson }}",
149
+ },
150
+ },
151
+ },
152
+ )
153
+
154
+ trigger_definition_target_kind = target_kind if target_kind == node_kind else None
155
+
156
+ return cls(
157
+ name=f"{target_kind}{NAME_SEPARATOR}by{NAME_SEPARATOR}{node_kind}",
158
+ template_hash=template_hash,
159
+ branch=branch,
160
+ trigger=event_trigger,
161
+ actions=[workflow],
162
+ target_kind=trigger_definition_target_kind,
163
+ )
164
+
165
+
166
+ class DisplayLabelJinja2GraphQLResponse(BaseModel):
167
+ node_id: str
168
+ display_label_value: str | None
169
+ variables: dict[str, Any] = Field(default_factory=dict)
170
+
171
+
172
+ class DisplayLabelJinja2GraphQL(BaseModel):
173
+ filter_key: str
174
+ node_schema: NodeSchema = Field(..., description="The node kind where the computed attribute is defined")
175
+ variables: list[str] = Field(..., description="The list of variable names used within the computed attribute")
176
+
177
+ def render_graphql_query(self, filter_id: str) -> str:
178
+ query_fields = self.query_fields
179
+ query_fields["id"] = None
180
+ query_fields["display_label"] = None
181
+ query = Query(
182
+ name="DisplayLabelFilter",
183
+ query={
184
+ self.node_schema.kind: {
185
+ "@filters": {self.filter_key: filter_id},
186
+ "edges": {"node": query_fields},
187
+ }
188
+ },
189
+ )
190
+
191
+ return query.render()
192
+
193
+ @property
194
+ def query_fields(self) -> dict[str, Any]:
195
+ output: dict[str, Any] = {}
196
+ for variable in self.variables:
197
+ field_name, remainder = variable.split("__", maxsplit=1)
198
+ if field_name in self.node_schema.attribute_names:
199
+ output[field_name] = {remainder: None}
200
+ elif field_name in self.node_schema.relationship_names:
201
+ related_attribute, related_value = remainder.split("__", maxsplit=1)
202
+ relationship = self.node_schema.get_relationship(name=field_name)
203
+ if relationship.cardinality == RelationshipCardinality.ONE:
204
+ if field_name not in output:
205
+ output[field_name] = {"node": {}}
206
+ output[field_name]["node"][related_attribute] = {related_value: None}
207
+ return output
208
+
209
+ def parse_response(self, response: dict[str, Any]) -> list[DisplayLabelJinja2GraphQLResponse]:
210
+ rendered_response: list[DisplayLabelJinja2GraphQLResponse] = []
211
+ if kind_payload := response.get(self.node_schema.kind):
212
+ edges = kind_payload.get("edges", [])
213
+ for node in edges:
214
+ if node_response := self.to_node_response(node_dict=node):
215
+ rendered_response.append(node_response)
216
+ return rendered_response
217
+
218
+ def to_node_response(self, node_dict: dict[str, Any]) -> DisplayLabelJinja2GraphQLResponse | None:
219
+ if node := node_dict.get("node"):
220
+ node_id = node.get("id")
221
+ else:
222
+ return None
223
+
224
+ display_label = node.get("display_label")
225
+ response = DisplayLabelJinja2GraphQLResponse(node_id=node_id, display_label_value=display_label)
226
+ for variable in self.variables:
227
+ field_name, remainder = variable.split("__", maxsplit=1)
228
+ response.variables[variable] = None
229
+ if field_content := node.get(field_name):
230
+ if field_name in self.node_schema.attribute_names:
231
+ response.variables[variable] = field_content.get(remainder)
232
+ elif field_name in self.node_schema.relationship_names:
233
+ relationship = self.node_schema.get_relationship(name=field_name)
234
+ if relationship.cardinality == RelationshipCardinality.ONE:
235
+ related_attribute, related_value = remainder.split("__", maxsplit=1)
236
+ node_content = field_content.get("node") or {}
237
+ related_attribute_content = node_content.get(related_attribute) or {}
238
+ response.variables[variable] = related_attribute_content.get(related_value)
239
+
240
+ return response
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import cast
4
+
5
+ from infrahub_sdk.exceptions import URLNotFoundError
6
+ from infrahub_sdk.template import Jinja2Template
7
+ from prefect import flow
8
+ from prefect.logging import get_run_logger
9
+
10
+ from infrahub.context import InfrahubContext # noqa: TC001 needed for prefect flow
11
+ from infrahub.core.registry import registry
12
+ from infrahub.events import BranchDeletedEvent
13
+ from infrahub.trigger.models import TriggerSetupReport, TriggerType
14
+ from infrahub.trigger.setup import setup_triggers_specific
15
+ from infrahub.workers.dependencies import get_client, get_component, get_database, get_workflow
16
+ from infrahub.workflows.catalogue import DISPLAY_LABELS_PROCESS_JINJA2, TRIGGER_UPDATE_DISPLAY_LABELS
17
+ from infrahub.workflows.utils import add_tags, wait_for_schema_to_converge
18
+
19
+ from .gather import gather_trigger_display_labels_jinja2
20
+ from .models import DisplayLabelJinja2GraphQL, DisplayLabelJinja2GraphQLResponse, DisplayLabelTriggerDefinition
21
+
22
+ UPDATE_DISPLAY_LABEL = """
23
+ mutation UpdateDisplayLabel(
24
+ $id: String!,
25
+ $kind: String!,
26
+ $value: String!
27
+ ) {
28
+ InfrahubUpdateDisplayLabel(
29
+ data: {id: $id, value: $value, kind: $kind}
30
+ ) {
31
+ ok
32
+ }
33
+ }
34
+ """
35
+
36
+
37
+ @flow(
38
+ name="display-label-jinja2-update-value",
39
+ flow_run_name="Update value for display_label on {node_kind}",
40
+ )
41
+ async def display_label_jinja2_update_value(
42
+ branch_name: str,
43
+ obj: DisplayLabelJinja2GraphQLResponse,
44
+ node_kind: str,
45
+ template: Jinja2Template,
46
+ ) -> None:
47
+ log = get_run_logger()
48
+ client = get_client()
49
+
50
+ await add_tags(branches=[branch_name], nodes=[obj.node_id], db_change=True)
51
+
52
+ value = await template.render(variables=obj.variables)
53
+ if value == obj.display_label_value:
54
+ log.debug(f"Ignoring to update {obj} with existing value on display_label={value}")
55
+ return
56
+
57
+ try:
58
+ await client.execute_graphql(
59
+ query=UPDATE_DISPLAY_LABEL,
60
+ variables={"id": obj.node_id, "kind": node_kind, "value": value},
61
+ branch_name=branch_name,
62
+ )
63
+ log.info(f"Updating {node_kind}.display_label='{value}' ({obj.node_id})")
64
+ except URLNotFoundError:
65
+ log.warning(
66
+ f"Updating {node_kind}.display_label='{value}' ({obj.node_id}) failed for branch {branch_name} (branch not found)"
67
+ )
68
+
69
+
70
+ @flow(
71
+ name="display-label-process-jinja2",
72
+ flow_run_name="Process display_labels for {target_kind}",
73
+ )
74
+ async def process_display_label(
75
+ branch_name: str,
76
+ node_kind: str,
77
+ object_id: str,
78
+ target_kind: str,
79
+ context: InfrahubContext, # noqa: ARG001
80
+ ) -> None:
81
+ log = get_run_logger()
82
+ client = get_client()
83
+
84
+ await add_tags(branches=[branch_name])
85
+
86
+ target_schema = branch_name if branch_name in registry.get_altered_schema_branches() else registry.default_branch
87
+ schema_branch = registry.schema.get_schema_branch(name=target_schema)
88
+ node_schema = schema_branch.get_node(name=target_kind, duplicate=False)
89
+
90
+ if node_kind == target_kind:
91
+ display_label_template = schema_branch.display_labels.get_template_node(kind=node_kind)
92
+ else:
93
+ display_label_template = schema_branch.display_labels.get_related_template(
94
+ related_kind=node_kind, target_kind=target_kind
95
+ )
96
+
97
+ jinja_template = Jinja2Template(template=display_label_template.template)
98
+ variables = jinja_template.get_variables()
99
+ display_label_graphql = DisplayLabelJinja2GraphQL(
100
+ node_schema=node_schema, variables=variables, filter_key=display_label_template.filter_key
101
+ )
102
+
103
+ query = display_label_graphql.render_graphql_query(filter_id=object_id)
104
+ response = await client.execute_graphql(query=query, branch_name=branch_name)
105
+ update_candidates = display_label_graphql.parse_response(response=response)
106
+
107
+ if not update_candidates:
108
+ log.debug("No nodes found that requires updates")
109
+ return
110
+
111
+ batch = await client.create_batch()
112
+ for node in update_candidates:
113
+ batch.add(
114
+ task=display_label_jinja2_update_value,
115
+ branch_name=branch_name,
116
+ obj=node,
117
+ node_kind=node_schema.kind,
118
+ template=jinja_template,
119
+ )
120
+
121
+ _ = [response async for _, response in batch.execute()]
122
+
123
+
124
+ @flow(name="display-labels-setup-jinja2", flow_run_name="Setup display labels in task-manager")
125
+ async def display_labels_setup_jinja2(
126
+ context: InfrahubContext, branch_name: str | None = None, event_name: str | None = None
127
+ ) -> None:
128
+ database = await get_database()
129
+ async with database.start_session() as db:
130
+ log = get_run_logger()
131
+
132
+ if branch_name:
133
+ await add_tags(branches=[branch_name])
134
+ component = await get_component()
135
+ await wait_for_schema_to_converge(branch_name=branch_name, component=component, db=db, log=log)
136
+
137
+ report: TriggerSetupReport = await setup_triggers_specific(
138
+ gatherer=gather_trigger_display_labels_jinja2, trigger_type=TriggerType.DISPLAY_LABEL_JINJA2
139
+ ) # type: ignore[misc]
140
+
141
+ # Configure all DisplayLabelTriggerDefinitions in Prefect
142
+ display_reports = [cast(DisplayLabelTriggerDefinition, entry) for entry in report.updated + report.created]
143
+ direct_target_triggers = [display_report for display_report in display_reports if display_report.target_kind]
144
+
145
+ for display_report in direct_target_triggers:
146
+ if event_name != BranchDeletedEvent.event_name and display_report.branch == branch_name:
147
+ await get_workflow().submit_workflow(
148
+ workflow=TRIGGER_UPDATE_DISPLAY_LABELS,
149
+ context=context,
150
+ parameters={
151
+ "branch_name": display_report.branch,
152
+ "kind": display_report.target_kind,
153
+ },
154
+ )
155
+
156
+ log.info(f"{report.in_use_count} Display labels for Jinja2 automation configuration completed")
157
+
158
+
159
+ @flow(
160
+ name="trigger-update-display-labels",
161
+ flow_run_name="Trigger updates for display labels for {kind}",
162
+ )
163
+ async def trigger_update_display_labels(
164
+ branch_name: str,
165
+ kind: str,
166
+ context: InfrahubContext,
167
+ ) -> None:
168
+ await add_tags(branches=[branch_name])
169
+
170
+ client = get_client()
171
+
172
+ # NOTE we only need the id of the nodes, this query will still query for the HFID
173
+ node_schema = registry.schema.get_node_schema(name=kind, branch=branch_name)
174
+ nodes = await client.all(
175
+ kind=kind,
176
+ branch=branch_name,
177
+ exclude=node_schema.attribute_names + node_schema.relationship_names,
178
+ populate_store=False,
179
+ )
180
+
181
+ for node in nodes:
182
+ await get_workflow().submit_workflow(
183
+ workflow=DISPLAY_LABELS_PROCESS_JINJA2,
184
+ context=context,
185
+ parameters={
186
+ "branch_name": branch_name,
187
+ "node_kind": kind,
188
+ "target_kind": kind,
189
+ "object_id": node.id,
190
+ "context": context,
191
+ },
192
+ )
@@ -0,0 +1,22 @@
1
+ from infrahub.events.branch_action import BranchDeletedEvent
2
+ from infrahub.events.schema_action import SchemaUpdatedEvent
3
+ from infrahub.trigger.models import BuiltinTriggerDefinition, EventTrigger, ExecuteWorkflow
4
+ from infrahub.workflows.catalogue import DISPLAY_LABELS_SETUP_JINJA2
5
+
6
+ TRIGGER_DISPLAY_LABELS_ALL_SCHEMA = BuiltinTriggerDefinition(
7
+ name="display-labels-setup-all",
8
+ trigger=EventTrigger(events={SchemaUpdatedEvent.event_name, BranchDeletedEvent.event_name}),
9
+ actions=[
10
+ ExecuteWorkflow(
11
+ workflow=DISPLAY_LABELS_SETUP_JINJA2,
12
+ parameters={
13
+ "branch_name": "{{ event.resource['infrahub.branch.name'] }}",
14
+ "event_name": "{{ event.event }}",
15
+ "context": {
16
+ "__prefect_kind": "json",
17
+ "value": {"__prefect_kind": "jinja", "template": "{{ event.payload['context'] | tojson }}"},
18
+ },
19
+ },
20
+ ),
21
+ ],
22
+ )