infrahub-server 1.6.3__py3-none-any.whl → 1.7.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 (250) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/exceptions.py +2 -2
  3. infrahub/api/schema.py +3 -1
  4. infrahub/artifacts/tasks.py +1 -0
  5. infrahub/auth.py +2 -2
  6. infrahub/cli/db.py +54 -28
  7. infrahub/computed_attribute/gather.py +3 -4
  8. infrahub/computed_attribute/tasks.py +23 -6
  9. infrahub/config.py +8 -0
  10. infrahub/constants/enums.py +12 -0
  11. infrahub/core/account.py +4 -4
  12. infrahub/core/attribute.py +106 -108
  13. infrahub/core/branch/models.py +44 -71
  14. infrahub/core/branch/tasks.py +5 -3
  15. infrahub/core/changelog/diff.py +1 -20
  16. infrahub/core/changelog/models.py +0 -7
  17. infrahub/core/constants/__init__.py +17 -0
  18. infrahub/core/constants/database.py +0 -1
  19. infrahub/core/constants/schema.py +0 -1
  20. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  21. infrahub/core/diff/branch_differ.py +1 -1
  22. infrahub/core/diff/conflict_transferer.py +1 -1
  23. infrahub/core/diff/data_check_synchronizer.py +4 -3
  24. infrahub/core/diff/enricher/cardinality_one.py +2 -2
  25. infrahub/core/diff/enricher/hierarchy.py +1 -1
  26. infrahub/core/diff/enricher/labels.py +1 -1
  27. infrahub/core/diff/merger/merger.py +28 -2
  28. infrahub/core/diff/merger/serializer.py +3 -10
  29. infrahub/core/diff/model/diff.py +1 -1
  30. infrahub/core/diff/query/merge.py +376 -135
  31. infrahub/core/diff/repository/repository.py +3 -1
  32. infrahub/core/graph/__init__.py +1 -1
  33. infrahub/core/graph/constraints.py +3 -3
  34. infrahub/core/graph/schema.py +2 -12
  35. infrahub/core/ipam/reconciler.py +8 -6
  36. infrahub/core/ipam/utilization.py +8 -15
  37. infrahub/core/manager.py +133 -152
  38. infrahub/core/merge.py +1 -1
  39. infrahub/core/metadata/__init__.py +0 -0
  40. infrahub/core/metadata/interface.py +37 -0
  41. infrahub/core/metadata/model.py +31 -0
  42. infrahub/core/metadata/query/__init__.py +0 -0
  43. infrahub/core/metadata/query/node_metadata.py +301 -0
  44. infrahub/core/migrations/graph/__init__.py +4 -0
  45. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
  46. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -12
  47. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  48. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  49. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  50. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  51. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  52. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  53. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  54. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  55. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  56. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +1 -1
  57. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +53 -0
  58. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  59. infrahub/core/migrations/query/__init__.py +2 -2
  60. infrahub/core/migrations/query/attribute_add.py +17 -6
  61. infrahub/core/migrations/query/attribute_remove.py +19 -5
  62. infrahub/core/migrations/query/attribute_rename.py +21 -5
  63. infrahub/core/migrations/query/node_duplicate.py +19 -4
  64. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  65. infrahub/core/migrations/schema/attribute_kind_update.py +21 -2
  66. infrahub/core/migrations/schema/attribute_name_update.py +1 -1
  67. infrahub/core/migrations/schema/attribute_supports_profile.py +5 -3
  68. infrahub/core/migrations/schema/models.py +3 -0
  69. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  70. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  71. infrahub/core/migrations/schema/node_kind_update.py +1 -1
  72. infrahub/core/migrations/schema/node_remove.py +24 -2
  73. infrahub/core/migrations/schema/tasks.py +4 -1
  74. infrahub/core/migrations/shared.py +13 -6
  75. infrahub/core/models.py +6 -6
  76. infrahub/core/node/__init__.py +157 -58
  77. infrahub/core/node/base.py +9 -5
  78. infrahub/core/node/create.py +7 -3
  79. infrahub/core/node/delete_validator.py +1 -1
  80. infrahub/core/node/standard.py +100 -14
  81. infrahub/core/order.py +30 -0
  82. infrahub/core/property.py +0 -1
  83. infrahub/core/protocols.py +1 -0
  84. infrahub/core/protocols_base.py +10 -2
  85. infrahub/core/query/__init__.py +5 -3
  86. infrahub/core/query/attribute.py +164 -49
  87. infrahub/core/query/branch.py +58 -70
  88. infrahub/core/query/delete.py +1 -1
  89. infrahub/core/query/diff.py +7 -7
  90. infrahub/core/query/ipam.py +104 -43
  91. infrahub/core/query/node.py +1072 -281
  92. infrahub/core/query/relationship.py +531 -325
  93. infrahub/core/query/resource_manager.py +107 -18
  94. infrahub/core/query/standard_node.py +25 -5
  95. infrahub/core/query/utils.py +2 -4
  96. infrahub/core/relationship/constraints/count.py +1 -1
  97. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  98. infrahub/core/relationship/constraints/peer_parent.py +1 -1
  99. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  100. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  101. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  102. infrahub/core/relationship/model.py +293 -139
  103. infrahub/core/schema/attribute_schema.py +2 -2
  104. infrahub/core/schema/basenode_schema.py +3 -0
  105. infrahub/core/schema/definitions/core/__init__.py +8 -2
  106. infrahub/core/schema/definitions/core/account.py +10 -10
  107. infrahub/core/schema/definitions/core/artifact.py +14 -8
  108. infrahub/core/schema/definitions/core/check.py +10 -4
  109. infrahub/core/schema/definitions/core/generator.py +26 -6
  110. infrahub/core/schema/definitions/core/graphql_query.py +1 -1
  111. infrahub/core/schema/definitions/core/group.py +9 -2
  112. infrahub/core/schema/definitions/core/ipam.py +80 -10
  113. infrahub/core/schema/definitions/core/menu.py +41 -7
  114. infrahub/core/schema/definitions/core/permission.py +16 -2
  115. infrahub/core/schema/definitions/core/profile.py +16 -2
  116. infrahub/core/schema/definitions/core/propose_change.py +24 -4
  117. infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
  118. infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
  119. infrahub/core/schema/definitions/core/repository.py +10 -0
  120. infrahub/core/schema/definitions/core/resource_pool.py +8 -1
  121. infrahub/core/schema/definitions/core/template.py +19 -2
  122. infrahub/core/schema/definitions/core/transform.py +11 -5
  123. infrahub/core/schema/definitions/core/webhook.py +27 -9
  124. infrahub/core/schema/manager.py +63 -43
  125. infrahub/core/schema/relationship_schema.py +6 -2
  126. infrahub/core/schema/schema_branch.py +48 -10
  127. infrahub/core/task/task.py +4 -2
  128. infrahub/core/utils.py +3 -25
  129. infrahub/core/validators/aggregated_checker.py +1 -1
  130. infrahub/core/validators/attribute/choices.py +1 -1
  131. infrahub/core/validators/attribute/enum.py +1 -1
  132. infrahub/core/validators/attribute/kind.py +1 -1
  133. infrahub/core/validators/attribute/length.py +1 -1
  134. infrahub/core/validators/attribute/min_max.py +1 -1
  135. infrahub/core/validators/attribute/number_pool.py +1 -1
  136. infrahub/core/validators/attribute/optional.py +1 -1
  137. infrahub/core/validators/attribute/regex.py +1 -1
  138. infrahub/core/validators/determiner.py +3 -3
  139. infrahub/core/validators/node/attribute.py +1 -1
  140. infrahub/core/validators/node/relationship.py +1 -1
  141. infrahub/core/validators/relationship/peer.py +1 -1
  142. infrahub/database/__init__.py +4 -4
  143. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  144. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  145. infrahub/dependencies/registry.py +2 -0
  146. infrahub/display_labels/tasks.py +12 -3
  147. infrahub/git/integrator.py +18 -18
  148. infrahub/git/tasks.py +1 -1
  149. infrahub/git/utils.py +1 -1
  150. infrahub/graphql/constants.py +3 -0
  151. infrahub/graphql/context.py +1 -1
  152. infrahub/graphql/field_extractor.py +1 -1
  153. infrahub/graphql/initialization.py +11 -0
  154. infrahub/graphql/loaders/account.py +134 -0
  155. infrahub/graphql/loaders/node.py +5 -12
  156. infrahub/graphql/loaders/peers.py +5 -7
  157. infrahub/graphql/manager.py +175 -21
  158. infrahub/graphql/metadata.py +91 -0
  159. infrahub/graphql/mutations/account.py +6 -6
  160. infrahub/graphql/mutations/attribute.py +0 -2
  161. infrahub/graphql/mutations/branch.py +9 -5
  162. infrahub/graphql/mutations/computed_attribute.py +1 -1
  163. infrahub/graphql/mutations/display_label.py +1 -1
  164. infrahub/graphql/mutations/hfid.py +1 -1
  165. infrahub/graphql/mutations/ipam.py +4 -6
  166. infrahub/graphql/mutations/main.py +9 -4
  167. infrahub/graphql/mutations/profile.py +16 -22
  168. infrahub/graphql/mutations/proposed_change.py +4 -4
  169. infrahub/graphql/mutations/relationship.py +40 -10
  170. infrahub/graphql/mutations/repository.py +14 -12
  171. infrahub/graphql/mutations/schema.py +2 -2
  172. infrahub/graphql/order.py +14 -0
  173. infrahub/graphql/queries/branch.py +62 -6
  174. infrahub/graphql/queries/resource_manager.py +25 -24
  175. infrahub/graphql/resolvers/account_metadata.py +84 -0
  176. infrahub/graphql/resolvers/ipam.py +6 -8
  177. infrahub/graphql/resolvers/many_relationship.py +77 -35
  178. infrahub/graphql/resolvers/resolver.py +59 -14
  179. infrahub/graphql/resolvers/single_relationship.py +87 -23
  180. infrahub/graphql/subscription/graphql_query.py +2 -0
  181. infrahub/graphql/types/__init__.py +0 -1
  182. infrahub/graphql/types/attribute.py +10 -5
  183. infrahub/graphql/types/branch.py +40 -53
  184. infrahub/graphql/types/enums.py +3 -0
  185. infrahub/graphql/types/metadata.py +28 -0
  186. infrahub/graphql/types/node.py +22 -2
  187. infrahub/graphql/types/relationship.py +10 -2
  188. infrahub/graphql/types/standard_node.py +12 -7
  189. infrahub/hfid/tasks.py +12 -3
  190. infrahub/lock.py +7 -0
  191. infrahub/menu/repository.py +1 -1
  192. infrahub/patch/queries/base.py +1 -1
  193. infrahub/pools/number.py +1 -8
  194. infrahub/profiles/gather.py +56 -0
  195. infrahub/profiles/mandatory_fields_checker.py +116 -0
  196. infrahub/profiles/models.py +66 -0
  197. infrahub/profiles/node_applier.py +154 -13
  198. infrahub/profiles/queries/get_profile_data.py +143 -31
  199. infrahub/profiles/tasks.py +79 -27
  200. infrahub/profiles/triggers.py +22 -0
  201. infrahub/proposed_change/action_checker.py +1 -1
  202. infrahub/proposed_change/tasks.py +4 -1
  203. infrahub/services/__init__.py +1 -1
  204. infrahub/services/adapters/cache/nats.py +1 -1
  205. infrahub/services/adapters/cache/redis.py +7 -0
  206. infrahub/tasks/artifact.py +1 -0
  207. infrahub/transformations/tasks.py +2 -2
  208. infrahub/trigger/catalogue.py +2 -0
  209. infrahub/trigger/models.py +1 -0
  210. infrahub/trigger/setup.py +3 -3
  211. infrahub/trigger/tasks.py +3 -0
  212. infrahub/validators/tasks.py +1 -0
  213. infrahub/webhook/gather.py +1 -1
  214. infrahub/webhook/models.py +1 -1
  215. infrahub/webhook/tasks.py +23 -7
  216. infrahub/workers/dependencies.py +9 -3
  217. infrahub/workers/infrahub_async.py +13 -4
  218. infrahub/workflows/catalogue.py +19 -0
  219. infrahub_sdk/analyzer.py +2 -2
  220. infrahub_sdk/branch.py +12 -39
  221. infrahub_sdk/checks.py +4 -4
  222. infrahub_sdk/client.py +36 -0
  223. infrahub_sdk/ctl/cli_commands.py +2 -1
  224. infrahub_sdk/ctl/graphql.py +15 -4
  225. infrahub_sdk/ctl/utils.py +2 -2
  226. infrahub_sdk/enums.py +6 -0
  227. infrahub_sdk/graphql/renderers.py +21 -0
  228. infrahub_sdk/graphql/utils.py +85 -0
  229. infrahub_sdk/node/attribute.py +12 -2
  230. infrahub_sdk/node/constants.py +12 -0
  231. infrahub_sdk/node/metadata.py +69 -0
  232. infrahub_sdk/node/node.py +65 -14
  233. infrahub_sdk/node/property.py +3 -0
  234. infrahub_sdk/node/related_node.py +37 -5
  235. infrahub_sdk/node/relationship.py +18 -1
  236. infrahub_sdk/operation.py +2 -2
  237. infrahub_sdk/schema/repository.py +1 -2
  238. infrahub_sdk/transforms.py +2 -2
  239. infrahub_sdk/types.py +18 -2
  240. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
  241. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +249 -228
  242. infrahub_testcontainers/container.py +3 -3
  243. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  244. infrahub_testcontainers/docker-compose.test.yml +13 -5
  245. infrahub_testcontainers/models.py +3 -3
  246. infrahub_testcontainers/performance_test.py +1 -1
  247. infrahub/graphql/models.py +0 -6
  248. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
  249. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
  250. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,33 +1,67 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Any
