infrahub-server 1.4.0b1__py3-none-any.whl → 1.4.1__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 (60) hide show
  1. infrahub/api/schema.py +3 -7
  2. infrahub/cli/db.py +25 -0
  3. infrahub/cli/db_commands/__init__.py +0 -0
  4. infrahub/cli/db_commands/check_inheritance.py +284 -0
  5. infrahub/cli/upgrade.py +3 -0
  6. infrahub/config.py +4 -4
  7. infrahub/core/attribute.py +6 -0
  8. infrahub/core/constants/__init__.py +1 -0
  9. infrahub/core/diff/model/path.py +0 -39
  10. infrahub/core/graph/__init__.py +1 -1
  11. infrahub/core/initialization.py +26 -21
  12. infrahub/core/manager.py +2 -2
  13. infrahub/core/migrations/__init__.py +2 -0
  14. infrahub/core/migrations/graph/__init__.py +4 -2
  15. infrahub/core/migrations/graph/m033_deduplicate_relationship_vertices.py +1 -1
  16. infrahub/core/migrations/graph/m035_orphan_relationships.py +43 -0
  17. infrahub/core/migrations/graph/{m035_drop_attr_value_index.py → m036_drop_attr_value_index.py} +3 -3
  18. infrahub/core/migrations/graph/{m036_index_attr_vals.py → m037_index_attr_vals.py} +3 -3
  19. infrahub/core/migrations/query/node_duplicate.py +26 -3
  20. infrahub/core/migrations/schema/attribute_kind_update.py +156 -0
  21. infrahub/core/models.py +5 -1
  22. infrahub/core/node/resource_manager/ip_address_pool.py +50 -48
  23. infrahub/core/node/resource_manager/ip_prefix_pool.py +55 -53
  24. infrahub/core/node/resource_manager/number_pool.py +20 -18
  25. infrahub/core/query/branch.py +37 -20
  26. infrahub/core/query/node.py +15 -0
  27. infrahub/core/relationship/model.py +13 -13
  28. infrahub/core/schema/definitions/internal.py +1 -1
  29. infrahub/core/schema/generated/attribute_schema.py +1 -1
  30. infrahub/core/schema/node_schema.py +0 -5
  31. infrahub/core/schema/schema_branch.py +2 -2
  32. infrahub/core/validators/attribute/choices.py +28 -3
  33. infrahub/core/validators/attribute/kind.py +5 -1
  34. infrahub/core/validators/determiner.py +22 -2
  35. infrahub/events/__init__.py +2 -0
  36. infrahub/events/proposed_change_action.py +22 -0
  37. infrahub/graphql/app.py +2 -1
  38. infrahub/graphql/context.py +1 -1
  39. infrahub/graphql/mutations/proposed_change.py +5 -0
  40. infrahub/graphql/mutations/relationship.py +1 -1
  41. infrahub/graphql/mutations/schema.py +14 -1
  42. infrahub/graphql/queries/diff/tree.py +53 -2
  43. infrahub/graphql/schema.py +3 -14
  44. infrahub/graphql/types/event.py +8 -0
  45. infrahub/permissions/__init__.py +3 -0
  46. infrahub/permissions/constants.py +13 -0
  47. infrahub/permissions/globals.py +32 -0
  48. infrahub/task_manager/event.py +5 -1
  49. infrahub_sdk/client.py +8 -8
  50. infrahub_sdk/node/node.py +2 -2
  51. infrahub_sdk/protocols.py +6 -40
  52. infrahub_sdk/pytest_plugin/items/graphql_query.py +1 -1
  53. infrahub_sdk/schema/repository.py +1 -1
  54. infrahub_sdk/testing/docker.py +1 -1
  55. infrahub_sdk/utils.py +11 -7
  56. {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/METADATA +4 -4
  57. {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/RECORD +60 -56
  58. {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/LICENSE.txt +0 -0
  59. {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/WHEEL +0 -0
  60. {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/entry_points.txt +0 -0
infrahub/api/schema.py CHANGED
@@ -36,6 +36,7 @@ from infrahub.events import EventMeta
36
36
  from infrahub.events.schema_action import SchemaUpdatedEvent
37
37
  from infrahub.exceptions import MigrationError
38
38
  from infrahub.log import get_log_data, get_logger
39
+ from infrahub.permissions import define_global_permission_from_branch
39
40
  from infrahub.types import ATTRIBUTE_PYTHON_TYPES
40
41
  from infrahub.worker import WORKER_IDENTITY
41
42
  from infrahub.workflows.catalogue import SCHEMA_APPLY_MIGRATION, SCHEMA_VALIDATE_MIGRATION
@@ -287,13 +288,8 @@ async def load_schema(
287
288
  context: InfrahubContext = Depends(get_context),
288
289
  ) -> SchemaUpdate:
289
290
  permission_manager.raise_for_permission(
290
- permission=GlobalPermission(
291
- action=GlobalPermissions.MANAGE_SCHEMA.value,
292
- decision=(
293
- PermissionDecision.ALLOW_DEFAULT
294
- if branch.name in (GLOBAL_BRANCH_NAME, registry.default_branch)
295
- else PermissionDecision.ALLOW_OTHER
296
- ).value,
291
+ permission=define_global_permission_from_branch(
292
+ permission=GlobalPermissions.MANAGE_SCHEMA, branch_name=branch.name
297
293
  )
298
294
  )
299
295
 
infrahub/cli/db.py CHANGED
@@ -53,6 +53,7 @@ from infrahub.database.neo4j import IndexManagerNeo4j
53
53
  from infrahub.log import get_logger
54
54
 
55
55
  from .constants import ERROR_BADGE, FAILED_BADGE, SUCCESS_BADGE
56
+ from .db_commands.check_inheritance import check_inheritance
56
57
  from .patch import patch_app
57
58
 
58
59
 
@@ -175,6 +176,30 @@ async def migrate_cmd(
175
176
  await dbdriver.close()
176
177
 
177
178
 
179
+ @app.command(name="check-inheritance")
180
+ async def check_inheritance_cmd(
181
+ ctx: typer.Context,
182
+ fix: bool = typer.Option(False, help="Fix the inheritance of any invalid nodes."),
183
+ config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
184
+ ) -> None:
185
+ """Check the database for any vertices with incorrect inheritance"""
186
+ logging.getLogger("infrahub").setLevel(logging.WARNING)
187
+ logging.getLogger("neo4j").setLevel(logging.ERROR)
188
+ logging.getLogger("prefect").setLevel(logging.ERROR)
189
+
190
+ config.load_and_exit(config_file_name=config_file)
191
+
192
+ context: CliContext = ctx.obj
193
+ dbdriver = await context.init_db(retry=1)
194
+ await initialize_registry(db=dbdriver)
195
+
196
+ success = await check_inheritance(db=dbdriver, fix=fix)
197
+ if not success:
198
+ raise typer.Exit(code=1)
199
+
200
+ await dbdriver.close()
201
+
202
+
178
203
  @app.command(name="update-core-schema")
179
204
  async def update_core_schema_cmd(
180
205
  ctx: typer.Context,
File without changes
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from rich import print as rprint
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from infrahub.core import registry
12
+ from infrahub.core.branch.models import Branch
13
+ from infrahub.core.constants import InfrahubKind
14
+ from infrahub.core.migrations.query.node_duplicate import NodeDuplicateQuery, SchemaNodeInfo
15
+ from infrahub.core.query import Query, QueryType
16
+ from infrahub.core.schema import SchemaRoot, internal_schema
17
+ from infrahub.core.schema.manager import SchemaManager
18
+ from infrahub.log import get_logger
19
+
20
+ from ..constants import FAILED_BADGE, SUCCESS_BADGE
21
+
22
+ if TYPE_CHECKING:
23
+ from infrahub.core.schema.node_schema import NodeSchema
24
+ from infrahub.database import InfrahubDatabase
25
+
26
+ log = get_logger()
27
+
28
+
29
+ class GetSchemaWithUpdatedInheritance(Query):
30
+ """
31
+ Get the name, namespace, and branch of any SchemaNodes with _updated_ inheritance
32
+ This query will only return schemas that have had `inherit_from` updated after they were created
33
+ """
34
+
35
+ name = "get_schema_with_updated_inheritance"
36
+ type = QueryType.READ
37
+ insert_return = False
38
+
39
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
40
+ query = """
41
+ // find inherit_from attributes that have been updated
42
+ MATCH p = (schema_node:SchemaNode)-[has_attr_e:HAS_ATTRIBUTE {status: "active"}]->(a:Attribute {name: "inherit_from"})
43
+ WHERE has_attr_e.to IS NULL
44
+ CALL (a) {
45
+ // only get branches on which the value was updated, we can ignore the initial create
46
+ MATCH (a)-[e:HAS_VALUE]->(:AttributeValue)
47
+ ORDER BY e.from ASC
48
+ // tail leaves out the earliest one, which is the initial create
49
+ RETURN tail(collect(e.branch)) AS branches
50
+ }
51
+ WITH schema_node, a, branches
52
+ WHERE size(branches) > 0
53
+ UNWIND branches AS branch
54
+ WITH DISTINCT schema_node, a, branch
55
+
56
+ //get branch details
57
+ CALL (branch) {
58
+ MATCH (b:Branch {name: branch})
59
+ RETURN b.branched_from AS branched_from, b.hierarchy_level AS branch_level
60
+ }
61
+
62
+ // get the namespace for the schema
63
+ CALL (schema_node, a, branch, branched_from, branch_level) {
64
+ MATCH (schema_node)-[e1:HAS_ATTRIBUTE]-(:Attribute {name: "namespace"})-[e2:HAS_VALUE]->(av)
65
+ WHERE (
66
+ e1.branch = branch OR
67
+ (e1.branch_level < branch_level AND e1.from <= branched_from)
68
+ ) AND e1.to IS NULL
69
+ AND e1.status = "active"
70
+ AND (
71
+ e2.branch = branch OR
72
+ (e2.branch_level < branch_level AND e2.from <= branched_from)
73
+ ) AND e2.to IS NULL
74
+ AND e2.status = "active"
75
+ ORDER BY e2.branch_level DESC, e1.branch_level DESC, e2.from DESC, e1.from DESC
76
+ RETURN av.value AS namespace
77
+ LIMIT 1
78
+ }
79
+
80
+ // get the name for the schema
81
+ CALL (schema_node, a, branch, branched_from, branch_level) {
82
+ MATCH (schema_node)-[e1:HAS_ATTRIBUTE]-(:Attribute {name: "name"})-[e2:HAS_VALUE]->(av)
83
+ WHERE (
84
+ e1.branch = branch OR
85
+ (e1.branch_level < branch_level AND e1.from <= branched_from)
86
+ ) AND e1.to IS NULL
87
+ AND e1.status = "active"
88
+ AND (
89
+ e2.branch = branch OR
90
+ (e2.branch_level < branch_level AND e2.from <= branched_from)
91
+ ) AND e2.to IS NULL
92
+ AND e2.status = "active"
93
+ ORDER BY e2.branch_level DESC, e1.branch_level DESC, e2.from DESC, e1.from DESC
94
+ RETURN av.value AS name
95
+ LIMIT 1
96
+ }
97
+ RETURN name, namespace, branch
98
+ """
99
+ self.return_labels = ["name", "namespace", "branch"]
100
+ self.add_to_query(query)
101
+
102
+ def get_updated_inheritance_kinds_by_branch(self) -> dict[str, list[str]]:
103
+ kinds_by_branch: dict[str, list[str]] = defaultdict(list)
104
+ for result in self.results:
105
+ name = result.get_as_type(label="name", return_type=str)
106
+ namespace = result.get_as_type(label="namespace", return_type=str)
107
+ branch = result.get_as_type(label="branch", return_type=str)
108
+ kinds_by_branch[branch].append(f"{namespace}{name}")
109
+ return kinds_by_branch
110
+
111
+
112
+ @dataclass
113
+ class KindLabelCount:
114
+ kind: str
115
+ labels: frozenset[str]
116
+ num_nodes: int
117
+
118
+
119
+ @dataclass
120
+ class KindLabelCountCorrected(KindLabelCount):
121
+ node_schema: NodeSchema
122
+
123
+
124
+ class GetAllKindsAndLabels(Query):
125
+ """
126
+ Get the kind, labels, and number of nodes for the given kinds and branch
127
+ """
128
+
129
+ name = "get_all_kinds_and_labels"
130
+ type = QueryType.READ
131
+ insert_return = False
132
+
133
+ def __init__(self, kinds: list[str] | None = None, **kwargs: Any) -> None:
134
+ super().__init__(**kwargs)
135
+ self.kinds = kinds
136
+
137
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
138
+ self.params["branch_name"] = self.branch.name
139
+ self.params["branched_from"] = self.branch.get_branched_from()
140
+ self.params["branch_level"] = self.branch.hierarchy_level
141
+ kinds_str = "Node"
142
+ if self.kinds:
143
+ kinds_str = "|".join(self.kinds)
144
+ query = """
145
+ MATCH (n:%(kinds_str)s)-[r:IS_PART_OF]->(:Root)
146
+ WHERE (
147
+ r.branch = $branch_name OR
148
+ (r.branch_level < $branch_level AND r.from <= $branched_from)
149
+ )
150
+ AND r.to IS NULL
151
+ AND r.status = "active"
152
+ RETURN DISTINCT n.kind AS kind, labels(n) AS labels, count(*) AS num_nodes
153
+ ORDER BY kind ASC
154
+ """ % {"kinds_str": kinds_str}
155
+ self.return_labels = ["kind", "labels", "num_nodes"]
156
+ self.add_to_query(query)
157
+
158
+ def get_kind_label_counts(self) -> list[KindLabelCount]:
159
+ kind_label_counts: list[KindLabelCount] = []
160
+ for result in self.results:
161
+ kind = result.get_as_type(label="kind", return_type=str)
162
+ num_nodes = result.get_as_type(label="num_nodes", return_type=int)
163
+ labels: list[str] = result.get_as_type(label="labels", return_type=list)
164
+ # we can ignore the Node label and the label that matches the kind
165
+ cleaned_labels = frozenset(str(lbl) for lbl in labels if lbl not in ["Node", "CoreNode", kind])
166
+ kind_label_counts.append(KindLabelCount(kind=kind, labels=cleaned_labels, num_nodes=num_nodes))
167
+ return kind_label_counts
168
+
169
+
170
+ def display_kind_label_counts(kind_label_counts_by_branch: dict[str, list[KindLabelCountCorrected]]) -> None:
171
+ console = Console()
172
+
173
+ table = Table(title="Incorrect Inheritance Nodes")
174
+
175
+ table.add_column("Branch")
176
+ table.add_column("Kind")
177
+ table.add_column("Incorrect Labels")
178
+ table.add_column("Num Nodes")
179
+
180
+ for branch_name, kind_label_counts in kind_label_counts_by_branch.items():
181
+ for kind_label_count in kind_label_counts:
182
+ table.add_row(
183
+ branch_name, kind_label_count.kind, str(list(kind_label_count.labels)), str(kind_label_count.num_nodes)
184
+ )
185
+
186
+ console.print(table)
187
+
188
+
189
+ async def check_inheritance(db: InfrahubDatabase, fix: bool = False) -> bool:
190
+ """
191
+ Run migrations to update the inheritance of any nodes with incorrect inheritance from a failed migration
192
+ 1. Identifies node schemas that have had their inheritance updated after they were created
193
+ a. includes the kind and branch of the inheritance update
194
+ 2. Checks nodes of the given kinds on the given branch to verify their inheritance is correct
195
+ 3. Displays counts of any kinds with incorrect inheritance on the given branch
196
+ 4. If fix is True, runs migrations to update the inheritance of any nodes with incorrect inheritance
197
+ on the correct branch
198
+ """
199
+
200
+ updated_inheritance_query = await GetSchemaWithUpdatedInheritance.init(db=db)
201
+ await updated_inheritance_query.execute(db=db)
202
+ updated_inheritance_kinds_by_branch = updated_inheritance_query.get_updated_inheritance_kinds_by_branch()
203
+
204
+ if not updated_inheritance_kinds_by_branch:
205
+ rprint(f"{SUCCESS_BADGE} No schemas have had their inheritance updated")
206
+ return True
207
+
208
+ schema_manager = SchemaManager()
209
+ registry.schema = schema_manager
210
+ schema = SchemaRoot(**internal_schema)
211
+ schema_manager.register_schema(schema=schema)
212
+ branches_by_name = {b.name: b for b in await Branch.get_list(db=db)}
213
+
214
+ kind_label_counts_by_branch: dict[str, list[KindLabelCountCorrected]] = defaultdict(list)
215
+ for branch_name, kinds in updated_inheritance_kinds_by_branch.items():
216
+ rprint(f"Checking branch: {branch_name}", end="...")
217
+ branch = branches_by_name[branch_name]
218
+ schema_branch = await schema_manager.load_schema_from_db(db=db, branch=branch)
219
+ kind_label_query = await GetAllKindsAndLabels.init(db=db, branch=branch, kinds=kinds)
220
+ await kind_label_query.execute(db=db)
221
+ kind_label_counts = kind_label_query.get_kind_label_counts()
222
+
223
+ for kind_label_count in kind_label_counts:
224
+ node_schema = schema_branch.get_node(name=kind_label_count.kind, duplicate=False)
225
+ correct_labels = frozenset(node_schema.inherit_from)
226
+ if kind_label_count.labels == correct_labels:
227
+ continue
228
+
229
+ kind_label_counts_by_branch[branch_name].append(
230
+ KindLabelCountCorrected(
231
+ kind=kind_label_count.kind,
232
+ labels=kind_label_count.labels,
233
+ num_nodes=kind_label_count.num_nodes,
234
+ node_schema=node_schema,
235
+ )
236
+ )
237
+ rprint("done")
238
+
239
+ if not kind_label_counts_by_branch:
240
+ rprint(f"{SUCCESS_BADGE} All nodes have the correct inheritance")
241
+ return True
242
+
243
+ display_kind_label_counts(kind_label_counts_by_branch)
244
+
245
+ if not fix:
246
+ rprint(f"{FAILED_BADGE} Use the --fix flag to fix the inheritance of any invalid nodes")
247
+ return False
248
+
249
+ for branch_name, kind_label_counts_corrected in kind_label_counts_by_branch.items():
250
+ for kind_label_count in kind_label_counts_corrected:
251
+ rprint(f"Fixing kind {kind_label_count.kind} on branch {branch_name}", end="...")
252
+ node_schema = kind_label_count.node_schema
253
+ migration_query = await NodeDuplicateQuery.init(
254
+ db=db,
255
+ branch=branches_by_name[branch_name],
256
+ previous_node=SchemaNodeInfo(
257
+ name=node_schema.name,
258
+ namespace=node_schema.namespace,
259
+ branch_support=node_schema.branch.value,
260
+ labels=list(kind_label_count.labels) + [kind_label_count.kind, InfrahubKind.NODE],
261
+ kind=kind_label_count.kind,
262
+ ),
263
+ new_node=SchemaNodeInfo(
264
+ name=node_schema.name,
265
+ namespace=node_schema.namespace,
266
+ branch_support=node_schema.branch.value,
267
+ labels=list(node_schema.inherit_from) + [kind_label_count.kind, InfrahubKind.NODE],
268
+ kind=kind_label_count.kind,
269
+ ),
270
+ )
271
+ await migration_query.execute(db=db)
272
+ rprint("done")
273
+
274
+ rprint(f"{SUCCESS_BADGE} All nodes have the correct inheritance")
275
+
276
+ if registry.default_branch in kind_label_counts_by_branch:
277
+ kinds = [kind_label_count.kind for kind_label_count in kind_label_counts_by_branch[registry.default_branch]]
278
+ rprint(
279
+ "[bold cyan]Note that migrations were run on the default branch for the following schema kinds: "
280
+ f"{', '.join(kinds)}. You should rebase any branches that include/will include changes using "
281
+ "the migrated schemas[/bold cyan]"
282
+ )
283
+
284
+ return True
infrahub/cli/upgrade.py CHANGED
@@ -14,6 +14,7 @@ from infrahub import config
14
14
  from infrahub.core.initialization import create_anonymous_role, create_default_account_groups, initialize_registry
15
15
  from infrahub.core.manager import NodeManager
16
16
  from infrahub.core.protocols import CoreAccount, CoreObjectPermission
17
+ from infrahub.dependencies.registry import build_component_registry
17
18
  from infrahub.menu.menu import default_menu
18
19
  from infrahub.menu.models import MenuDict
19
20
  from infrahub.menu.repository import MenuRepository
@@ -54,6 +55,8 @@ async def upgrade_cmd(
54
55
 
55
56
  await initialize_registry(db=dbdriver)
56
57
 
58
+ build_component_registry()
59
+
57
60
  # NOTE add step to validate if the database and the task manager are reachable
58
61
 
59
62
  # -------------------------------------------
infrahub/config.py CHANGED
@@ -328,6 +328,8 @@ class DevelopmentSettings(BaseSettings):
328
328
 
329
329
 
330
330
  class BrokerSettings(BaseSettings):
331
+ """Configuration settings for the message bus."""
332
+
331
333
  model_config = SettingsConfigDict(env_prefix="INFRAHUB_BROKER_")
332
334
  enable: bool = True
333
335
  tls_enabled: bool = Field(default=False, description="Indicates if TLS is enabled for the connection")
@@ -441,9 +443,7 @@ class GitSettings(BaseSettings):
441
443
 
442
444
 
443
445
  class HTTPSettings(BaseSettings):
444
- """The HTTP settings control how Infrahub interacts with external HTTP servers
445
-
446
- This can be things like webhooks and OAuth2 providers"""
446
+ """The HTTP settings control how Infrahub interacts with external HTTP servers. This can be things like webhooks and OAuth2 providers."""
447
447
 
448
448
  model_config = SettingsConfigDict(env_prefix="INFRAHUB_HTTP_")
449
449
  timeout: int = Field(default=10, description="Default connection timeout in seconds")
@@ -667,7 +667,7 @@ class SecuritySettings(BaseSettings):
667
667
  oidc_providers: list[OIDCProvider] = Field(default_factory=list, description="The selected OIDC providers")
668
668
  oidc_provider_settings: SecurityOIDCProviderSettings = Field(default_factory=SecurityOIDCProviderSettings)
669
669
  restrict_untrusted_jinja2_filters: bool = Field(
670
- default=True, description="Indicates if untrusted Jinja2 filters should be disallowd for computed attributes"
670
+ default=True, description="Indicates if untrusted Jinja2 filters should be disallowed for computed attributes"
671
671
  )
672
672
  _oauth2_settings: dict[str, SecurityOAuth2Settings] = PrivateAttr(default_factory=dict)
673
673
  _oidc_settings: dict[str, SecurityOIDCSettings] = PrivateAttr(default_factory=dict)
@@ -680,6 +680,12 @@ class String(BaseAttribute):
680
680
  type = str
681
681
  value: str
682
682
 
683
+ @classmethod
684
+ def validate_content(cls, value: Any, name: str, schema: AttributeSchema) -> None:
685
+ if value is not None and not is_large_attribute_type(schema.kind):
686
+ validate_string_length(value=str(value))
687
+ super().validate_content(value=value, name=name, schema=schema)
688
+
683
689
 
684
690
  class StringOptional(String):
685
691
  value: str | None
@@ -66,6 +66,7 @@ class EventType(InfrahubStringEnum):
66
66
  PROPOSED_CHANGE_APPROVED = f"{EVENT_NAMESPACE}.proposed_change.approved"
67
67
  PROPOSED_CHANGE_REJECTED = f"{EVENT_NAMESPACE}.proposed_change.rejected"
68
68
  PROPOSED_CHANGE_APPROVAL_REVOKED = f"{EVENT_NAMESPACE}.proposed_change.approval_revoked"
69
+ PROPOSED_CHANGE_APPROVALS_REVOKED = f"{EVENT_NAMESPACE}.proposed_change.approvals_revoked"
69
70
  PROPOSED_CHANGE_REJECTION_REVOKED = f"{EVENT_NAMESPACE}.proposed_change.rejection_revoked"
70
71
  PROPOSED_CHANGE_THREAD_CREATED = f"{EVENT_NAMESPACE}.proposed_change_thread.created"
71
72
  PROPOSED_CHANGE_THREAD_UPDATED = f"{EVENT_NAMESPACE}.proposed_change_thread.updated"
@@ -22,8 +22,6 @@ if TYPE_CHECKING:
22
22
  from neo4j.graph import Relationship as Neo4jRelationship
23
23
  from whenever import TimeDelta
24
24
 
25
- from infrahub.graphql.initialization import GraphqlContext
26
-
27
25
 
28
26
  @dataclass
29
27
  class TimeRange:
@@ -314,12 +312,6 @@ class EnrichedDiffRelationship(BaseSummary):
314
312
  )
315
313
 
316
314
 
317
- @dataclass
318
- class ParentNodeInfo:
319
- node: EnrichedDiffNode
320
- relationship_name: str = "undefined"
321
-
322
-
323
315
  @dataclass
324
316
  class EnrichedDiffNode(BaseSummary):
325
317
  identifier: NodeIdentifier
@@ -364,37 +356,6 @@ class EnrichedDiffNode(BaseSummary):
364
356
  rel.clear_conflicts()
365
357
  self.conflict = None
366
358
 
367
- def get_parent_info(self, graphql_context: GraphqlContext | None = None) -> ParentNodeInfo | None:
368
- for r in self.relationships:
369
- for n in r.nodes:
370
- relationship_name: str = "undefined"
371
-
372
- if not graphql_context:
373
- return ParentNodeInfo(node=n, relationship_name=relationship_name)
374
-
375
- node_schema = graphql_context.db.schema.get(name=self.kind)
376
- rel_schema = node_schema.get_relationship(name=r.name)
377
-
378
- parent_schema = graphql_context.db.schema.get(name=n.kind)
379
- rels_parent = parent_schema.get_relationships_by_identifier(id=rel_schema.get_identifier())
380
-
381
- if rels_parent and len(rels_parent) == 1:
382
- relationship_name = rels_parent[0].name
383
- elif rels_parent and len(rels_parent) > 1:
384
- for rel_parent in rels_parent:
385
- if (
386
- rel_schema.direction == RelationshipDirection.INBOUND
387
- and rel_parent.direction == RelationshipDirection.OUTBOUND
388
- ) or (
389
- rel_schema.direction == RelationshipDirection.OUTBOUND
390
- and rel_parent.direction == RelationshipDirection.INBOUND
391
- ):
392
- relationship_name = rel_parent.name
393
- break
394
-
395
- return ParentNodeInfo(node=n, relationship_name=relationship_name)
396
- return None
397
-
398
359
  def get_all_child_nodes(self) -> set[EnrichedDiffNode]:
399
360
  all_children = set()
400
361
  for r in self.relationships:
@@ -1 +1 @@
1
- GRAPH_VERSION = 36
1
+ GRAPH_VERSION = 37
@@ -37,7 +37,7 @@ from infrahub.exceptions import DatabaseError
37
37
  from infrahub.graphql.manager import GraphQLSchemaManager
38
38
  from infrahub.log import get_logger
39
39
  from infrahub.menu.utils import create_default_menu
40
- from infrahub.permissions import PermissionBackend
40
+ from infrahub.permissions import PermissionBackend, get_or_create_global_permission
41
41
  from infrahub.storage import InfrahubObjectStorage
42
42
 
43
43
  if TYPE_CHECKING:
@@ -371,20 +371,13 @@ async def create_default_role(db: InfrahubDatabase) -> CoreAccountRole:
371
371
  await proposed_change_permission.save(db=db)
372
372
 
373
373
  # Other permissions, created to keep references of them from the start
374
- for permission_action, permission_description in (
375
- (GlobalPermissions.EDIT_DEFAULT_BRANCH, "Allow a user to change data in the default branch"),
376
- (GlobalPermissions.MANAGE_ACCOUNTS, "Allow a user to manage accounts, account roles and account groups"),
377
- (GlobalPermissions.MANAGE_PERMISSIONS, "Allow a user to manage permissions"),
378
- (GlobalPermissions.MERGE_BRANCH, "Allow a user to merge branches"),
374
+ for permission_action in (
375
+ GlobalPermissions.EDIT_DEFAULT_BRANCH,
376
+ GlobalPermissions.MANAGE_ACCOUNTS,
377
+ GlobalPermissions.MANAGE_PERMISSIONS,
378
+ GlobalPermissions.MERGE_BRANCH,
379
379
  ):
380
- permission = await Node.init(db=db, schema=InfrahubKind.GLOBALPERMISSION)
381
- await permission.new(
382
- db=db,
383
- action=permission_action.value,
384
- decision=PermissionDecision.ALLOW_ALL.value,
385
- description=permission_description,
386
- )
387
- await permission.save(db=db)
380
+ await get_or_create_global_permission(db=db, permission=permission_action)
388
381
 
389
382
  view_permission = await Node.init(db=db, schema=InfrahubKind.OBJECTPERMISSION)
390
383
  await view_permission.new(
@@ -428,18 +421,31 @@ async def create_default_role(db: InfrahubDatabase) -> CoreAccountRole:
428
421
 
429
422
 
430
423
  async def create_proposed_change_reviewer_role(db: InfrahubDatabase) -> CoreAccountRole:
431
- reviewer_permission = await Node.init(db=db, schema=InfrahubKind.GLOBALPERMISSION)
432
- await reviewer_permission.new(
424
+ edit_default_branch_permission = await get_or_create_global_permission(
425
+ db=db, permission=GlobalPermissions.EDIT_DEFAULT_BRANCH
426
+ )
427
+ reviewer_permission = await get_or_create_global_permission(
428
+ db=db, permission=GlobalPermissions.REVIEW_PROPOSED_CHANGE
429
+ )
430
+
431
+ proposed_change_update_permission = await Node.init(db=db, schema=InfrahubKind.OBJECTPERMISSION)
432
+ await proposed_change_update_permission.new(
433
433
  db=db,
434
- action=GlobalPermissions.REVIEW_PROPOSED_CHANGE.value,
434
+ name="ProposedChange",
435
+ namespace="Core",
436
+ action=PermissionAction.UPDATE.value,
435
437
  decision=PermissionDecision.ALLOW_ALL.value,
436
- description="Allow a user to approve or revoke proposed changes",
438
+ description="Allow a user to update proposed changes",
437
439
  )
438
- await reviewer_permission.save(db=db)
440
+ await proposed_change_update_permission.save(db=db)
439
441
 
440
442
  role_name = "Proposed Change Reviewer"
441
443
  role = await Node.init(db=db, schema=CoreAccountRole)
442
- await role.new(db=db, name=role_name, permissions=[reviewer_permission])
444
+ await role.new(
445
+ db=db,
446
+ name=role_name,
447
+ permissions=[edit_default_branch_permission, reviewer_permission, proposed_change_update_permission],
448
+ )
443
449
  await role.save(db=db)
444
450
  log.info(f"Created account role: {role_name}")
445
451
 
@@ -497,7 +503,6 @@ async def create_default_account_groups(
497
503
 
498
504
  default_role = await create_default_role(db=db)
499
505
  proposed_change_reviewer_role = await create_proposed_change_reviewer_role(db=db)
500
-
501
506
  await create_accounts_group(
502
507
  db=db, name="Infrahub Users", roles=[default_role, proposed_change_reviewer_role], accounts=accounts or []
503
508
  )
infrahub/core/manager.py CHANGED
@@ -400,7 +400,7 @@ class NodeManager:
400
400
 
401
401
  results = []
402
402
  for peer in peers_info:
403
- result = await Relationship(schema=schema, branch=branch, at=at, node_id=peer.source_id).load(
403
+ result = Relationship(schema=schema, branch=branch, at=at, node_id=peer.source_id).load(
404
404
  db=db,
405
405
  id=peer.rel_node_id,
406
406
  db_id=peer.rel_node_db_id,
@@ -408,7 +408,7 @@ class NodeManager:
408
408
  data=peer,
409
409
  )
410
410
  if fetch_peers:
411
- await result.set_peer(value=peer_nodes[peer.peer_id])
411
+ result.set_peer(value=peer_nodes[peer.peer_id])
412
412
  results.append(result)
413
413
 
414
414
  return results
@@ -1,3 +1,4 @@
1
+ from .schema.attribute_kind_update import AttributeKindUpdateMigration
1
2
  from .schema.attribute_name_update import AttributeNameUpdateMigration
2
3
  from .schema.node_attribute_add import NodeAttributeAddMigration
3
4
  from .schema.node_attribute_remove import NodeAttributeRemoveMigration
@@ -17,6 +18,7 @@ MIGRATION_MAP: dict[str, type[SchemaMigration] | None] = {
17
18
  "node.relationship.remove": PlaceholderDummyMigration,
18
19
  "attribute.name.update": AttributeNameUpdateMigration,
19
20
  "attribute.branch.update": None,
21
+ "attribute.kind.update": AttributeKindUpdateMigration,
20
22
  "relationship.branch.update": None,
21
23
  "relationship.direction.update": None,
22
24
  "relationship.identifier.update": PlaceholderDummyMigration,
@@ -36,8 +36,9 @@ from .m031_check_number_attributes import Migration031
36
36
  from .m032_cleanup_orphaned_branch_relationships import Migration032
37
37
  from .m033_deduplicate_relationship_vertices import Migration033
38
38
  from .m034_find_orphaned_schema_fields import Migration034
39
- from .m035_drop_attr_value_index import Migration035
40
- from .m036_index_attr_vals import Migration036
39
+ from .m035_orphan_relationships import Migration035
40
+ from .m036_drop_attr_value_index import Migration036
41
+ from .m037_index_attr_vals import Migration037
41
42
 
42
43
  if TYPE_CHECKING:
43
44
  from infrahub.core.root import Root
@@ -81,6 +82,7 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
81
82
  Migration034,
82
83
  Migration035,
83
84
  Migration036,
85
+ Migration037,
84
86
  ]
85
87
 
86
88
 
@@ -89,7 +89,7 @@ class Migration033(GraphMigration):
89
89
  """
90
90
 
91
91
  name: str = "033_deduplicate_relationship_vertices"
92
- minimum_version: int = 31
92
+ minimum_version: int = 32
93
93
  queries: Sequence[type[Query]] = [DeduplicateRelationshipVerticesQuery]
94
94
 
95
95
  async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.core.migrations.shared import GraphMigration, MigrationResult
6
+
7
+ from ...query import Query, QueryType
8
+
9
+ if TYPE_CHECKING:
10
+ from infrahub.database import InfrahubDatabase
11
+
12
+
13
+ class CleanupOrphanedRelationshipsQuery(Query):
14
+ name = "cleanup_orphaned_relationships"
15
+ type = QueryType.WRITE
16
+ insert_return = False
17
+
18
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
19
+ query = """
20
+ MATCH (rel:Relationship)-[:IS_RELATED]-(peer:Node)
21
+ WITH DISTINCT rel, peer.uuid AS p_uuid
22
+ WITH rel, count(*) AS num_peers
23
+ WHERE num_peers < 2
24
+ DETACH DELETE rel
25
+ """
26
+ self.add_to_query(query)
27
+
28
+
29
+ class Migration035(GraphMigration):
30
+ """
31
+ Remove Relationship vertices that only have a single peer
32
+ """
33
+
34
+ name: str = "035_clean_up_orphaned_relationships"
35
+ minimum_version: int = 34
36
+ queries: Sequence[type[Query]] = [CleanupOrphanedRelationshipsQuery]
37
+
38
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
39
+ return MigrationResult()
40
+
41
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
42
+ # overrides parent class to skip transaction in case there are a lot of relationships to delete
43
+ return await self.do_execute(db=db)