infrahub-server 1.6.3__py3-none-any.whl → 1.7.0b0__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 (161) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/schema.py +3 -1
  3. infrahub/artifacts/tasks.py +1 -0
  4. infrahub/auth.py +2 -2
  5. infrahub/cli/db.py +6 -6
  6. infrahub/computed_attribute/gather.py +3 -4
  7. infrahub/computed_attribute/tasks.py +23 -6
  8. infrahub/config.py +8 -0
  9. infrahub/constants/enums.py +12 -0
  10. infrahub/core/account.py +5 -8
  11. infrahub/core/attribute.py +106 -108
  12. infrahub/core/branch/models.py +44 -71
  13. infrahub/core/branch/tasks.py +5 -3
  14. infrahub/core/changelog/diff.py +1 -20
  15. infrahub/core/changelog/models.py +0 -7
  16. infrahub/core/constants/__init__.py +17 -0
  17. infrahub/core/constants/database.py +0 -1
  18. infrahub/core/constants/schema.py +0 -1
  19. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  20. infrahub/core/diff/data_check_synchronizer.py +3 -2
  21. infrahub/core/diff/enricher/cardinality_one.py +1 -1
  22. infrahub/core/diff/merger/merger.py +27 -1
  23. infrahub/core/diff/merger/serializer.py +3 -10
  24. infrahub/core/diff/model/diff.py +1 -1
  25. infrahub/core/diff/query/merge.py +376 -135
  26. infrahub/core/graph/__init__.py +1 -1
  27. infrahub/core/graph/constraints.py +2 -2
  28. infrahub/core/graph/schema.py +2 -12
  29. infrahub/core/manager.py +132 -126
  30. infrahub/core/metadata/__init__.py +0 -0
  31. infrahub/core/metadata/interface.py +37 -0
  32. infrahub/core/metadata/model.py +31 -0
  33. infrahub/core/metadata/query/__init__.py +0 -0
  34. infrahub/core/metadata/query/node_metadata.py +301 -0
  35. infrahub/core/migrations/graph/__init__.py +4 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +3 -8
  37. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  38. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  39. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  40. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  41. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  42. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  43. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  44. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  45. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  46. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +38 -0
  47. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  48. infrahub/core/migrations/query/attribute_add.py +17 -6
  49. infrahub/core/migrations/query/attribute_remove.py +19 -5
  50. infrahub/core/migrations/query/attribute_rename.py +21 -5
  51. infrahub/core/migrations/query/node_duplicate.py +19 -4
  52. infrahub/core/migrations/schema/attribute_kind_update.py +25 -7
  53. infrahub/core/migrations/schema/attribute_supports_profile.py +3 -1
  54. infrahub/core/migrations/schema/models.py +3 -0
  55. infrahub/core/migrations/schema/node_attribute_add.py +4 -1
  56. infrahub/core/migrations/schema/node_remove.py +24 -2
  57. infrahub/core/migrations/schema/tasks.py +4 -1
  58. infrahub/core/migrations/shared.py +13 -6
  59. infrahub/core/models.py +6 -6
  60. infrahub/core/node/__init__.py +156 -57
  61. infrahub/core/node/create.py +7 -3
  62. infrahub/core/node/standard.py +100 -14
  63. infrahub/core/property.py +0 -1
  64. infrahub/core/protocols_base.py +6 -2
  65. infrahub/core/query/__init__.py +6 -7
  66. infrahub/core/query/attribute.py +161 -46
  67. infrahub/core/query/branch.py +57 -69
  68. infrahub/core/query/diff.py +4 -4
  69. infrahub/core/query/node.py +618 -180
  70. infrahub/core/query/relationship.py +449 -300
  71. infrahub/core/query/standard_node.py +25 -5
  72. infrahub/core/query/utils.py +2 -4
  73. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  74. infrahub/core/relationship/model.py +293 -139
  75. infrahub/core/schema/attribute_parameters.py +1 -28
  76. infrahub/core/schema/attribute_schema.py +17 -11
  77. infrahub/core/schema/manager.py +63 -43
  78. infrahub/core/schema/relationship_schema.py +6 -2
  79. infrahub/core/schema/schema_branch.py +48 -76
  80. infrahub/core/task/task.py +4 -2
  81. infrahub/core/utils.py +0 -22
  82. infrahub/core/validators/attribute/kind.py +2 -5
  83. infrahub/core/validators/determiner.py +3 -3
  84. infrahub/database/__init__.py +3 -3
  85. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  86. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  87. infrahub/dependencies/registry.py +2 -0
  88. infrahub/display_labels/tasks.py +12 -3
  89. infrahub/git/integrator.py +18 -18
  90. infrahub/git/tasks.py +1 -1
  91. infrahub/graphql/app.py +2 -2
  92. infrahub/graphql/constants.py +3 -0
  93. infrahub/graphql/context.py +1 -1
  94. infrahub/graphql/initialization.py +11 -0
  95. infrahub/graphql/loaders/account.py +134 -0
  96. infrahub/graphql/loaders/node.py +5 -12
  97. infrahub/graphql/loaders/peers.py +5 -7
  98. infrahub/graphql/manager.py +158 -18
  99. infrahub/graphql/metadata.py +91 -0
  100. infrahub/graphql/models.py +33 -3
  101. infrahub/graphql/mutations/account.py +5 -5
  102. infrahub/graphql/mutations/attribute.py +0 -2
  103. infrahub/graphql/mutations/branch.py +9 -5
  104. infrahub/graphql/mutations/computed_attribute.py +1 -1
  105. infrahub/graphql/mutations/display_label.py +1 -1
  106. infrahub/graphql/mutations/hfid.py +1 -1
  107. infrahub/graphql/mutations/ipam.py +4 -6
  108. infrahub/graphql/mutations/main.py +9 -4
  109. infrahub/graphql/mutations/profile.py +16 -22
  110. infrahub/graphql/mutations/proposed_change.py +4 -4
  111. infrahub/graphql/mutations/relationship.py +40 -10
  112. infrahub/graphql/mutations/repository.py +14 -12
  113. infrahub/graphql/mutations/schema.py +2 -2
  114. infrahub/graphql/queries/branch.py +62 -6
  115. infrahub/graphql/queries/diff/tree.py +5 -5
  116. infrahub/graphql/resolvers/account_metadata.py +84 -0
  117. infrahub/graphql/resolvers/ipam.py +6 -8
  118. infrahub/graphql/resolvers/many_relationship.py +77 -35
  119. infrahub/graphql/resolvers/resolver.py +16 -12
  120. infrahub/graphql/resolvers/single_relationship.py +87 -23
  121. infrahub/graphql/subscription/graphql_query.py +2 -0
  122. infrahub/graphql/types/__init__.py +0 -1
  123. infrahub/graphql/types/attribute.py +10 -5
  124. infrahub/graphql/types/branch.py +40 -53
  125. infrahub/graphql/types/enums.py +3 -0
  126. infrahub/graphql/types/metadata.py +28 -0
  127. infrahub/graphql/types/node.py +22 -2
  128. infrahub/graphql/types/relationship.py +10 -2
  129. infrahub/graphql/types/standard_node.py +4 -3
  130. infrahub/hfid/tasks.py +12 -3
  131. infrahub/profiles/gather.py +56 -0
  132. infrahub/profiles/mandatory_fields_checker.py +116 -0
  133. infrahub/profiles/models.py +66 -0
  134. infrahub/profiles/node_applier.py +153 -12
  135. infrahub/profiles/queries/get_profile_data.py +143 -31
  136. infrahub/profiles/tasks.py +79 -27
  137. infrahub/profiles/triggers.py +22 -0
  138. infrahub/proposed_change/tasks.py +4 -1
  139. infrahub/tasks/artifact.py +1 -0
  140. infrahub/transformations/tasks.py +2 -2
  141. infrahub/trigger/catalogue.py +2 -0
  142. infrahub/trigger/models.py +1 -0
  143. infrahub/trigger/setup.py +3 -3
  144. infrahub/trigger/tasks.py +3 -0
  145. infrahub/validators/tasks.py +1 -0
  146. infrahub/webhook/models.py +1 -1
  147. infrahub/webhook/tasks.py +1 -1
  148. infrahub/workers/dependencies.py +9 -3
  149. infrahub/workers/infrahub_async.py +13 -4
  150. infrahub/workflows/catalogue.py +19 -0
  151. infrahub_sdk/node/constants.py +1 -0
  152. infrahub_sdk/node/related_node.py +13 -4
  153. infrahub_sdk/node/relationship.py +8 -0
  154. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/METADATA +17 -16
  155. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/RECORD +161 -143
  156. infrahub_testcontainers/container.py +3 -3
  157. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  158. infrahub_testcontainers/docker-compose.test.yml +13 -5
  159. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/WHEEL +0 -0
  160. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/entry_points.txt +0 -0
  161. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -2,13 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