3
3
 
4
- from infrahub.core.constants import NULL_VALUE
4
+ from infrahub.core.constants import NULL_VALUE, RelationshipDirection
5
5
  from infrahub.core.query import Query, QueryType
6
6
  from infrahub.database import InfrahubDatabase
7
7
 
8
8
 
9
+ @dataclass
10
+ class RelationshipFilter:
11
+ relationship_identifier: str
12
+ direction: RelationshipDirection
13
+
14
+ def __hash__(self) -> int:
15
+ return hash((self.relationship_identifier, self.direction))
16
+
17
+
9
18
  @dataclass
10
19
  class ProfileData:
11
20
  uuid: str
12
21
  priority: float | int
13
22
  attribute_values: dict[str, Any]
23
+ relationship_peers: dict[RelationshipFilter, list[str]]
14
24
 
15
25
 
16
26
  class GetProfileDataQuery(Query):
17
27
  type: QueryType = QueryType.READ
18
28
  insert_return: bool = False
19
29
 
20
- def __init__(self, *args: Any, profile_ids: list[str], attr_names: list[str], **kwargs: Any):
30
+ def __init__(
31
+ self,
32
+ *args: Any,
33
+ profile_ids: list[str],
34
+ attr_names: list[str],
35
+ relationship_filters: list[RelationshipFilter] | None = None,
36
+ **kwargs: Any,
37
+ ) -> None:
21
38
  super().__init__(*args, **kwargs)
