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
@@ -2,18 +2,23 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Mapping
4
4
 
5
+ from infrahub import lock
5
6
  from infrahub.core import registry
6
7
  from infrahub.core.constants import RelationshipCardinality, RelationshipKind
7
8
  from infrahub.core.constraint.node.runner import NodeConstraintRunner
8
- from infrahub.core.manager import NodeManager
9
9
  from infrahub.core.node import Node
10
+ from infrahub.core.node.lock_utils import get_lock_names_on_object_mutation
10
11
  from infrahub.core.protocols import CoreObjectTemplate
12
+ from infrahub.core.schema import GenericSchema
11
13
  from infrahub.dependencies.registry import get_component_registry
14
+ from infrahub.lock import InfrahubMultiLock
15
+ from infrahub.profiles.node_applier import NodeProfilesApplier
12
16
 
13
17
  if TYPE_CHECKING:
14
18
  from infrahub.core.branch import Branch
15
19
  from infrahub.core.relationship.model import RelationshipManager
16
20
  from infrahub.core.schema import MainSchemaTypes, NonGenericSchemaTypes, RelationshipSchema
21
+ from infrahub.core.timestamp import Timestamp
17
22
  from infrahub.database import InfrahubDatabase
18
23
 
19
24
 
@@ -87,6 +92,7 @@ async def handle_template_relationships(
87
92
  template: CoreObjectTemplate,
88
93
  fields: list,
89
94
  constraint_runner: NodeConstraintRunner | None = None,
95
+ at: Timestamp | None = None,
90
96
  ) -> None:
91
97
  if constraint_runner is None:
92
98
  component_registry = get_component_registry()
@@ -114,7 +120,7 @@ async def handle_template_relationships(
114
120
  current_template=template,
115
121
  )
116
122
 
117
- obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch)
123
+ obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch, at=at)
118
124
  await obj_peer.new(db=db, **obj_peer_data)
119
125
  await constraint_runner.check(node=obj_peer, field_filters=list(obj_peer_data))
120
126
  await obj_peer.save(db=db)
@@ -126,6 +132,7 @@ async def handle_template_relationships(
126
132
  obj=obj_peer,
127
133
  template=template_relationship_peer,
128
134
  fields=fields,
135
+ at=at,
129
136
  )
130
137
 
131
138
 
@@ -136,43 +143,20 @@ async def get_profile_ids(db: InfrahubDatabase, obj: Node) -> set[str]:
136
143
  return {pr.peer_id for pr in profile_rels}
137
144
 
138
145
 
139
- async def refresh_for_profile_update(
140
- db: InfrahubDatabase,
141
- branch: Branch,
142
- obj: Node,
143
- schema: NonGenericSchemaTypes,
144
- previous_profile_ids: set[str] | None = None,
145
- ) -> Node:
146
- if not hasattr(obj, "profiles"):
147
- return obj
148
- current_profile_ids = await get_profile_ids(db=db, obj=obj)
149
- if previous_profile_ids is None or previous_profile_ids != current_profile_ids:
150
- refreshed_node = await NodeManager.get_one_by_id_or_default_filter(
151
- db=db,
152
- kind=schema.kind,
153
- id=obj.get_id(),
154
- branch=branch,
155
- include_owner=True,
156
- include_source=True,
157
- )
158
- refreshed_node._node_changelog = obj.node_changelog
159
- return refreshed_node
160
- return obj
161
-
162
-
163
146
  async def _do_create_node(
164
147
  node_class: type[Node],
148
+ node_constraint_runner: NodeConstraintRunner,
165
149
  db: InfrahubDatabase,
166
- data: dict,
167
150
  schema: NonGenericSchemaTypes,
168
- fields_to_validate: list,
169
151
  branch: Branch,
170
- node_constraint_runner: NodeConstraintRunner,
152
+ fields_to_validate: list[str],
153
+ data: dict[str, Any],
154
+ at: Timestamp | None = None,
171
155
  ) -> Node:
172
156
  obj = await node_class.init(db=db, schema=schema, branch=branch)