- from graphene import Boolean, Field, Int, List, NonNull, String
5
+ from graphene import Boolean, Field, Int, List, NonNull, ObjectType, String
6
6
 
7
7
  from infrahub.core.branch import Branch
8
- from infrahub.core.constants import GLOBAL_BRANCH_NAME
8
+ from infrahub.core.node.standard import StandardNodeQueryFields
9
9
 
10
10
  from ...exceptions import BranchNotFoundError
11
11
  from .enums import InfrahubBranchStatus
12
+ from .metadata import InfrahubStandardNodeMetaData
12
13
  from .standard_node import InfrahubObjectType
13
14
 
14
15
  if TYPE_CHECKING:
@@ -34,56 +35,35 @@ class BranchType(InfrahubObjectType):
34
35
  name = "Branch"
35
36
  model = Branch
36
37
 
37
- @staticmethod
38
- async def _map_fields_to_graphql(objs: list[Branch], fields: dict) -> list[dict[str, Any]]:
39
- return [await obj.to_graphql(fields=fields) for obj in objs]
40
-
41
38
  @classmethod
42
39
  async def get_list(
43
40
  cls,
44
- fields: dict,
41
+ fields: StandardNodeQueryFields,
45
42
  graphql_context: GraphqlContext,
46
43
  **kwargs: Any,
47
44
  ) -> list[dict[str, Any]]:
48
45
  async with graphql_context.db.start_session(read_only=True) as db:
49
46
  objs = await Branch.get_list(db=db, **kwargs)
47
+ return [await obj.to_graphql_flat(fields=fields.node) for obj in objs]
50
48
 
51
- if not objs:
52
- return []
53
-
54
- return await cls._map_fields_to_graphql(objs=objs, fields=fields)
55
49
 
56
- @classmethod
57
- async def get_by_name(
58
- cls,
59
- fields: dict,
60
- graphql_context: GraphqlContext,
61
- name: str,
62
- ) -> dict[str, Any]:
63
- branch_responses = await cls.get_list(fields=fields, graphql_context=graphql_context, name=name)
64
-
65
- if branch_responses:
66
- return branch_responses[0]
67
- raise BranchNotFoundError(f"Branch with name '{name}' not found")
68
-
69
-
70
- class RequiredStringValueField(InfrahubObjectType):
50
+ class RequiredStringValueField(ObjectType):
71
51
  value = String(required=True)
72
52
 
73
53
 
74
- class NonRequiredStringValueField(InfrahubObjectType):
54
+ class NonRequiredStringValueField(ObjectType):
75
55
  value = String(required=False)
76
56
 
77
57
 
78
- class NonRequiredIntValueField(InfrahubObjectType):
58
+ class NonRequiredIntValueField(ObjectType):
79
59
  value = Int(required=False)
80
60
 
81
61
 
82
- class NonRequiredBooleanValueField(InfrahubObjectType):
62
+ class NonRequiredBooleanValueField(ObjectType):
83
63
  value = Boolean(required=False)
84
64
 
85
65
 
86
- class StatusField(InfrahubObjectType):
66
+ class StatusField(ObjectType):
87
67
  value = InfrahubBranchStatus(required=True)
88
68
 
89
69
 
@@ -101,36 +81,43 @@ class InfrahubBranch(BranchType):
101
81
  )
102
82
  has_schema_changes = Field(NonRequiredBooleanValueField, required=False)
103
83
 