22
39
  self.profile_ids = profile_ids
23
40
  self.attr_names = attr_names
41
+ self.relationship_filters = relationship_filters or []
24
42
 
25
43
  async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
26
- branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
44
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
27
45
  self.params.update(branch_params)
28
46
  self.params["profile_ids"] = self.profile_ids
29
47
  self.params["attr_names"] = self.attr_names + ["profile_priority"]
30
48
 
49
+ # Prepare relationship filters
50
+ outbound_identifiers = []
51
+ inbound_identifiers = []
52
+ bidirectional_identifiers = []
53
+ for rf in self.relationship_filters:
54
+ if rf.direction == RelationshipDirection.OUTBOUND:
55
+ outbound_identifiers.append(rf.relationship_identifier)
56
+ elif rf.direction == RelationshipDirection.INBOUND:
57
+ inbound_identifiers.append(rf.relationship_identifier)
58
+ elif rf.direction == RelationshipDirection.BIDIR:
59
+ bidirectional_identifiers.append(rf.relationship_identifier)
60
+
61
+ self.params["outbound_identifiers"] = outbound_identifiers
62
+ self.params["inbound_identifiers"] = inbound_identifiers
63
+ self.params["bidirectional_identifiers"] = bidirectional_identifiers
64
+
31
65
  query = """
32
66
  // --------------
33
67
  // get the Profile nodes
@@ -48,51 +82,129 @@ WHERE is_active = TRUE
48
82
  // --------------
49
83
  // get the attributes that we care about
50
84
  // --------------
51
- MATCH (profile)-[:HAS_ATTRIBUTE]-(attr:Attribute)
85
+ OPTIONAL MATCH (profile)-[:HAS_ATTRIBUTE]-(attr:Attribute)
52
86
  WHERE attr.name IN $attr_names
53
87
  WITH DISTINCT profile, attr
54
88
  CALL (profile, attr) {
55
- MATCH (profile)-[r:HAS_ATTRIBUTE]->(attr)
89
+ OPTIONAL MATCH (profile)-[r:HAS_ATTRIBUTE]->(attr)
56
90
  WHERE %(branch_filter)s
57
91
  ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
58
- RETURN r.status = "active" AS is_active
92
+ RETURN r.status = "active" AS r1_is_active
59
93
  }
60
- WITH profile, attr
61
- WHERE is_active = TRUE
62
94
  // --------------
63
95
  // get the attribute values
64
96
  // --------------
65
97
  CALL (attr) {
66
- MATCH (attr)-[r:HAS_VALUE]->(av)
98
+ OPTIONAL MATCH (attr)-[r:HAS_VALUE]->(av)
67
99
  WHERE %(branch_filter)s
68
100
  ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
69
- RETURN av, r.status = "active" AS is_active
101
+ RETURN av, r.status = "active" AS r2_is_active
70
102
  LIMIT 1
71
103
  }
72
- WITH profile, attr, av
73
- WHERE is_active = TRUE
74
- RETURN profile.uuid AS profile_uuid, attr.name AS attr_name, av.value AS attr_value
104
+ // --------------
105
+ // filter out null and inactive attributes
106
+ // --------------
107
+ WITH profile, CASE
108
+ WHEN attr IS NOT NULL AND av IS NOT NULL AND r1_is_active = TRUE AND r2_is_active = TRUE THEN [attr.name, av.value]
109
+ ELSE NULL
110
+ END AS attribute_details
111
+ WITH profile, collect(attribute_details) AS attributes
112
+ // --------------
113
+ // get all possible relationships we might want for this profile
114
+ // --------------
115
+ OPTIONAL MATCH (profile)-[r:IS_RELATED]-(rel:Relationship)
116
+ WHERE rel.name IN $outbound_identifiers + $bidirectional_identifiers + $inbound_identifiers
117
+ AND %(branch_filter)s
118
+ WITH DISTINCT profile, attributes, rel
119
+ // --------------
120
+ // filter to active near-side relationships with names and directions we want
121
+ // --------------
122
+ CALL (profile, rel) {
123
+ OPTIONAL MATCH (profile)-[r:IS_RELATED]-(rel)
124
+ WHERE (
125
+ (rel.name IN $outbound_identifiers AND startNode(r) = profile)
126
+ OR (rel.name IN $bidirectional_identifiers AND startNode(r) = profile)
127
+ OR (rel.name IN $inbound_identifiers AND startNode(r) = rel)
128
+ )
129
+ AND %(branch_filter)s
130
+ RETURN r AS r1
131
+ ORDER BY r1.branch_level DESC, r1.from DESC, r1.status ASC
132
+ LIMIT 1
133
+ }
134
+ WITH profile, attributes, r1, rel
135
+ // --------------
136
+ // filter to active far-side relationships with names and directions we want
137
+ // --------------
138
+ CALL (profile, rel) {
139
+ OPTIONAL MATCH (rel)-[r:IS_RELATED]-(peer)
140
+ WHERE peer <> profile
141
+ AND (
142
+ (rel.name IN $outbound_identifiers AND startNode(r) = rel)
143
+ OR (rel.name IN $bidirectional_identifiers AND startNode(r) = peer)
144
+ OR (rel.name IN $inbound_identifiers AND startNode(r) = peer)
145
+ )
146
+ AND %(branch_filter)s
147
+ RETURN r AS r2, peer
148
+ ORDER BY r2.branch_level DESC, r2.from DESC, r2.status ASC
149
+ LIMIT 1
150
+ }
151
+ WITH profile, attributes, r1, rel, r2, peer
152
+ // --------------
153
+ // save the direction of the relationship
154
+ // --------------
155
+ WITH *, CASE
156
+ WHEN r1 IS NULL OR r2 IS NULL THEN NULL
157
+ WHEN startNode(r1) = profile AND startNode(r2) = rel THEN "outbound"
158
+ WHEN startNode(r1) = rel AND startNode(r2) = peer THEN "inbound"
159
+ ELSE "bidirectional"
160
+ END AS direction
161
+ // --------------
162
+ // filter out null and inactive relationships
163
+ // --------------
164
+ WITH profile, attributes, CASE
165
+ WHEN rel IS NOT NULL AND peer IS NOT NULL AND r1.status = "active" AND r2.status = "active" THEN [rel.name, direction, peer.uuid]
166
+ ELSE NULL
167
+ END AS relationship_details
168
+ WITH profile, attributes, collect(relationship_details) AS relationships
169
+ RETURN profile.uuid AS profile_uuid, attributes, relationships
75
170
  """ % {"branch_filter": branch_filter}