173
157
  await obj.new(db=db, **data)
174
158
  await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
175
- await obj.save(db=db)
159
+ await obj.save(db=db, at=at)
176
160
 
177
161
  object_template = await obj.get_object_template(db=db)
178
162
  if object_template:
@@ -182,50 +166,70 @@ async def _do_create_node(
182
166
  template=object_template,
183
167
  obj=obj,
184
168
  fields=fields_to_validate,
169
+ at=at,
185
170
  )
186
171
  return obj
187
172
 
188
173
 
189
174
  async def create_node(
190
- data: dict,
175
+ data: dict[str, Any],
191
176
  db: InfrahubDatabase,
192
177
  branch: Branch,
193
- schema: NonGenericSchemaTypes,
178
+ schema: MainSchemaTypes,
179
+ at: Timestamp | None = None,
194
180
  ) -> Node:
195
181
  """Create a node in the database if constraint checks succeed."""
196
182
 
183
+ if isinstance(schema, GenericSchema):
184
+ raise ValueError(f"Node of generic schema `{schema.name=}` can not be instantiated.")
185
+
197
186
  component_registry = get_component_registry()
198
- node_constraint_runner = await component_registry.get_component(
199
- NodeConstraintRunner, db=db.start_session() if not db.is_transaction else db, branch=branch
200
- )
201
187
  node_class = Node
202
188
  if schema.kind in registry.node:
203
189
  node_class = registry.node[schema.kind]
204
190
 
205
191
  fields_to_validate = list(data)
206
- if db.is_transaction:
207
- obj = await _do_create_node(
208
- node_class=node_class,
209
- node_constraint_runner=node_constraint_runner,
210
- db=db,
211
- schema=schema,
212
- branch=branch,
213
- fields_to_validate=fields_to_validate,
214
- data=data,
215
- )
216
- else:
217
- async with db.start_transaction() as dbt:
192
+
193
+ preview_obj = await node_class.init(db=db, schema=schema, branch=branch)
194
+ await preview_obj.new(db=db, process_pools=False, **data)
195
+ schema_branch = db.schema.get_schema_branch(name=branch.name)
196
+ lock_names = get_lock_names_on_object_mutation(node=preview_obj, schema_branch=schema_branch)
197
+
198
+ obj: Node
199
+ async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names, metrics=False):
200
+ if db.is_transaction:
201
+ node_constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
202
+
218
203
  obj = await _do_create_node(
219
204
  node_class=node_class,
220
205
  node_constraint_runner=node_constraint_runner,
221
- db=dbt,
206
+ db=db,
222
207
  schema=schema,
223
208
  branch=branch,
224
209
  fields_to_validate=fields_to_validate,
225
210
  data=data,
211
+ at=at,
226
212
  )
213
+ else:
214
+ async with db.start_transaction() as dbt:
215
+ node_constraint_runner = await component_registry.get_component(
216
+ NodeConstraintRunner, db=dbt, branch=branch
217
+ )
218
+
219
+ obj = await _do_create_node(
220
+ node_class=node_class,
221
+ node_constraint_runner=node_constraint_runner,
222
+ db=dbt,
223
+ schema=schema,
224
+ branch=branch,
225
+ fields_to_validate=fields_to_validate,
226
+ data=data,
227
+ at=at,
228
+ )
227
229
 
228
230
  if await get_profile_ids(db=db, obj=obj):
229
- obj = await refresh_for_profile_update(db=db, branch=branch, schema=schema, obj=obj)
231
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
232
+ await node_profiles_applier.apply_profiles(node=obj)
233
+ await obj.save(db=db)
230
234
 
231
235
  return obj