84
+ @classmethod
85
+ async def get_list(
86
+ cls,
87
+ fields: StandardNodeQueryFields,
88
+ graphql_context: GraphqlContext,
89
+ **kwargs: Any,
90
+ ) -> list[dict[str, Any]]:
91
+ async with graphql_context.db.start_session(read_only=True) as db:
92
+ objs = await Branch.get_list(db=db, **kwargs)
93
+ return [await obj.to_graphql(fields=fields) for obj in objs]
94
+
95
+ @classmethod
96
+ async def get_by_name(
97
+ cls,
98
+ fields: dict,
99
+ graphql_context: GraphqlContext,
100
+ name: str,
101
+ ) -> dict[str, Any]:
102
+ branch_responses = await cls.get_list(
103
+ fields=StandardNodeQueryFields(node=fields), graphql_context=graphql_context, name=name
104
+ )
105
+
106
+ if branch_responses:
107
+ return branch_responses[0]
108
+ raise BranchNotFoundError(f"Branch with name '{name}' not found")
109
+
104
110
  class Meta:
105
111
  description = "InfrahubBranch"
106
112
  name = "InfrahubBranch"
107
113
 
108
- @staticmethod
109
- async def _map_fields_to_graphql(objs: list[Branch], fields: dict) -> list[dict[str, Any]]:
110
- field_keys = fields.keys()
111
- result: list[dict[str, Any]] = []
112
- for obj in objs:
113
- if obj.name == GLOBAL_BRANCH_NAME:
114
- continue
115
- data: dict[str, Any] = {}
116
- for field in field_keys:
117
- if field == "id":
118
- data["id"] = obj.uuid
119
- continue
120
- value = getattr(obj, field, None)
121
- if isinstance(fields.get(field), dict):
122
- data[field] = {"value": value}
123
- else:
124
- data[field] = value
125
- result.append(data)
126
- return result
127
-
128
-
129
- class InfrahubBranchEdge(InfrahubObjectType):
114
+
115
+ class InfrahubBranchEdge(ObjectType):
130
116
  node = Field(InfrahubBranch, required=True)
117
+ node_metadata = Field(InfrahubStandardNodeMetaData, required=True)
131
118
 
132
119
 
133
- class InfrahubBranchType(InfrahubObjectType):
120
+ class InfrahubBranchType(ObjectType):
134
121
  count = Field(Int, description="Total number of items")
135
122
  edges = Field(NonNull(List(of_type=NonNull(InfrahubBranchEdge))))
136
123
  default_branch = Field(
@@ -1,5 +1,6 @@
1
1
  from graphene import Enum
2
2
 
3
+ from infrahub.constants.enums import OrderDirection
3
4
  from infrahub.core import constants
4
5
  from infrahub.core.branch.enums import BranchStatus
5
6
  from infrahub.permissions import constants as permission_constants
@@ -13,3 +14,5 @@ Severity = Enum.from_enum(constants.Severity)
13
14
  BranchRelativePermissionDecision = Enum.from_enum(permission_constants.BranchRelativePermissionDecision)
14
15
 
15
16
  InfrahubBranchStatus = Enum.from_enum(BranchStatus)
17
+
18
+ InfrahubOrderDirection = Enum.from_enum(OrderDirection)
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from graphene import Boolean, DateTime, Field, InputObjectType, ObjectType
4
+
5
+ from infrahub.graphql.types.enums import InfrahubOrderDirection
6
+
7
+
8
+ class InfrahubNodeMetadataOrder(InputObjectType):
9
+ created_at = Field(InfrahubOrderDirection, required=False, description="Order by creation timestamp")
10
+ updated_at = Field(InfrahubOrderDirection, required=False, description="Order by updated timestamp")
11
+
12
+
13
+ class OrderInput(InputObjectType):
14
+ disable = Boolean(required=False)
15
+ node_metadata = Field(InfrahubNodeMetadataOrder, required=False, description="Order settings for branch metadata")
16
+
17
+
18
+ class InfrahubStandardNodeMetaData(ObjectType):
19
+ """Base metadata type for standard nodes.
20
+
21
+ Note: created_by and updated_by fields are added dynamically by
22
+ GraphQLSchemaManager._patch_static_types() to use the GenericAccount interface.
23
+ """
24
+
25
+ created_at = DateTime(required=False, description="Date/Time the object has been created")
26
+ updated_at = DateTime(
27
+ required=False, description="Date/Time when the object was last modified by a user or a system task"
28
+ )
@@ -5,7 +5,7 @@ from typing import Any
5
5
  from graphene import ObjectType
6
6
  from graphene.types.objecttype import ObjectTypeOptions
7
7
 
8
- from infrahub.core.schema import GenericSchema, MainSchemaTypes, NodeSchema, ProfileSchema, TemplateSchema
8
+ from infrahub.core.schema import MainSchemaTypes
9
9
 
10
10
 
11
11
  class InfrahubObjectOptions(ObjectTypeOptions):
@@ -21,7 +21,27 @@ class InfrahubObject(ObjectType):
21
21
  _meta: InfrahubObjectOptions | None = None,
22
22
  **options: Any,
23
23
  ) -> None:
24
- if not isinstance(schema, NodeSchema | GenericSchema | ProfileSchema | TemplateSchema):
24
+ if not isinstance(schema, MainSchemaTypes):
25
+ raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
26
+
27
+ if not _meta:
28
+ _meta = InfrahubObjectOptions(cls)
29
+
30
+ _meta.schema = schema
31
+
32
+ super().__init_subclass_with_meta__(_meta=_meta, interfaces=interfaces, **options)
33
+
34
+
35
+ class InfrahubObjectWithoutMeta(ObjectType):
36
+ @classmethod
37
+ def __init_subclass_with_meta__(
38
+ cls,
39
+ schema: MainSchemaTypes | None = None,
40
+ interfaces: tuple = (),
41
+ _meta: InfrahubObjectOptions | None = None,
42
+ **options: Any,
43
+ ) -> None:
44
+ if not isinstance(schema, MainSchemaTypes):
25
45
  raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
26
46
 
27
47
  if not _meta:
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from graphene import Field, List, NonNull, ObjectType, String
3
+ from graphene import DateTime, Field, List, NonNull, ObjectType, String
4
4
 
5
5
 
6
6
  class RelationshipPeer(ObjectType):
@@ -8,7 +8,15 @@ class RelationshipPeer(ObjectType):
8
8
  kind = String(required=False)
9
9
 
10
10
 
11
- class Relationship(ObjectType):
11
+ class InfrahubRelationshipMetaObject(ObjectType):
12
+ updated_by = String(required=False, description="User that last modified the relationship")
13
+ updated_at = DateTime(
14
+ required=False,
15
+ description="Date/Time when the relationship was last modified by a user or a system task",
16
+ )
17
+
18
+
19
+ class Relationship(InfrahubRelationshipMetaObject):
12
20
  id = String(required=False)
13
21
  identifier = String(required=False)
14
22
  peers = List(NonNull(RelationshipPeer))
@@ -8,6 +8,7 @@ from graphene.types.objecttype import ObjectTypeOptions
8
8
  from infrahub import config
9
9
 
10
10
  if TYPE_CHECKING:
11
+ from infrahub.core.node.standard import StandardNodeQueryFields
11
12
  from infrahub.graphql.initialization import GraphqlContext
12
13
 
13
14
 
@@ -26,7 +27,9 @@ class InfrahubObjectType(ObjectType):
26
27
  super().__init_subclass_with_meta__(_meta=_meta, interfaces=interfaces, **options)
27
28
 
28
29
  @classmethod
29
- async def get_list(cls, fields: dict[str, Any], graphql_context: GraphqlContext, **kwargs) -> list[dict[str, Any]]:
30
+ async def get_list(
31
+ cls, fields: StandardNodeQueryFields, graphql_context: GraphqlContext, **kwargs
32
+ ) -> list[dict[str, Any]]:
30
33
  async with graphql_context.db.session(database=config.SETTINGS.database.database_name) as db:
31
34
  filters = {key: value for key, value in kwargs.items() if "__" in key and value}
32
35
 
@@ -36,7 +39,6 @@ class InfrahubObjectType(ObjectType):
36
39
  at=graphql_context.at,
37
40
  branch=graphql_context.branch,
38
41
  account=graphql_context.account_session,
39
- include_source=True,
40
42
  db=db,
41
43
  )
42
44
  else:
@@ -44,7 +46,6 @@ class InfrahubObjectType(ObjectType):
44
46
  at=graphql_context.at,
45
47
  branch=graphql_context.branch,
46
48
  account=graphql_context.account_session,
47
- include_source=True,
48
49
  db=db,
49
50
  )
50
51
 
infrahub/hfid/tasks.py CHANGED
@@ -20,9 +20,11 @@ UPDATE_HFID = """
20
20
  mutation UpdateHFID(
21
21
  $id: String!,
22
22
  $kind: String!,
23
- $value: [String!]!
23
+ $value: [String!]!,
24
+ $context_account_id: String!
24
25
  ) {
25
26
  InfrahubUpdateHFID(
27
+ context: {account: {id: $context_account_id}},
26
28
  data: {id: $id, value: $value, kind: $kind}
27
29
  ) {
28
30
  ok
@@ -40,6 +42,7 @@ async def hfid_update_value(
40
42
  obj: HFIDGraphQLResponse,
41
43
  node_kind: str,
42
44
  hfid_definition: list[str],
45
+ context: InfrahubContext,
43
46
  ) -> None:
44
47
  log = get_run_logger()
45
48
  client = get_client()
@@ -58,7 +61,12 @@ async def hfid_update_value(
58
61
  try:
59
62
  await client.execute_graphql(
60
63
  query=UPDATE_HFID,
61
- variables={"id": obj.node_id, "kind": node_kind, "value": rendered_hfid},
64
+ variables={
65
+ "id": obj.node_id,
66
+ "kind": node_kind,
67
+ "value": rendered_hfid,
68
+ "context_account_id": context.account.account_id,
69
+ },
62
70
  branch_name=branch_name,
63
71
  )
64
72
  log.info(f"Updating {node_kind}.human_friendly_id='{rendered_hfid}' ({obj.node_id})")
@@ -77,7 +85,7 @@ async def process_hfid(
77
85
  node_kind: str,
78
86
  object_id: str,
79
87
  target_kind: str,
80
- context: InfrahubContext, # noqa: ARG001
88
+ context: InfrahubContext,
81
89
  ) -> None:
82
90
  log = get_run_logger()
83
91
  client = get_client()
@@ -115,6 +123,7 @@ async def process_hfid(
115
123
  obj=node,
116
124
  node_kind=node_schema.kind,
117
125
  hfid_definition=hfid_definition.hfid,
126
+ context=context,
118
127
  )
119
128
 
120
129
  _ = [response async for _, response in batch.execute()]
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from prefect import task
4
+ from prefect.cache_policies import NONE
5
+
6
+ from infrahub.core.constants import RelationshipKind
7
+ from infrahub.core.registry import registry
8
+ from infrahub.database import InfrahubDatabase # noqa: TC001 needed for prefect flow
9
+ from infrahub.workflows.catalogue import PROFILE_REFRESH_PROCESS
10
+
11
+ from .models import ProfileRefreshTriggerDefinition
12
+
13
+
14
+ @task(name="gather-trigger-profile-refresh", cache_policy=NONE)
15
+ async def gather_trigger_profile_refresh(
16
+ db: InfrahubDatabase | None = None, # noqa: ARG001 Needed to have a common function signature for gathering functions
17
+ ) -> list[ProfileRefreshTriggerDefinition]:
18
+ """Gather profile refresh triggers for all profile schemas.
19
+
20
+ This function creates trigger definitions for each profile schema that will
21
+ listen for `NodeUpdatedEvent` on profiles. When a profile's attributes or
22
+ relationships change, the trigger will fire and execute the profile refresh
23
+ workflow to re-apply profiles to all related nodes.
24
+ """
25
+ branches_with_diff_from_main = registry.get_altered_schema_branches()
26
+ branches_to_process: list[tuple[str, list[str]]] = [(branch, []) for branch in branches_with_diff_from_main]
27
+ branches_to_process.append((registry.default_branch, branches_with_diff_from_main))
28
+
29
+ triggers: list[ProfileRefreshTriggerDefinition] = []
30
+
31
+ for branch_scope, branches_out_of_scope in branches_to_process:
32
+ schema_branch = registry.schema.get_schema_branch(name=branch_scope)
33
+
34
+ for profile_name in schema_branch.profile_names:
35
+ profile_schema = schema_branch.get_profile(name=profile_name, duplicate=False)
36
+
37
+ trigger_attr = [attr.name for attr in profile_schema.attributes if attr.name != "profile_name"]
38
+ trigger_rels = [
39
+ rel.name
40
+ for rel in profile_schema.relationships
41
+ if rel.kind in {RelationshipKind.GENERIC, RelationshipKind.ATTRIBUTE} and rel.name != "related_nodes"
42
+ ]
43
+ trigger_fields = sorted(trigger_attr + trigger_rels)
44
+
45
+ if trigger_fields:
46
+ triggers.append(
47
+ ProfileRefreshTriggerDefinition.from_profile_schema(
48
+ branch=branch_scope,
49
+ profile_kind=profile_schema.kind,
50
+ trigger_fields=trigger_fields,
51
+ workflow=PROFILE_REFRESH_PROCESS,
52
+ branches_out_of_scope=branches_out_of_scope,
53
+ )
54
+ )
55
+
56
+ return triggers
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from infrahub.core.query.node import NodeGetByHFIDQuery
7
+
8
+ from .queries.get_profile_data import GetProfileDataQuery, RelationshipFilter
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Sequence
12
+
13
+ from infrahub.core.branch import Branch
14
+ from infrahub.core.schema import NodeSchema
15
+ from infrahub.database import InfrahubDatabase
16
+
17
+
18
+ @dataclass
19
+ class ProfileIdentifiers:
20
+ ids: list[str]
21
+ hfids: list[list[str]]
22
+
23
+
24
+ def _extract_profile_identifiers_from_input(profiles_data: Sequence[Any] | None) -> ProfileIdentifiers:
25
+ """Extract profile IDs and HFIDs from input data."""
26
+ if not profiles_data:
27
+ return ProfileIdentifiers(ids=[], hfids=[])
28
+
29
+ ids: list[str] = []
30
+ hfids: list[list[str]] = []
31
+ for item in profiles_data:
32
+ if isinstance(item, str):
33
+ # IDs
34
+ ids.append(item)
35
+ elif isinstance(item, dict):
36
+ # Dicts with `id` or `hfid` keys
37
+ if profile_id := item.get("id"):
38
+ ids.append(profile_id)
39
+ elif profile_hfid := item.get("hfid"):
40
+ hfids.append(profile_hfid)
41
+ elif hasattr(item, "id"):
42
+ # Avoid circular import by not importing Node directly
43
+ ids.append(item.id)
44
+
45
+ return ProfileIdentifiers(ids=ids, hfids=hfids)
46
+
47
+
48
+ async def _resolve_hfids_to_ids(
49
+ db: InfrahubDatabase, branch: Branch, profile_kind: str, hfids: list[list[str]]
50
+ ) -> list[str]:
51
+ query = await NodeGetByHFIDQuery.init(db=db, branch=branch, node_kind=profile_kind, hfids=hfids)
52
+ await query.execute(db=db)
53
+ return query.get_node_uuids()
54
+
55
+
56
+ class ProfilesMandatoryFieldGetter:
57
+ def __init__(self, db: InfrahubDatabase, branch: Branch) -> None:
58
+ self.db = db
59
+ self.branch = branch
60
+
61
+ async def get_mandatory_fields_from_profiles(
62
+ self,
63
+ schema: NodeSchema,
64
+ profiles_data: Sequence[Any] | None,
65
+ mandatory_attr_names: list[str],
66
+ mandatory_rel_names: list[str],
67
+ ) -> tuple[set[str], set[str]]:
68
+ """Get mandatory attributes and relationships that are provided by profiles."""
69
+ identifiers = _extract_profile_identifiers_from_input(profiles_data=profiles_data)
70
+
71
+ profile_ids = list(identifiers.ids)
72
+ if identifiers.hfids:
73
+ resolved_ids = await _resolve_hfids_to_ids(
74
+ db=self.db, branch=self.branch, profile_kind=f"Profile{schema.kind}", hfids=identifiers.hfids
75
+ )
76
+ profile_ids.extend(resolved_ids)
77
+
78
+ if not profile_ids:
79
+ return set(), set()
80
+
81
+ rel_filters: list[RelationshipFilter] = []
82
+ rel_name_to_filter: dict[str, RelationshipFilter] = {}
83
+ for rel_name in mandatory_rel_names:
84
+ rel_schema = schema.get_relationship(name=rel_name)
85
+ if not rel_schema.support_profiles:
86
+ continue
87
+
88
+ rel_filter = RelationshipFilter(
89
+ relationship_identifier=f"profile_{rel_schema.get_identifier()}", direction=rel_schema.direction
90
+ )
91
+ rel_filters.append(rel_filter)
92
+ rel_name_to_filter[rel_name] = rel_filter
93
+
94
+ query = await GetProfileDataQuery.init(
95
+ db=self.db,
96
+ branch=self.branch,
97
+ profile_ids=profile_ids,
98
+ attr_names=mandatory_attr_names,
99
+ relationship_filters=rel_filters,
100
+ )
101
+ await query.execute(db=self.db)
102
+ profile_data_list = query.get_profile_data()
103
+
104
+ provided_attrs: set[str] = set()
105
+ provided_rels: set[str] = set()
106
+
107
+ for profile_data in profile_data_list:
108
+ for attr_name in mandatory_attr_names:
109
+ if profile_data.attribute_values.get(attr_name) is not None:
110
+ provided_attrs.add(attr_name)
111
+
112
+ for rel_name, rel_filter in rel_name_to_filter.items():
113
+ if profile_data.relationship_peers.get(rel_filter):
114
+ provided_rels.add(rel_name)
115
+
116
+ return provided_attrs, provided_rels
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Self
4
+
5
+ from infrahub.core.registry import registry
6
+ from infrahub.events import NodeUpdatedEvent
7
+ from infrahub.trigger.constants import NAME_SEPARATOR
8
+ from infrahub.trigger.models import EventTrigger, ExecuteWorkflow, TriggerBranchDefinition, TriggerType
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.workflows.models import WorkflowDefinition
12
+
13
+
14
+ class ProfileRefreshTriggerDefinition(TriggerBranchDefinition):
15
+ """Trigger definition for profile refresh when profile attributes/relationships change."""
16
+
17
+ type: TriggerType = TriggerType.PROFILE
18
+ profile_kind: str
19
+
20
+ @classmethod
21
+ def from_profile_schema(
22
+ cls,
23
+ branch: str,
24
+ profile_kind: str,
25
+ trigger_fields: list[str],
26
+ workflow: WorkflowDefinition,
27
+ branches_out_of_scope: list[str] | None = None,
28
+ ) -> Self:
29
+ """Create a trigger definition for profile refresh when profile attributes/relationships change."""
30
+ event_trigger = EventTrigger()
31
+ event_trigger.events.add(NodeUpdatedEvent.event_name)
32
+ event_trigger.match = {"infrahub.node.kind": profile_kind}
33
+
34
+ if branches_out_of_scope:
35
+ event_trigger.match["infrahub.branch.name"] = [f"!{b}" for b in branches_out_of_scope]
36
+ elif branch != registry.default_branch:
37
+ event_trigger.match["infrahub.branch.name"] = branch
38
+
39
+ event_trigger.match_related = {
40
+ "prefect.resource.role": ["infrahub.node.attribute_update", "infrahub.node.relationship_update"],
41
+ "infrahub.field.name": trigger_fields,
42
+ }
43
+
44
+ workflow_action = ExecuteWorkflow(
45
+ workflow=workflow,
46
+ parameters={
47
+ "branch_name": "{{ event.resource['infrahub.branch.name'] }}",
48
+ "profile_kind": profile_kind,
49
+ "profile_id": "{{ event.resource['infrahub.node.id'] }}",
50
+ "context": {
51
+ "__prefect_kind": "json",
52
+ "value": {
53
+ "__prefect_kind": "jinja",
54
+ "template": "{{ event.payload['context'] | tojson }}",
55
+ },
56
+ },
57
+ },
58
+ )
59
+
60
+ return cls(
61
+ name=f"{profile_kind}{NAME_SEPARATOR}refresh",
62
+ branch=branch,
63
+ profile_kind=profile_kind,
64
+ trigger=event_trigger,
65
+ actions=[workflow_action],
66
+ )