76
171
  self.add_to_query(query)
77
- self.return_labels = ["profile_uuid", "attr_name", "attr_value"]
172
+ self.return_labels = ["profile_uuid", "attributes", "relationships"]
78
173
 
79
174
  def get_profile_data(self) -> list[ProfileData]:
80
- profile_data_by_uuid: dict[str, ProfileData] = {}
175
+ profile_data_list: list[ProfileData] = []
81
176
  for result in self.results:
82
177
  profile_uuid = result.get_as_type(label="profile_uuid", return_type=str)
83
- if profile_uuid not in profile_data_by_uuid:
84
- profile_data_by_uuid[profile_uuid] = ProfileData(
85
- uuid=profile_uuid, priority=float("inf"), attribute_values={}
86
- )
87
- profile_data = profile_data_by_uuid[profile_uuid]
88
- attr_name = result.get_as_type(label="attr_name", return_type=str)
89
- attr_value: Any = result.get(label="attr_value")
90
- if attr_value == NULL_VALUE:
91
- attr_value = None
92
- if attr_name == "profile_priority":
93
- if attr_value is not None and not isinstance(attr_value, int):
94
- attr_value = int(attr_value)
95
- profile_data.priority = attr_value
96
- else:
97
- profile_data.attribute_values[attr_name] = attr_value
98
- return list(profile_data_by_uuid.values())
178
+ attributes = result.get(label="attributes")
179
+ relationships = result.get(label="relationships")
180
+
181
+ profile_data = ProfileData(
182
+ uuid=profile_uuid, priority=float("inf"), attribute_values={}, relationship_peers={}
183
+ )
184
+
185
+ for attr_pair in attributes:
186
+ if not isinstance(attr_pair, list) or len(attr_pair) != 2:
187
+ continue
188
+ attr_name, attr_value = attr_pair
189
+ if attr_value == NULL_VALUE:
190
+ attr_value = None
191
+ if attr_name == "profile_priority":
192
+ if attr_value is not None and not isinstance(attr_value, int):
193
+ attr_value = int(attr_value)
194
+ profile_data.priority = attr_value
195
+ else:
196
+ profile_data.attribute_values[attr_name] = attr_value
197
+
198
+ # Parse relationships
199
+ for rel_tuple in relationships:
200
+ if not isinstance(rel_tuple, list) or len(rel_tuple) != 3:
201
+ continue
202
+ rel_name, direction_str, peer_uuid = rel_tuple
203
+ direction = RelationshipDirection(direction_str)
204
+ rel_filter = RelationshipFilter(relationship_identifier=rel_name, direction=direction)
205
+ if rel_filter not in profile_data.relationship_peers:
206
+ profile_data.relationship_peers[rel_filter] = []
207
+ profile_data.relationship_peers[rel_filter].append(peer_uuid)
208
+
209
+ profile_data_list.append(profile_data)
210
+ return profile_data_list
@@ -1,11 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from prefect import flow
4
6
  from prefect.logging import get_run_logger