@@ -0,0 +1,124 @@
1
+ import hashlib
2
+ from typing import TYPE_CHECKING
3
+
4
+ from infrahub.core.node import Node
5
+ from infrahub.core.schema import GenericSchema
6
+ from infrahub.core.schema.schema_branch import SchemaBranch
7
+
8
+ if TYPE_CHECKING:
9
+ from infrahub.core.relationship import RelationshipManager
10
+
11
+
12
+ RESOURCE_POOL_LOCK_NAMESPACE = "resource_pool"
13
+
14
+
15
+ def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch) -> list[str]:
16
+ """
17
+ Return kinds for which we want to lock during creating / updating an object of a given schema node.
18
+ Lock should be performed on schema kind and its generics having a uniqueness_constraint defined.
19
+ If a generic uniqueness constraint is the same as the node schema one,
20
+ it means node schema overrided this constraint, in which case we only need to lock on the generic.
21
+ """
22
+
23
+ node_schema = schema_branch.get(name=kind, duplicate=False)
24
+
25
+ schema_uc = None
26
+ kinds = []
27
+ if node_schema.uniqueness_constraints:
28
+ kinds.append(node_schema.kind)
29
+ schema_uc = node_schema.uniqueness_constraints
30
+
31
+ if isinstance(node_schema, GenericSchema):
32
+ return kinds
33
+
34
+ generics_kinds = node_schema.inherit_from
35
+
36
+ node_schema_kind_removed = False
37
+ for generic_kind in generics_kinds:
38
+ generic_uc = schema_branch.get(name=generic_kind, duplicate=False).uniqueness_constraints
39
+ if generic_uc:
40
+ kinds.append(generic_kind)
41
+ if not node_schema_kind_removed and generic_uc == schema_uc:
42
+ # Check whether we should remove original schema kind as it simply overrides uniqueness_constraint
43
+ # of a generic
44
+ kinds.pop(0)
45
+ node_schema_kind_removed = True
46
+ return kinds
47
+
48
+
49
+ def _hash(value: str) -> str:
50
+ # Do not use builtin `hash` for lock names as due to randomization results would differ between
51
+ # different processes.
52
+ return hashlib.sha256(value.encode()).hexdigest()
53
+
54
+
55
+ def get_lock_names_on_object_mutation(node: Node, schema_branch: SchemaBranch) -> list[str]:
56
+ """
57
+ Return lock names for object on which we want to avoid concurrent mutation (create/update).
58
+ Lock names include kind, some generic kinds, resource pool ids, and values of attributes of corresponding uniqueness constraints.
59
+ """
60
+
61
+ lock_names: set[str] = set()
62
+
63
+ # Check if node is using resource manager allocation via attributes
64
+ for attr_name in node.get_schema().attribute_names:
65
+ attribute = getattr(node, attr_name, None)
66
+ if attribute is not None and getattr(attribute, "from_pool", None) and "id" in attribute.from_pool:
67
+ lock_names.add(f"{RESOURCE_POOL_LOCK_NAMESPACE}.{attribute.from_pool['id']}")
68
+
69
+ # Check if relationships allocate resources
70
+ for rel_name in node._relationships:
71
+ rel_manager: RelationshipManager = getattr(node, rel_name)
72
+ for rel in rel_manager._relationships:
73
+ if rel.from_pool and "id" in rel.from_pool:
74
+ lock_names.add(f"{RESOURCE_POOL_LOCK_NAMESPACE}.{rel.from_pool['id']}")
75
+
76
+ lock_kinds = _get_kinds_to_lock_on_object_mutation(node.get_kind(), schema_branch)
77
+ for kind in lock_kinds:
78
+ schema = schema_branch.get(name=kind, duplicate=False)
79
+ ucs = schema.uniqueness_constraints
80
+ if ucs is None:
81
+ continue
82
+
83
+ ucs_lock_names: list[str] = []
84
+ uc_attributes_names = set()
85
+
86
+ for uc in ucs:
87
+ uc_attributes_values = []
88
+ # Keep only attributes constraints
89
+ for field_path in uc:
90
+ # Some attributes may exist in different uniqueness constraints, we de-duplicate them
91
+ if field_path in uc_attributes_names:
92
+ continue
93
+
94
+ # Exclude relationships uniqueness constraints
95
+ schema_path = schema.parse_schema_path(path=field_path, schema=schema_branch)
96
+ if schema_path.related_schema is not None or schema_path.attribute_schema is None:
97
+ continue
98
+
99
+ uc_attributes_names.add(field_path)
100
+ attr = getattr(node, schema_path.attribute_schema.name, None)
101
+ if attr is None or attr.value is None:
102
+ # `attr.value` being None corresponds to optional unique attribute.
103
+ # `attr` being None is not supposed to happen.
104
+ value_hashed = _hash("")
105
+ else:
106
+ value_hashed = _hash(str(attr.value))
107
+
108
+ uc_attributes_values.append(value_hashed)
109
+
110
+ if uc_attributes_values:
111
+ uc_lock_name = ".".join(uc_attributes_values)
112
+ ucs_lock_names.append(uc_lock_name)
113
+
114
+ if not ucs_lock_names:
115
+ continue
116
+
117
+ partial_lock_name = kind + "." + ".".join(ucs_lock_names)
118
+ lock_names.add(build_object_lock_name(partial_lock_name))
119
+
120
+ return sorted(lock_names)
121
+
122
+
123
+ def build_object_lock_name(name: str) -> str:
124
+ return f"global.object.{name}"
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+
7
+ from infrahub_sdk.template import Jinja2Template
8
+
9
+ from infrahub.core.query.node import AttributeFromDB
10
+ from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
11
+
12
+ from ..attribute import BaseAttribute, ListAttributeOptional, StringOptional
13
+
14
+ if TYPE_CHECKING:
15
+ from infrahub.core.node import Node
16
+ from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
17
+ from infrahub.core.schema.attribute_schema import AttributeSchema
18
+ from infrahub.core.timestamp import Timestamp
19
+ from infrahub.database import InfrahubDatabase
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class NodePropertyAttribute(Generic[T]):
25
+ """A node property attribute is a construct that seats between a property and an attribute.
26
+
27
+ View it as a property, set at the node level but stored in the database as an attribute. It usually is something computed from other components of
28
+ a node, such as its attributes and its relationships.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
34
+ template: T | None,
35
+ value: AttributeFromDB | T | None = None,
36
+ ) -> None:
37
+ self.node_schema = node_schema
38
+
39
+ self.node_attributes: list[str] = []
40
+ self.node_relationships: list[str] = []
41
+
42
+ self.template = template
43
+ self._value = value
44
+ self._manually_assigned = False
45
+
46
+ self.schema: AttributeSchema
47
+
48
+ self.analyze_variables()
49
+
50
+ def needs_update(self, fields: list[str] | None) -> bool:
51
+ """Tell if this node property attribute must be recomputed given a list of updated fields of a node."""
52
+ if self._manually_assigned or not fields:
53
+ return True
54
+ for field in fields:
55
+ if field in self.node_attributes or field in self.node_relationships:
56
+ return True
57
+
58
+ return False
59
+
60
+ @property
61
+ def attribute_value(self) -> AttributeFromDB | dict[str, T | None]:
62
+ if isinstance(self._value, AttributeFromDB):
63
+ return self._value
64
+ return {"value": self._value}
65
+
66
+ def set_value(self, value: T | None, manually_assigned: bool = False) -> None:
67
+ """Force the value of the node property attribute to the given one."""
68
+ if isinstance(self._value, AttributeFromDB):
69
+ self._value.value = value
70
+ else:
71
+ self._value = value
72
+
73
+ if manually_assigned:
74
+ self._manually_assigned = True
75
+
76
+ def get_value(self, node: Node, at: Timestamp) -> T | None:
77
+ if isinstance(self._value, AttributeFromDB):
78
+ attr = self.get_node_attribute(node=node, at=at)
79
+ return attr.value # type: ignore
80
+
81
+ return self._value
82
+
83
+ @abstractmethod
84
+ def analyze_variables(self) -> None: ...
85
+
86
+ @abstractmethod
87
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None: ...
88
+
89
+ @abstractmethod
90
+ def get_node_attribute(self, node: Node, at: Timestamp) -> BaseAttribute: ...
91
+
92
+
93
+ class DisplayLabel(NodePropertyAttribute[str]):
94
+ def __init__(
95
+ self,
96
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
97
+ template: str | None,
98
+ value: AttributeFromDB | str | None = None,
99
+ ) -> None:
100
+ super().__init__(node_schema=node_schema, template=template, value=value)
101
+
102
+ self.schema = node_schema.get_attribute(name="display_label")
103
+
104
+ @property
105
+ def is_jinja2_template(self) -> bool:
106
+ if self.template is None:
107
+ return False
108
+
109
+ return any(c in self.template for c in "{}")
110
+
111
+ def _analyze_plain_value(self) -> None:
112
+ if self.template is None or "__" not in self.template:
113
+ return
114
+
115
+ items = self.template.split("__", maxsplit=1)
116
+ if items[0] not in self.node_schema.attribute_names:
117
+ raise ValueError(f"{items[0]} is not an attribute of {self.node_schema.kind}")
118
+
119
+ self.node_attributes.append(items[0])
120
+
121
+ def _analyze_jinja2_value(self) -> None:
122
+ if self.template is None or not self.is_jinja2_template:
123
+ return
124
+
125
+ tpl = Jinja2Template(template=self.template)
126
+ for variable in tpl.get_variables():
127
+ items = variable.split("__", maxsplit=1)
128
+ if items[0] in self.node_schema.attribute_names:
129
+ self.node_attributes.append(items[0])
130
+ elif items[0] in self.node_schema.relationship_names:
131
+ self.node_relationships.append(items[0])
132
+ else:
133
+ raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
134
+
135
+ def analyze_variables(self) -> None:
136
+ """Look at variables used in the display label and record attributes and relationships required to compute it."""
137
+ if not self.is_jinja2_template:
138
+ self._analyze_plain_value()
139
+ else:
140
+ self._analyze_jinja2_value()
141
+
142
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None:
143
+ """Update the display label value by recomputing it from the template."""
144
+ if self.template is None or self._manually_assigned:
145
+ return
146
+
147
+ if node.get_schema() != self.node_schema:
148
+ raise ValueError(
149
+ f"display_label for schema {self.node_schema.kind} cannot be rendered for node {node.get_schema().kind} {node.id}"
150
+ )
151
+
152
+ if not self.is_jinja2_template:
153
+ path_value = await node.get_path_value(db=db, path=self.template)
154
+ # Use .value for enum to keep compat with old style display label
155
+ self.set_value(value=str(path_value if not isinstance(path_value, Enum) else path_value.value))
156
+ return
157
+
158
+ jinja2_template = Jinja2Template(template=self.template)
159
+
160
+ variables: dict[str, Any] = {}
161
+ for variable in jinja2_template.get_variables():
162
+ variables[variable] = await node.get_path_value(db=db, path=variable)
163
+
164
+ self.set_value(value=await jinja2_template.render(variables=variables))
165
+
166
+ def get_node_attribute(self, node: Node, at: Timestamp) -> StringOptional:
167
+ """Return a node attribute that can be stored in the database for this display label and node."""
168
+ return StringOptional(
169
+ name="display_label",
170
+ schema=self.schema,
171
+ branch=node.get_branch(),
172
+ at=at,
173
+ node=node,
174
+ data=self.attribute_value,
175
+ )
176
+
177
+
178
+ class HumanFriendlyIdentifier(NodePropertyAttribute[list[str]]):
179
+ def __init__(
180
+ self,
181
+ node_schema: NodeSchema | ProfileSchema | TemplateSchema,
182
+ template: list[str] | None,
183
+ value: AttributeFromDB | list[str] | None = None,
184
+ ) -> None:
185
+ super().__init__(node_schema=node_schema, template=template, value=value)
186
+
187
+ self.schema = node_schema.get_attribute(name="human_friendly_id")
188
+
189
+ def _analyze_single_variable(self, value: str) -> None:
190
+ items = value.split("__", maxsplit=1)
191
+ if items[0] in self.node_schema.attribute_names:
192
+ self.node_attributes.append(items[0])
193
+ elif items[0] in self.node_schema.relationship_names:
194
+ self.node_relationships.append(items[0])
195
+ else:
196
+ raise ValueError(f"{items[0]} is neither an attribute or a relationship of {self.node_schema.kind}")
197
+
198
+ def analyze_variables(self) -> None:
199
+ """Look at variables used in the HFID and record attributes and relationships required to compute it."""
200
+ for item in self.template or []:
201
+ self._analyze_single_variable(value=item)
202
+
203
+ async def compute(self, db: InfrahubDatabase, node: Node) -> None:
204
+ """Update the HFID value by recomputing it from the template."""
205
+ if self.template is None or self._manually_assigned:
206
+ return
207
+
208
+ if node.get_schema() != self.node_schema:
209
+ raise ValueError(
210
+ f"human_friendly_id for schema {self.node_schema.kind} cannot be computed for node {node.get_schema().kind} {node.id}"
211
+ )
212
+
213
+ value: list[str] = []
214
+ for path in self.template:
215
+ path_value = await node.get_path_value(db=db, path=path)
216
+ # Use .value for enum to be consistent with display label
217
+ value.append(path_value if not isinstance(path_value, Enum) else path_value.value)
218
+
219
+ self.set_value(value=value)
220
+
221
+ def get_node_attribute(self, node: Node, at: Timestamp) -> ListAttributeOptional:
222
+ """Return a node attribute that can be stored in the database for this HFID and node."""
223
+ return ListAttributeOptional(
224
+ name="human_friendly_id",
225
+ schema=self.schema,
226
+ branch=node.get_branch(),
227
+ at=at,
228
+ node=node,
229
+ data=self.attribute_value,
230
+ )
@@ -15,6 +15,7 @@ from infrahub.exceptions import PoolExhaustedError, ValidationError
15
15
  from infrahub.pools.address import get_available
16
16
 
17
17
  from .. import Node
18
+ from ..lock_utils import RESOURCE_POOL_LOCK_NAMESPACE
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from infrahub.core.branch import Branch
@@ -34,7 +35,7 @@ class CoreIPAddressPool(Node):
34
35
  prefixlen: int | None = None,
35
36
  at: Timestamp | None = None,
36
37
  ) -> Node:
37
- async with lock.registry.get(name=self.get_id(), namespace="resource_pool"):
38
+ async with lock.registry.get(name=self.get_id(), namespace=RESOURCE_POOL_LOCK_NAMESPACE):
38
39
  # Check if there is already a resource allocated with this identifier
39
40
  # if not, pull all existing prefixes and allocated the next available
40
41
 
@@ -17,6 +17,7 @@ from infrahub.exceptions import ValidationError
17
17
  from infrahub.pools.prefix import get_next_available_prefix
18
18
 
19
19
  from .. import Node
20
+ from ..lock_utils import RESOURCE_POOL_LOCK_NAMESPACE
20
21
 
21
22
  if TYPE_CHECKING:
22
23
  from infrahub.core.branch import Branch
@@ -37,7 +38,7 @@ class CoreIPPrefixPool(Node):
37
38
  prefix_type: str | None = None,
38
39
  at: Timestamp | None = None,
39
40
  ) -> Node:
40
- async with lock.registry.get(name=self.get_id(), namespace="resource_pool"):
41
+ async with lock.registry.get(name=self.get_id(), namespace=RESOURCE_POOL_LOCK_NAMESPACE):
41
42
  # Check if there is already a resource allocated with this identifier
42
43
  # if not, pull all existing prefixes and allocated the next available
43
44
  if identifier:
@@ -9,6 +9,7 @@ from infrahub.core.schema.attribute_parameters import NumberAttributeParameters
9
9
  from infrahub.exceptions import PoolExhaustedError
10
10
 
11
11
  from .. import Node
12
+ from ..lock_utils import RESOURCE_POOL_LOCK_NAMESPACE
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from infrahub.core.branch import Branch
@@ -63,7 +64,7 @@ class CoreNumberPool(Node):
63
64
  identifier: str | None = None,
64
65
  at: Timestamp | None = None,
65
66
  ) -> int:
66
- async with lock.registry.get(name=self.get_id(), namespace="resource_pool"):
67
+ async with lock.registry.get(name=self.get_id(), namespace=RESOURCE_POOL_LOCK_NAMESPACE):
67
68
  # NOTE: ideally we should use the HFID as the identifier (if available)
68
69
  # one of the challenge with using the HFID is that it might change over time
69
70
  # so we need to ensure that the identifier is stable, or we need to handle the case where the identifier changes
@@ -111,7 +111,7 @@ class StandardNode(BaseModel):
111
111
  node = result.get("n")
112
112
 
113
113
  self.id = node.element_id
114
- self.uuid = node["uuid"]
114
+ self.uuid = UUID(node["uuid"])
115
115
 
116
116
  return True
117
117
 
infrahub/core/property.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import Enum
3
4
  from typing import TYPE_CHECKING
4
5
  from uuid import UUID
5
6
 
@@ -26,6 +27,10 @@ class NodePropertyData(BaseModel):
26
27
  peer_id: str
27
28
 
28
29
 
30
+ class ClearValue(Enum):
31
+ CLEAR = "clear"
32
+
33
+
29
34
  class FlagPropertyMixin:
30
35
  _flag_properties: list[str] = [v.value for v in FlagProperty]
31
36
 
@@ -51,6 +56,7 @@ class NodePropertyMixin:
51
56
  for node in self._node_properties:
52
57
  setattr(self, f"_{node}", None)
53
58
  setattr(self, f"{node}_id", None)
59
+ setattr(self, f"_clear_{node}", False)
54
60
 
55
61
  if not kwargs:
56
62
  return
@@ -79,12 +85,14 @@ class NodePropertyMixin:
79
85
 
80
86
  def clear_owner(self) -> None:
81
87
  self._set_node_property(name="owner", value=None)
88
+ self._clear_owner = True
82
89
 
83
90
  async def get_source(self, db: InfrahubDatabase) -> Node | None:
84
91
  return await self._get_node_property(name="source", db=db)
85
92
 
86
93
  def clear_source(self) -> None:
87
94
  self._set_node_property(name="source", value=None)
95
+ self._clear_source = True
88
96
 
89
97
  def set_source(self, value: str | Node | UUID) -> None:
90
98
  self._set_node_property(name="source", value=value)
@@ -95,6 +103,9 @@ class NodePropertyMixin:
95
103
  def set_owner(self, value: str | Node | UUID) -> None:
96
104
  self._set_node_property(name="owner", value=value)
97
105
 
106
+ def is_clear(self, name: str) -> bool:
107
+ return getattr(self, f"_clear_{name}", False)
108
+
98
109
  def _get_node_property_from_cache(self, name: str) -> Node:
99
110
  """Return the node attribute if it's already present locally,
100
111
  Otherwise raise an exception
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from .protocols_base import CoreNode
7
+ from infrahub.core.protocols_base import CoreNode
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from enum import Enum
@@ -125,6 +125,7 @@ class CoreGenericRepository(CoreNode):
125
125
  queries: RelationshipManager
126
126
  checks: RelationshipManager
127
127
  generators: RelationshipManager
128
+ groups_objects: RelationshipManager
128
129
 
129
130
 
130
131
  class CoreGroup(CoreNode):
@@ -349,6 +350,10 @@ class CoreGeneratorAction(CoreAction):
349
350
  generator: RelationshipManager
350
351
 
351
352
 
353
+ class CoreGeneratorAwareGroup(CoreGroup):
354
+ pass
355
+
356
+
352
357
  class CoreGeneratorCheck(CoreCheck):
353
358
  instance: String
354
359
 
@@ -360,6 +365,8 @@ class CoreGeneratorDefinition(CoreTaskTarget):
360
365
  file_path: String
361
366
  class_name: String
362
367
  convert_query_response: BooleanOptional
368
+ execute_in_proposed_change: BooleanOptional
369
+ execute_after_merge: BooleanOptional
363
370
  query: RelationshipManager
364
371
  repository: RelationshipManager
365
372
  targets: RelationshipManager