infrahub-server 1.3.1__py3-none-any.whl → 1.3.3__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.
- infrahub/cli/db.py +194 -13
- infrahub/core/branch/enums.py +8 -0
- infrahub/core/branch/models.py +28 -5
- infrahub/core/branch/tasks.py +5 -7
- infrahub/core/diff/calculator.py +4 -1
- infrahub/core/diff/coordinator.py +32 -34
- infrahub/core/diff/diff_locker.py +26 -0
- infrahub/core/diff/query_parser.py +23 -32
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +4 -3
- infrahub/core/merge.py +31 -16
- infrahub/core/migrations/graph/__init__.py +24 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +4 -3
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +4 -3
- infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +105 -0
- infrahub/core/migrations/graph/m033_deduplicate_relationship_vertices.py +97 -0
- infrahub/core/node/__init__.py +3 -0
- infrahub/core/node/constraints/grouped_uniqueness.py +88 -132
- infrahub/core/node/resource_manager/ip_address_pool.py +5 -3
- infrahub/core/node/resource_manager/ip_prefix_pool.py +7 -4
- infrahub/core/node/resource_manager/number_pool.py +3 -1
- infrahub/core/node/standard.py +4 -0
- infrahub/core/query/branch.py +25 -56
- infrahub/core/query/node.py +78 -24
- infrahub/core/query/relationship.py +11 -8
- infrahub/core/relationship/model.py +10 -5
- infrahub/core/validators/uniqueness/model.py +17 -0
- infrahub/core/validators/uniqueness/query.py +212 -1
- infrahub/dependencies/builder/diff/coordinator.py +3 -0
- infrahub/dependencies/builder/diff/locker.py +8 -0
- infrahub/graphql/mutations/main.py +25 -4
- infrahub/graphql/mutations/tasks.py +2 -0
- infrahub_sdk/node/node.py +22 -10
- infrahub_sdk/node/related_node.py +7 -0
- {infrahub_server-1.3.1.dist-info → infrahub_server-1.3.3.dist-info}/METADATA +1 -1
- {infrahub_server-1.3.1.dist-info → infrahub_server-1.3.3.dist-info}/RECORD +42 -37
- infrahub_testcontainers/container.py +1 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +3 -0
- infrahub_testcontainers/docker-compose.test.yml +1 -0
- {infrahub_server-1.3.1.dist-info → infrahub_server-1.3.3.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.3.1.dist-info → infrahub_server-1.3.3.dist-info}/WHEEL +0 -0
- {infrahub_server-1.3.1.dist-info → infrahub_server-1.3.3.dist-info}/entry_points.txt +0 -0
|
@@ -486,6 +486,7 @@ class DiffQueryParser:
|
|
|
486
486
|
self._previous_node_field_specifiers = previous_node_field_specifiers or NodeFieldSpecifierMap()
|
|
487
487
|
self._new_node_field_specifiers: NodeFieldSpecifierMap | None = None
|
|
488
488
|
self._current_node_field_specifiers: NodeFieldSpecifierMap | None = None
|
|
489
|
+
self._diff_node_field_specifiers: NodeFieldSpecifierMap = NodeFieldSpecifierMap()
|
|
489
490
|
|
|
490
491
|
def get_branches(self) -> set[str]:
|
|
491
492
|
return set(self._final_diff_root_by_branch.keys())
|
|
@@ -497,33 +498,17 @@ class DiffQueryParser:
|
|
|
497
498
|
return self._final_diff_root_by_branch[branch]
|
|
498
499
|
return DiffRoot(from_time=self.from_time, to_time=self.to_time, uuid=str(uuid4()), branch=branch, nodes=[])
|
|
499
500
|
|
|
500
|
-
def get_diff_node_field_specifiers(self) -> NodeFieldSpecifierMap:
|
|
501
|
-
node_field_specifiers_map = NodeFieldSpecifierMap()
|
|
502
|
-
if self.diff_branch_name not in self._diff_root_by_branch:
|
|
503
|
-
return node_field_specifiers_map
|
|
504
|
-
diff_root = self._diff_root_by_branch[self.diff_branch_name]
|
|
505
|
-
for node in diff_root.nodes_by_identifier.values():
|
|
506
|
-
for attribute_name in node.attributes_by_name:
|
|
507
|
-
node_field_specifiers_map.add_entry(node_uuid=node.uuid, kind=node.kind, field_name=attribute_name)
|
|
508
|
-
for relationship_diff in node.relationships_by_identifier.values():
|
|
509
|
-
node_field_specifiers_map.add_entry(
|
|
510
|
-
node_uuid=node.uuid, kind=node.kind, field_name=relationship_diff.identifier
|
|
511
|
-
)
|
|
512
|
-
return node_field_specifiers_map
|
|
513
|
-
|
|
514
501
|
def get_new_node_field_specifiers(self) -> NodeFieldSpecifierMap:
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
self.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return
|
|
524
|
-
|
|
525
|
-
self._current_node_field_specifiers = self._previous_node_field_specifiers - new_node_field_specifiers
|
|
526
|
-
return self._current_node_field_specifiers
|
|
502
|
+
return self._diff_node_field_specifiers - self._previous_node_field_specifiers
|
|
503
|
+
|
|
504
|
+
def is_new_node_field_specifier(self, node_uuid: str, kind: str, field_name: str) -> bool:
|
|
505
|
+
if not self._diff_node_field_specifiers.has_entry(node_uuid=node_uuid, kind=kind, field_name=field_name):
|
|
506
|
+
return False
|
|
507
|
+
if self._previous_node_field_specifiers and self._previous_node_field_specifiers.has_entry(
|
|
508
|
+
node_uuid=node_uuid, kind=kind, field_name=field_name
|
|
509
|
+
):
|
|
510
|
+
return False
|
|
511
|
+
return True
|
|
527
512
|
|
|
528
513
|
def read_result(self, query_result: QueryResult) -> None:
|
|
529
514
|
try:
|
|
@@ -533,8 +518,6 @@ class DiffQueryParser:
|
|
|
533
518
|
return
|
|
534
519
|
database_path = DatabasePath.from_cypher_path(cypher_path=path)
|
|
535
520
|
self._parse_path(database_path=database_path)
|
|
536
|
-
self._current_node_field_specifiers = None
|
|
537
|
-
self._new_node_field_specifiers = None
|
|
538
521
|
|
|
539
522
|
def parse(self, include_unchanged: bool = False) -> None:
|
|
540
523
|
self._new_node_field_specifiers = None
|
|
@@ -617,11 +600,15 @@ class DiffQueryParser:
|
|
|
617
600
|
branch_name = database_path.deepest_branch
|
|
618
601
|
from_time = self.from_time
|
|
619
602
|
if branch_name == self.base_branch_name:
|
|
620
|
-
|
|
621
|
-
if new_node_field_specifiers.has_entry(
|
|
603
|
+
if self.is_new_node_field_specifier(
|
|
622
604
|
node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=attribute_name
|
|
623
605
|
):
|
|
624
606
|
from_time = self.diff_branched_from_time
|
|
607
|
+
else:
|
|
608
|
+
# Add to diff node field specifiers if this is the diff branch
|
|
609
|
+
self._diff_node_field_specifiers.add_entry(
|
|
610
|
+
node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=attribute_name
|
|
611
|
+
)
|
|
625
612
|
if attribute_name not in diff_node.attributes_by_name:
|
|
626
613
|
diff_node.attributes_by_name[attribute_name] = DiffAttributeIntermediate(
|
|
627
614
|
uuid=database_path.attribute_id,
|
|
@@ -663,11 +650,15 @@ class DiffQueryParser:
|
|
|
663
650
|
branch_name = database_path.deepest_branch
|
|
664
651
|
from_time = self.from_time
|
|
665
652
|
if branch_name == self.base_branch_name:
|
|
666
|
-
|
|
667
|
-
if new_node_field_specifiers.has_entry(
|
|
653
|
+
if self.is_new_node_field_specifier(
|
|
668
654
|
node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=relationship_schema.get_identifier()
|
|
669
655
|
):
|
|
670
656
|
from_time = self.diff_branched_from_time
|
|
657
|
+
else:
|
|
658
|
+
# Add to diff node field specifiers if this is the diff branch
|
|
659
|
+
self._diff_node_field_specifiers.add_entry(
|
|
660
|
+
node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=relationship_schema.get_identifier()
|
|
661
|
+
)
|
|
671
662
|
diff_relationship = DiffRelationshipIntermediate(
|
|
672
663
|
name=relationship_schema.name,
|
|
673
664
|
cardinality=relationship_schema.cardinality,
|
infrahub/core/graph/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
GRAPH_VERSION =
|
|
1
|
+
GRAPH_VERSION = 33
|
infrahub/core/initialization.py
CHANGED
|
@@ -6,6 +6,7 @@ from infrahub import config, lock
|
|
|
6
6
|
from infrahub.constants.database import DatabaseType
|
|
7
7
|
from infrahub.core import registry
|
|
8
8
|
from infrahub.core.branch import Branch
|
|
9
|
+
from infrahub.core.branch.enums import BranchStatus
|
|
9
10
|
from infrahub.core.constants import (
|
|
10
11
|
DEFAULT_IP_NAMESPACE,
|
|
11
12
|
GLOBAL_BRANCH_NAME,
|
|
@@ -224,7 +225,7 @@ async def create_root_node(db: InfrahubDatabase) -> Root:
|
|
|
224
225
|
async def create_default_branch(db: InfrahubDatabase) -> Branch:
|
|
225
226
|
branch = Branch(
|
|
226
227
|
name=registry.default_branch,
|
|
227
|
-
status=
|
|
228
|
+
status=BranchStatus.OPEN,
|
|
228
229
|
description="Default Branch",
|
|
229
230
|
hierarchy_level=1,
|
|
230
231
|
is_default=True,
|
|
@@ -241,7 +242,7 @@ async def create_default_branch(db: InfrahubDatabase) -> Branch:
|
|
|
241
242
|
async def create_global_branch(db: InfrahubDatabase) -> Branch:
|
|
242
243
|
branch = Branch(
|
|
243
244
|
name=GLOBAL_BRANCH_NAME,
|
|
244
|
-
status=
|
|
245
|
+
status=BranchStatus.OPEN,
|
|
245
246
|
description="Global Branch",
|
|
246
247
|
hierarchy_level=1,
|
|
247
248
|
is_global=True,
|
|
@@ -264,7 +265,7 @@ async def create_branch(
|
|
|
264
265
|
description = description or f"Branch {branch_name}"
|
|
265
266
|
branch = Branch(
|
|
266
267
|
name=branch_name,
|
|
267
|
-
status=
|
|
268
|
+
status=BranchStatus.OPEN,
|
|
268
269
|
hierarchy_level=2,
|
|
269
270
|
description=description,
|
|
270
271
|
is_default=False,
|
infrahub/core/merge.py
CHANGED
|
@@ -9,7 +9,7 @@ from infrahub.core.models import SchemaUpdateValidationResult
|
|
|
9
9
|
from infrahub.core.protocols import CoreRepository
|
|
10
10
|
from infrahub.core.registry import registry
|
|
11
11
|
from infrahub.core.timestamp import Timestamp
|
|
12
|
-
from infrahub.exceptions import ValidationError
|
|
12
|
+
from infrahub.exceptions import MergeFailedError, ValidationError
|
|
13
13
|
from infrahub.log import get_logger
|
|
14
14
|
|
|
15
15
|
from ..git.models import GitRepositoryMerge
|
|
@@ -18,6 +18,7 @@ from ..workflows.catalogue import GIT_REPOSITORIES_MERGE
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from infrahub.core.branch import Branch
|
|
20
20
|
from infrahub.core.diff.coordinator import DiffCoordinator
|
|
21
|
+
from infrahub.core.diff.diff_locker import DiffLocker
|
|
21
22
|
from infrahub.core.diff.merger.merger import DiffMerger
|
|
22
23
|
from infrahub.core.diff.model.path import EnrichedDiffRoot
|
|
23
24
|
from infrahub.core.diff.repository.repository import DiffRepository
|
|
@@ -39,6 +40,7 @@ class BranchMerger:
|
|
|
39
40
|
diff_coordinator: DiffCoordinator,
|
|
40
41
|
diff_merger: DiffMerger,
|
|
41
42
|
diff_repository: DiffRepository,
|
|
43
|
+
diff_locker: DiffLocker,
|
|
42
44
|
destination_branch: Branch | None = None,
|
|
43
45
|
service: InfrahubServices | None = None,
|
|
44
46
|
):
|
|
@@ -48,6 +50,7 @@ class BranchMerger:
|
|
|
48
50
|
self.diff_coordinator = diff_coordinator
|
|
49
51
|
self.diff_merger = diff_merger
|
|
50
52
|
self.diff_repository = diff_repository
|
|
53
|
+
self.diff_locker = diff_locker
|
|
51
54
|
self.migrations: list[SchemaUpdateMigrationInfo] = []
|
|
52
55
|
self._merge_at = Timestamp()
|
|
53
56
|
|
|
@@ -185,22 +188,34 @@ class BranchMerger:
|
|
|
185
188
|
)
|
|
186
189
|
log.info("Diff updated for merge")
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
async
|
|
190
|
-
|
|
191
|
+
log.info("Acquiring lock for merge")
|
|
192
|
+
async with self.diff_locker.acquire_lock(
|
|
193
|
+
target_branch_name=self.destination_branch.name,
|
|
194
|
+
source_branch_name=self.source_branch.name,
|
|
195
|
+
is_incremental=False,
|
|
191
196
|
):
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
log.info("Lock acquired for merge")
|
|
198
|
+
try:
|
|
199
|
+
errors: list[str] = []
|
|
200
|
+
async for conflict_path, conflict in self.diff_repository.get_all_conflicts_for_diff(
|
|
201
|
+
diff_branch_name=self.source_branch.name, tracking_id=BranchTrackingId(name=self.source_branch.name)
|
|
202
|
+
):
|
|
203
|
+
if conflict.selected_branch is None or conflict.resolvable is False:
|
|
204
|
+
errors.append(conflict_path)
|
|
205
|
+
|
|
206
|
+
if errors:
|
|
207
|
+
raise ValidationError(
|
|
208
|
+
f"Unable to merge the branch '{self.source_branch.name}', conflict resolution missing: {', '.join(errors)}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# TODO need to find a way to properly communicate back to the user any issue that could come up during the merge
|
|
212
|
+
# From the Graph or From the repositories
|
|
213
|
+
self._merge_at = Timestamp(at)
|
|
214
|
+
branch_diff = await self.diff_merger.merge_graph(at=self._merge_at)
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
log.exception("Merge failed, beginning rollback")
|
|
217
|
+
await self.rollback()
|
|
218
|
+
raise MergeFailedError(branch_name=self.source_branch.name) from exc
|
|
204
219
|
await self.merge_repositories()
|
|
205
220
|
return branch_diff
|
|
206
221
|
|
|
@@ -33,6 +33,8 @@ from .m028_delete_diffs import Migration028
|
|
|
33
33
|
from .m029_duplicates_cleanup import Migration029
|
|
34
34
|
from .m030_illegal_edges import Migration030
|
|
35
35
|
from .m031_check_number_attributes import Migration031
|
|
36
|
+
from .m032_cleanup_orphaned_branch_relationships import Migration032
|
|
37
|
+
from .m033_deduplicate_relationship_vertices import Migration033
|
|
36
38
|
|
|
37
39
|
if TYPE_CHECKING:
|
|
38
40
|
from infrahub.core.root import Root
|
|
@@ -71,6 +73,8 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
|
|
|
71
73
|
Migration029,
|
|
72
74
|
Migration030,
|
|
73
75
|
Migration031,
|
|
76
|
+
Migration032,
|
|
77
|
+
Migration033,
|
|
74
78
|
]
|
|
75
79
|
|
|
76
80
|
|
|
@@ -85,3 +89,23 @@ async def get_graph_migrations(
|
|
|
85
89
|
applicable_migrations.append(migration)
|
|
86
90
|
|
|
87
91
|
return applicable_migrations
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_migration_by_number(
|
|
95
|
+
migration_number: int | str,
|
|
96
|
+
) -> GraphMigration | InternalSchemaMigration | ArbitraryMigration:
|
|
97
|
+
# Convert to string and pad with zeros if needed
|
|
98
|
+
try:
|
|
99
|
+
num = int(migration_number)
|
|
100
|
+
migration_str = f"{num:03d}"
|
|
101
|
+
except (ValueError, TypeError) as exc:
|
|
102
|
+
raise ValueError(f"Invalid migration number: {migration_number}") from exc
|
|
103
|
+
|
|
104
|
+
migration_name = f"Migration{migration_str}"
|
|
105
|
+
|
|
106
|
+
# Find the migration in the MIGRATIONS list
|
|
107
|
+
for migration_class in MIGRATIONS:
|
|
108
|
+
if migration_class.__name__ == migration_name:
|
|
109
|
+
return migration_class.init()
|
|
110
|
+
|
|
111
|
+
raise ValueError(f"Migration {migration_number} not found")
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Sequence
|
|
4
4
|
|
|
5
5
|
from infrahub.core.branch import Branch
|
|
6
|
+
from infrahub.core.branch.enums import BranchStatus
|
|
6
7
|
from infrahub.core.constants import GLOBAL_BRANCH_NAME, BranchSupportType, InfrahubKind
|
|
7
8
|
from infrahub.core.migrations.shared import MigrationResult
|
|
8
9
|
from infrahub.core.query import Query, QueryType
|
|
@@ -20,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
20
21
|
|
|
21
22
|
global_branch = Branch(
|
|
22
23
|
name=GLOBAL_BRANCH_NAME,
|
|
23
|
-
status=
|
|
24
|
+
status=BranchStatus.OPEN,
|
|
24
25
|
description="Global Branch",
|
|
25
26
|
hierarchy_level=1,
|
|
26
27
|
is_global=True,
|
|
@@ -29,7 +30,7 @@ global_branch = Branch(
|
|
|
29
30
|
|
|
30
31
|
default_branch = Branch(
|
|
31
32
|
name="main",
|
|
32
|
-
status=
|
|
33
|
+
status=BranchStatus.OPEN,
|
|
33
34
|
description="Default Branch",
|
|
34
35
|
hierarchy_level=1,
|
|
35
36
|
is_global=False,
|
|
@@ -105,7 +106,7 @@ class Migration012AddLabelData(NodeDuplicateQuery):
|
|
|
105
106
|
|
|
106
107
|
branch = Branch(
|
|
107
108
|
name=GLOBAL_BRANCH_NAME,
|
|
108
|
-
status=
|
|
109
|
+
status=BranchStatus.OPEN,
|
|
109
110
|
description="Global Branch",
|
|
110
111
|
hierarchy_level=1,
|
|
111
112
|
is_global=True,
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Sequence
|
|
4
4
|
|
|
5
5
|
from infrahub.core.branch import Branch
|
|
6
|
+
from infrahub.core.branch.enums import BranchStatus
|
|
6
7
|
from infrahub.core.constants import (
|
|
7
8
|
GLOBAL_BRANCH_NAME,
|
|
8
9
|
BranchSupportType,
|
|
@@ -23,7 +24,7 @@ if TYPE_CHECKING:
|
|
|
23
24
|
|
|
24
25
|
default_branch = Branch(
|
|
25
26
|
name="main",
|
|
26
|
-
status=
|
|
27
|
+
status=BranchStatus.OPEN,
|
|
27
28
|
description="Default Branch",
|
|
28
29
|
hierarchy_level=1,
|
|
29
30
|
is_global=False,
|
|
@@ -42,7 +43,7 @@ class Migration013ConvertCoreRepositoryWithCred(Query):
|
|
|
42
43
|
|
|
43
44
|
global_branch = Branch(
|
|
44
45
|
name=GLOBAL_BRANCH_NAME,
|
|
45
|
-
status=
|
|
46
|
+
status=BranchStatus.OPEN,
|
|
46
47
|
description="Global Branch",
|
|
47
48
|
hierarchy_level=1,
|
|
48
49
|
is_global=True,
|
|
@@ -176,7 +177,7 @@ class Migration013ConvertCoreRepositoryWithoutCred(Query):
|
|
|
176
177
|
|
|
177
178
|
global_branch = Branch(
|
|
178
179
|
name=GLOBAL_BRANCH_NAME,
|
|
179
|
-
status=
|
|
180
|
+
status=BranchStatus.OPEN,
|
|
180
181
|
description="Global Branch",
|
|
181
182
|
hierarchy_level=1,
|
|
182
183
|
is_global=True,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from infrahub.core.migrations.shared import MigrationResult
|
|
6
|
+
from infrahub.core.query import Query, QueryType
|
|
7
|
+
from infrahub.core.query.branch import DeleteBranchRelationshipsQuery
|
|
8
|
+
from infrahub.log import get_logger
|
|
9
|
+
|
|
10
|
+
from ..shared import ArbitraryMigration
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from infrahub.database import InfrahubDatabase
|
|
14
|
+
|
|
15
|
+
log = get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeletedBranchCleanupQuery(Query):
|
|
19
|
+
"""
|
|
20
|
+
Find all unique edge branch names for which there is no Branch object
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name = "deleted_branch_cleanup"
|
|
24
|
+
type = QueryType.WRITE
|
|
25
|
+
insert_return = False
|
|
26
|
+
|
|
27
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
28
|
+
query = """
|
|
29
|
+
MATCH (b:Branch)
|
|
30
|
+
WITH collect(DISTINCT b.name) AS branch_names
|
|
31
|
+
MATCH ()-[e]->()
|
|
32
|
+
WHERE e.branch IS NOT NULL
|
|
33
|
+
AND NOT e.branch IN branch_names
|
|
34
|
+
RETURN DISTINCT (e.branch) AS branch_name
|
|
35
|
+
"""
|
|
36
|
+
self.add_to_query(query)
|
|
37
|
+
self.return_labels = ["branch_name"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DeleteOrphanRelationshipsQuery(Query):
|
|
41
|
+
"""
|
|
42
|
+
Find all Relationship vertices that link to fewer than 2 Node vertices and delete them
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
name = "delete_orphan_relationships"
|
|
46
|
+
type = QueryType.WRITE
|
|
47
|
+
insert_return = False
|
|
48
|
+
|
|
49
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
50
|
+
query = """
|
|
51
|
+
MATCH (r:Relationship)-[:IS_RELATED]-(n:Node)
|
|
52
|
+
WITH DISTINCT r, n
|
|
53
|
+
WITH r, count(*) AS node_count
|
|
54
|
+
WHERE node_count < 2
|
|
55
|
+
DETACH DELETE r
|
|
56
|
+
"""
|
|
57
|
+
self.add_to_query(query)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Migration032(ArbitraryMigration):
|
|
61
|
+
"""
|
|
62
|
+
Delete edges for branches that were not completely deleted
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name: str = "032_cleanup_deleted_branches"
|
|
66
|
+
minimum_version: int = 31
|
|
67
|
+
|
|
68
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
69
|
+
return MigrationResult()
|
|
70
|
+
|
|
71
|
+
async def execute(self, db: InfrahubDatabase) -> MigrationResult:
|
|
72
|
+
migration_result = MigrationResult()
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
log.info("Get partially deleted branch names...")
|
|
76
|
+
orphaned_branches_query = await DeletedBranchCleanupQuery.init(db=db)
|
|
77
|
+
await orphaned_branches_query.execute(db=db)
|
|
78
|
+
|
|
79
|
+
orphaned_branch_names = []
|
|
80
|
+
for result in orphaned_branches_query.get_results():
|
|
81
|
+
branch_name = result.get_as_type("branch_name", str)
|
|
82
|
+
orphaned_branch_names.append(branch_name)
|
|
83
|
+
|
|
84
|
+
if not orphaned_branch_names:
|
|
85
|
+
log.info("No partially deleted branches found. All done.")
|
|
86
|
+
return migration_result
|
|
87
|
+
|
|
88
|
+
log.info(f"Found {len(orphaned_branch_names)} orphaned branch names: {orphaned_branch_names}")
|
|
89
|
+
|
|
90
|
+
for branch_name in orphaned_branch_names:
|
|
91
|
+
log.info(f"Cleaning up branch '{branch_name}'...")
|
|
92
|
+
delete_query = await DeleteBranchRelationshipsQuery.init(db=db, branch_name=branch_name)
|
|
93
|
+
await delete_query.execute(db=db)
|
|
94
|
+
log.info(f"Branch '{branch_name}' cleaned up.")
|
|
95
|
+
|
|
96
|
+
log.info("Deleting orphaned relationships...")
|
|
97
|
+
delete_relationships_query = await DeleteOrphanRelationshipsQuery.init(db=db)
|
|
98
|
+
await delete_relationships_query.execute(db=db)
|
|
99
|
+
log.info("Orphaned relationships deleted.")
|
|
100
|
+
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
migration_result.errors.append(str(exc))
|
|
103
|
+
log.exception("Error during branch cleanup")
|
|
104
|
+
|
|
105
|
+
return migration_result
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
from infrahub.log import get_logger
|
|
7
|
+
|
|
8
|
+
from ...query import Query, QueryType
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from infrahub.database import InfrahubDatabase
|
|
12
|
+
|
|
13
|
+
log = get_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeduplicateRelationshipVerticesQuery(Query):
|
|
17
|
+
"""
|
|
18
|
+
For each group of duplicate Relationships with the same UUID, delete any Relationship that meets the following criteria:
|
|
19
|
+
- is linked to a deleted node (only if the delete time is before the Relationship's from time)
|
|
20
|
+
- is linked to a node on an incorrect branch (ie Relationship added on main, but Node is on a branch)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name = "deduplicate_relationship_vertices"
|
|
24
|
+
type = QueryType.WRITE
|
|
25
|
+
insert_return = False
|
|
26
|
+
|
|
27
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
28
|
+
query = """
|
|
29
|
+
MATCH (root:Root)
|
|
30
|
+
WITH root.default_branch AS default_branch_name
|
|
31
|
+
// ------------
|
|
32
|
+
// Find all Relationship vertices with duplicate UUIDs
|
|
33
|
+
// ------------
|
|
34
|
+
MATCH (r:Relationship)
|
|
35
|
+
WITH r.uuid AS r_uuid, default_branch_name, count(*) AS num_dups
|
|
36
|
+
WHERE num_dups > 1
|
|
37
|
+
WITH DISTINCT r_uuid, default_branch_name
|
|
38
|
+
// ------------
|
|
39
|
+
// get the branched_from time for each relationship edge and node
|
|
40
|
+
// ------------
|
|
41
|
+
MATCH (rel:Relationship {uuid: r_uuid})
|
|
42
|
+
CALL (rel) {
|
|
43
|
+
MATCH (rel)-[is_rel_e:IS_RELATED {status: "active"}]-(n:Node)
|
|
44
|
+
MATCH (rel_branch:Branch {name: is_rel_e.branch})
|
|
45
|
+
RETURN is_rel_e, rel_branch.branched_from AS rel_branched_from, n
|
|
46
|
+
}
|
|
47
|
+
// ------------
|
|
48
|
+
// for each IS_RELATED edge of the relationship, check the IS_PART_OF edges of the Node vertex
|
|
49
|
+
// to determine if this side of the relationship is legal
|
|
50
|
+
// ------------
|
|
51
|
+
CALL (n, is_rel_e, rel_branched_from, default_branch_name) {
|
|
52
|
+
OPTIONAL MATCH (n)-[is_part_of_e:IS_PART_OF {status: "active"}]->(:Root)
|
|
53
|
+
WHERE (
|
|
54
|
+
// the Node's create time must precede the Relationship's create time
|
|
55
|
+
is_part_of_e.from <= is_rel_e.from AND (is_part_of_e.to >= is_rel_e.from OR is_part_of_e.to IS NULL)
|
|
56
|
+
// the Node must have been created on a branch of equal or lesser depth than the Relationship
|
|
57
|
+
AND is_part_of_e.branch_level <= is_rel_e.branch_level
|
|
58
|
+
// if the Node and Relationships were created on branch_level = 2, then they must be on the same branch
|
|
59
|
+
AND (
|
|
60
|
+
is_part_of_e.branch_level = 1
|
|
61
|
+
OR is_part_of_e.branch = is_rel_e.branch
|
|
62
|
+
)
|
|
63
|
+
// if the Node was created on the default branch, and the Relationship was created on a branch,
|
|
64
|
+
// then the Node must have been created after the branched_from time of the Relationship's branch
|
|
65
|
+
AND (
|
|
66
|
+
is_part_of_e.branch <> default_branch_name
|
|
67
|
+
OR is_rel_e.branch_level = 1
|
|
68
|
+
OR is_part_of_e.from <= rel_branched_from
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
WITH is_part_of_e IS NOT NULL AS is_legal
|
|
72
|
+
ORDER BY is_legal DESC
|
|
73
|
+
RETURN is_legal
|
|
74
|
+
LIMIT 1
|
|
75
|
+
}
|
|
76
|
+
WITH rel, is_legal
|
|
77
|
+
ORDER BY rel, is_legal ASC
|
|
78
|
+
WITH rel, head(collect(is_legal)) AS is_legal
|
|
79
|
+
WHERE is_legal = false
|
|
80
|
+
DETACH DELETE rel
|
|
81
|
+
"""
|
|
82
|
+
self.add_to_query(query)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Migration033(GraphMigration):
|
|
86
|
+
"""
|
|
87
|
+
Identifies duplicate Relationship vertices that have the same UUID property. Deletes any duplicates that
|
|
88
|
+
are linked to deleted nodes or nodes on in incorrect branch.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name: str = "033_deduplicate_relationship_vertices"
|
|
92
|
+
minimum_version: int = 31
|
|
93
|
+
queries: Sequence[type[Query]] = [DeduplicateRelationshipVerticesQuery]
|
|
94
|
+
|
|
95
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
96
|
+
result = MigrationResult()
|
|
97
|
+
return result
|
infrahub/core/node/__init__.py
CHANGED
|
@@ -82,6 +82,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
82
82
|
def get_schema(self) -> NonGenericSchemaTypes:
|
|
83
83
|
return self._schema
|
|
84
84
|
|
|
85
|
+
def get_branch(self) -> Branch:
|
|
86
|
+
return self._branch
|
|
87
|
+
|
|
85
88
|
def get_kind(self) -> str:
|
|
86
89
|
"""Return the main Kind of the Object."""
|
|
87
90
|
return self._schema.kind
|