5
7
 
6
- from infrahub.workers.dependencies import get_client, get_workflow
8
+ from infrahub.context import InfrahubContext # noqa: TC001 needed for prefect flow
9
+ from infrahub.trigger.models import TriggerSetupReport, TriggerType
10
+ from infrahub.trigger.setup import setup_triggers_specific
11
+ from infrahub.workers.dependencies import get_client, get_component, get_database, get_workflow
7
12
  from infrahub.workflows.catalogue import PROFILE_REFRESH
8
- from infrahub.workflows.utils import add_tags
13
+ from infrahub.workflows.utils import add_tags, wait_for_schema_to_converge
14
+
15
+ from .gather import gather_trigger_profile_refresh
16
+
17
+ if TYPE_CHECKING:
18
+ from infrahub_sdk.node.relationship import RelationshipManager
9
19
 
10
20
  REFRESH_PROFILES_MUTATION = """
11
21
  mutation RefreshProfiles(
@@ -20,44 +30,86 @@ mutation RefreshProfiles(
20
30
  """
21
31
 
22
32
 
23
- @flow(
24
- name="object-profiles-refresh",
25
- flow_run_name="Refresh profiles for {node_id}",
26
- )
27
- async def object_profiles_refresh(
28
- branch_name: str,
29
- node_id: str,
30
- ) -> None:
33
+ @flow(name="object-profiles-refresh", flow_run_name="Refresh profiles for {node_id}")
34
+ async def object_profiles_refresh(branch_name: str, node_id: str) -> None:
31
35
  log = get_run_logger()
32
36
  client = get_client()
33
37
 
34
38
  await add_tags(branches=[branch_name], nodes=[node_id], db_change=True)
35
- await client.execute_graphql(
36
- query=REFRESH_PROFILES_MUTATION,
37
- variables={"id": node_id},
38
- branch_name=branch_name,
39
- )
39
+ await client.execute_graphql(query=REFRESH_PROFILES_MUTATION, variables={"id": node_id}, branch_name=branch_name)
40
40
  log.info(f"Profiles refreshed for {node_id}")
41
41
 
42
42
 
43
- @flow(
44
- name="objects-profiles-refresh-multiple",
45
- flow_run_name="Refresh profiles for multiple objects",
46
- )
47
- async def objects_profiles_refresh_multiple(
43
+ @flow(name="objects-profiles-refresh-multiple", flow_run_name="Refresh profiles for multiple objects")
44
+ async def objects_profiles_refresh_multiple(branch_name: str, node_ids: list[str]) -> None:
45
+ log = get_run_logger()
46
+
47
+ await add_tags(branches=[branch_name])
48
+
49
+ for node_id in node_ids:
50
+ log.info(f"Requesting profile refresh for {node_id}")
51
+ await get_workflow().submit_workflow(
52
+ workflow=PROFILE_REFRESH, parameters={"branch_name": branch_name, "node_id": node_id}
53
+ )
54
+
55
+
56
+ @flow(name="profile-refresh-setup", flow_run_name="Setup profile refresh triggers")
57
+ async def profile_refresh_setup(
58
+ context: InfrahubContext, # noqa: ARG001
59
+ branch_name: str | None = None,
60
+ event_name: str | None = None, # noqa: ARG001
61
+ ) -> None:
62
+ """Setup Prefect automations for profile refresh triggers.
63
+
64
+ This flow is triggered by schema changes and sets up automations that will
65
+ listen for profile updates. When a profile's attributes or relationships
66
+ change, the corresponding automation will trigger profile refresh for all
67
+ related nodes.
68
+ """
69
+ database = await get_database()
70
+ async with database.start_session() as db:
71
+ log = get_run_logger()
72
+
73
+ if branch_name:
74
+ await add_tags(branches=[branch_name])
75
+ component = await get_component()
76
+ await wait_for_schema_to_converge(branch_name=branch_name, component=component, db=db, log=log)
77
+
78
+ report: TriggerSetupReport = await setup_triggers_specific(
79
+ gatherer=gather_trigger_profile_refresh, trigger_type=TriggerType.PROFILE
80
+ ) # type: ignore[misc]
81
+
82
+ log.info(f"{report.in_use_count} Profile refresh automation configuration completed")
83
+
84
+
85
+ @flow(name="profile-refresh-process", flow_run_name="Process profile refresh for {profile_kind}")
86
+ async def profile_refresh_process(
48
87
  branch_name: str,
49
- node_ids: list[str],
88
+ profile_kind: str,
89
+ profile_id: str,
90
+ context: InfrahubContext, # noqa: ARG001
50
91
  ) -> None:
92
+ """Process profile refresh when a profile's attributes or relationships change.
93
+
94
+ This flow fetches all nodes related to the profile via the `related_nodes`
95
+ relationship and submits profile refresh workflows for each of them.
96
+ """
51
97
  log = get_run_logger()
98
+ client = get_client()
52
99
 
53
100
  await add_tags(branches=[branch_name])
54
101
 
55
- for node_id in node_ids:
102
+ profile = await client.get(kind=profile_kind, id=profile_id, branch=branch_name, include=["related_nodes"])
103
+ related_nodes: RelationshipManager = profile.related_nodes # type: ignore
104
+
105
+ if not related_nodes.peer_ids:
106
+ log.info(f"No related nodes found for profile {profile_id}")
107
+ return
108
+
109
+ log.info(f"Found {len(related_nodes.peer_ids)} related nodes for profile {profile_id}")
110
+
111
+ for node_id in related_nodes.peer_ids:
56
112
  log.info(f"Requesting profile refresh for {node_id}")
57
113
  await get_workflow().submit_workflow(
58
- workflow=PROFILE_REFRESH,
59
- parameters={
60
- "branch_name": branch_name,
61
- "node_id": node_id,
62
- },
114
+ workflow=PROFILE_REFRESH, parameters={"branch_name": branch_name, "node_id": node_id}
63
115
  )
@@ -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 PROFILE_REFRESH_SETUP
5
+
6
+ TRIGGER_PROFILE_REFRESH_SETUP = BuiltinTriggerDefinition(
7
+ name="profile-refresh-setup-all",
8
+ trigger=EventTrigger(events={SchemaUpdatedEvent.event_name, BranchDeletedEvent.event_name}),
9
+ actions=[
10
+ ExecuteWorkflow(
11
+ workflow=PROFILE_REFRESH_SETUP,
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
+ )
@@ -125,7 +125,7 @@ class ActionRule:
125
125
 
126
126
 
127
127
  class ActionRulesEvaluator:
128
- def __init__(self, rules: list[ActionRule]):
128
+ def __init__(self, rules: list[ActionRule]) -> None:
129
129
  self.rules = rules
130
130
 
131
131
  async def evaluate(
@@ -622,6 +622,7 @@ async def validate_artifacts_generation(model: RequestArtifactDefinitionCheck, c
622
622
 
623
623
  log = get_run_logger()
624
624
  client = get_client()
625
+ client.request_context = context.to_request_context()
625
626
 
626
627
  artifact_definition = await client.get(
627
628
  kind=CoreArtifactDefinition,
@@ -781,6 +782,7 @@ async def run_generator_as_check(model: RunGeneratorAsCheckModel, context: Infra
781
782
  await add_tags(branches=[model.branch_name], nodes=[model.proposed_change], db_change=True)
782
783
 
783
784
  client = get_client()
785
+ client.request_context = context.to_request_context()
784
786
  log = get_run_logger()
785
787
 
786
788
  repository = await get_initialized_repo(
@@ -859,7 +861,7 @@ async def run_generator_as_check(model: RunGeneratorAsCheckModel, context: Infra
859
861
  if check:
860
862
  check.created_at.value = Timestamp().to_string()
861
863
  check.conclusion.value = conclusion.value
862
- await check.save()
864
+ await check.save(request_context=context.to_request_context())
863
865
  else:
864
866
  check = await client.create(
865
867
  kind=InfrahubKind.GENERATORCHECK,
@@ -925,6 +927,7 @@ async def request_generator_definition_check(model: RequestGeneratorDefinitionCh
925
927
 
926
928
  log = get_run_logger()
927
929
  client = get_client()
930
+ client.request_context = context.to_request_context()
928
931
 
929
932
  proposed_change = await client.get(kind=InfrahubKind.PROPOSEDCHANGE, id=model.proposed_change)
930
933
 
@@ -54,7 +54,7 @@ class InfrahubServices:
54
54
  message_bus: InfrahubMessageBus | None = None,
55
55
  workflow: InfrahubWorkflow | None = None,
56
56
  component: InfrahubComponent | None = None,
57
- ):
57
+ ) -> None:
58
58
  """
59
59
  This method should not be called directly, use `new` instead for a proper initialization.
60
60
  """
@@ -23,7 +23,7 @@ class NATSCache(InfrahubCache):
23
23
  jetstream: nats.js.JetStreamContext,
24
24
  kv: dict[int, nats.js.kv.KeyValue],
25
25
  kv_buckets: dict[str, KVTTL],
26
- ):
26
+ ) -> None:
27
27
  self.connection = connection
28
28
  self.jetstream = jetstream
29
29
  self.kv = kv
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  import redis.asyncio as redis
6
+ from redis import UsernamePasswordCredentialProvider
6
7
 
7
8
  from infrahub import config
8
9
  from infrahub.services.adapters.cache import InfrahubCache
@@ -13,10 +14,16 @@ if TYPE_CHECKING:
13
14
 
14
15
  class RedisCache(InfrahubCache):
15
16
  def __init__(self) -> None:
17
+ credential_provider: UsernamePasswordCredentialProvider | None = None
18
+ if config.SETTINGS.cache.username and config.SETTINGS.cache.password:
19
+ credential_provider = UsernamePasswordCredentialProvider(
20
+ username=config.SETTINGS.cache.username, password=config.SETTINGS.cache.password
21
+ )
16
22
  self.connection = redis.Redis(
17
23
  host=config.SETTINGS.cache.address,
18
24
  port=config.SETTINGS.cache.service_port,
19
25
  db=config.SETTINGS.cache.database,
26
+ credential_provider=credential_provider,
20
27
  ssl=config.SETTINGS.cache.tls_enabled,
21
28
  ssl_cert_reqs="optional" if not config.SETTINGS.cache.tls_insecure else "none",
22
29
  ssl_check_hostname=not config.SETTINGS.cache.tls_insecure,
@@ -13,6 +13,7 @@ from infrahub.workers.dependencies import get_client
13
13
  async def define_artifact(model: CheckArtifactCreate | RequestArtifactGenerate) -> tuple[InfrahubNode, bool]:
14
14
  """Return an artifact together with a flag to indicate if the artifact is created now or already existed."""
15
15
  client = get_client()
16
+ client.request_context = model.context.to_request_context()
16
17
  created = False
17
18
  if model.artifact_id:
18
19
  artifact = await client.get(kind=InfrahubKind.ARTIFACT, id=model.artifact_id, branch=model.branch_name)
@@ -33,7 +33,7 @@ async def transform_python(message: TransformPythonData) -> Any:
33
33
  location=message.transform_location,
34
34
  data=message.data,
35
35
  convert_query_response=message.convert_query_response,
36
- ) # type: ignore[misc]
36
+ ) # type: ignore[call-overload]
37
37
 
38
38
  return transformed_data
39
39
 
@@ -54,6 +54,6 @@ async def transform_render_jinja2_template(message: TransformJinjaTemplateData)
54
54
 
55
55
  rendered_template = await repo.render_jinja2_template.with_options(timeout_seconds=message.timeout)(
56
56
  commit=message.commit, location=message.template_location, data={"data": message.data}
57
- ) # type: ignore[misc]
57
+ ) # type: ignore[call-overload]
58
58
 
59
59
  return rendered_template
@@ -6,6 +6,7 @@ from infrahub.computed_attribute.triggers import (
6
6
  )
7
7
  from infrahub.display_labels.triggers import TRIGGER_DISPLAY_LABELS_ALL_SCHEMA
8
8
  from infrahub.hfid.triggers import TRIGGER_HFID_ALL_SCHEMA
9
+ from infrahub.profiles.triggers import TRIGGER_PROFILE_REFRESH_SETUP
9
10
  from infrahub.schema.triggers import TRIGGER_SCHEMA_UPDATED
10
11
  from infrahub.trigger.models import TriggerDefinition
11
12
  from infrahub.webhook.triggers import TRIGGER_WEBHOOK_DELETE, TRIGGER_WEBHOOK_SETUP_UPDATE
@@ -17,6 +18,7 @@ builtin_triggers: list[TriggerDefinition] = [
17
18
  TRIGGER_COMPUTED_ATTRIBUTE_PYTHON_SETUP_COMMIT,
18
19
  TRIGGER_DISPLAY_LABELS_ALL_SCHEMA,
19
20
  TRIGGER_HFID_ALL_SCHEMA,
21
+ TRIGGER_PROFILE_REFRESH_SETUP,
20
22
  TRIGGER_SCHEMA_UPDATED,
21
23
  TRIGGER_WEBHOOK_DELETE,
22
24
  TRIGGER_WEBHOOK_SETUP_UPDATE,
@@ -101,6 +101,7 @@ class TriggerType(StrEnum):
101
101
  COMPUTED_ATTR_PYTHON_QUERY = "computed_attr_python_query"
102
102
  DISPLAY_LABEL_JINJA2 = "display_label_jinja2"
103
103
  HUMAN_FRIENDLY_ID = "human_friendly_id"
104
+ PROFILE = "profile"
104
105
  # OBJECT = "object"
105
106
 
106
107
  @property
infrahub/trigger/setup.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Awaitable, Callable
1
+ from typing import TYPE_CHECKING, Awaitable, Callable, Sequence
2
2
 
3
3
  from prefect import get_run_logger, task
4
4
  from prefect.automations import AutomationCore
@@ -46,7 +46,7 @@ def compare_automations(
46
46
 
47
47
  @task(name="trigger-setup-specific", task_run_name="Setup triggers of a specific kind", cache_policy=NONE) # type: ignore[arg-type]
48
48
  async def setup_triggers_specific(
49
- gatherer: Callable[[InfrahubDatabase | None], Awaitable[list[TriggerDefinition]]],
49
+ gatherer: Callable[[InfrahubDatabase | None], Awaitable[Sequence[TriggerDefinition]]],
50
50
  trigger_type: TriggerType,
51
51
  db: InfrahubDatabase | None = None,
52
52
  ) -> TriggerSetupReport:
@@ -69,7 +69,7 @@ async def setup_triggers_specific(
69
69
  @task(name="trigger-setup", task_run_name="Setup triggers", cache_policy=NONE)
70
70
  async def setup_triggers(
71
71
  client: PrefectClient,
72
- triggers: list[TriggerDefinition],
72
+ triggers: Sequence[TriggerDefinition],
73
73
  trigger_type: TriggerType | None = None,
74
74
  force_update: bool = False,
75
75
  ) -> TriggerSetupReport:
infrahub/trigger/tasks.py CHANGED
@@ -8,6 +8,7 @@ from infrahub.computed_attribute.gather import (
8
8
  )
9
9
  from infrahub.display_labels.gather import gather_trigger_display_labels_jinja2
10
10
  from infrahub.hfid.gather import gather_trigger_hfid
11
+ from infrahub.profiles.gather import gather_trigger_profile_refresh
11
12
  from infrahub.trigger.catalogue import builtin_triggers
12
13
  from infrahub.webhook.gather import gather_trigger_webhook
13
14
  from infrahub.workers.dependencies import get_database
@@ -28,6 +29,7 @@ async def trigger_configure_all() -> None:
28
29
  computed_attribute_python_query_triggers,
29
30
  ) = await gather_trigger_computed_attribute_python(db=db)
30
31
  action_rules = await gather_trigger_action_rules(db=db)
32
+ profile_refresh_triggers = await gather_trigger_profile_refresh()
31
33
  triggers = (
32
34
  computed_attribute_j2_triggers
33
35
  + computed_attribute_python_triggers
@@ -35,6 +37,7 @@ async def trigger_configure_all() -> None:
35
37
  + display_label_triggers
36
38
  + human_friendly_id_triggers
37
39
  + builtin_triggers
40
+ + profile_refresh_triggers
38
41
  + webhook_trigger
39
42
  + action_rules
40
43
  )
@@ -29,6 +29,7 @@ async def start_validator[ValidatorType: CoreValidator](
29
29
  validator = cast("ValidatorType", validator)
30
30
  else:
31
31
  data["proposed_change"] = proposed_change
32
+ client.request_context = context.to_request_context()
32
33
  validator = await client.create(kind=validator_type, data=data)
33
34
  await validator.save()
34
35
 
@@ -13,5 +13,5 @@ from .models import WebhookTriggerDefinition
13
13
  @task(name="gather-trigger-webhook", task_run_name="Gather webhook triggers", cache_policy=NONE)
14
14
  async def gather_trigger_webhook(db: InfrahubDatabase) -> list[WebhookTriggerDefinition]:
15
15
  webhooks = await NodeManager.query(db=db, schema=CoreWebhook)
16
- triggers = [WebhookTriggerDefinition.from_object(webhook) for webhook in webhooks]
16
+ triggers = [WebhookTriggerDefinition.from_object(webhook) for webhook in webhooks if webhook.active.value]
17
17
  return triggers
@@ -233,7 +233,7 @@ class TransformWebhook(Webhook):
233
233
  convert_query_response=self.convert_query_response,
234
234
  data={"data": {"data": data, **context.model_dump()}},
235
235
  client=client,
236
- ) # type: ignore[misc]
236
+ ) # type: ignore[call-overload]
237
237
 
238
238
  @classmethod
239
239
  def from_object(cls, obj: CoreCustomWebhook, transform: CoreTransformPython) -> Self: