infrahub-server 1.4.10__py3-none-any.whl → 1.5.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.
- infrahub/actions/tasks.py +200 -16
- infrahub/api/artifact.py +3 -0
- infrahub/api/query.py +2 -0
- infrahub/api/schema.py +3 -0
- infrahub/auth.py +5 -5
- infrahub/cli/db.py +2 -2
- infrahub/config.py +7 -2
- infrahub/core/attribute.py +22 -19
- infrahub/core/branch/models.py +2 -2
- infrahub/core/branch/needs_rebase_status.py +11 -0
- infrahub/core/branch/tasks.py +2 -2
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/convert_object_type/object_conversion.py +201 -0
- infrahub/core/convert_object_type/repository_conversion.py +89 -0
- infrahub/core/convert_object_type/schema_mapping.py +27 -3
- infrahub/core/diff/query/artifact.py +1 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +2 -2
- infrahub/core/manager.py +3 -81
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
- infrahub/core/node/__init__.py +23 -2
- infrahub/core/node/create.py +67 -35
- infrahub/core/node/lock_utils.py +98 -0
- infrahub/core/property.py +11 -0
- infrahub/core/protocols.py +1 -0
- infrahub/core/query/attribute.py +27 -15
- infrahub/core/query/node.py +47 -184
- infrahub/core/query/relationship.py +43 -26
- infrahub/core/query/subquery.py +0 -8
- infrahub/core/relationship/model.py +59 -19
- infrahub/core/schema/attribute_schema.py +0 -2
- infrahub/core/schema/definitions/core/repository.py +7 -0
- infrahub/core/schema/relationship_schema.py +0 -1
- infrahub/core/schema/schema_branch.py +3 -2
- infrahub/generators/models.py +31 -12
- infrahub/generators/tasks.py +3 -1
- infrahub/git/base.py +38 -1
- infrahub/graphql/api/dependencies.py +2 -4
- infrahub/graphql/api/endpoints.py +2 -2
- infrahub/graphql/app.py +2 -4
- infrahub/graphql/initialization.py +2 -3
- infrahub/graphql/manager.py +212 -137
- infrahub/graphql/middleware.py +12 -0
- infrahub/graphql/mutations/branch.py +11 -0
- infrahub/graphql/mutations/computed_attribute.py +110 -3
- infrahub/graphql/mutations/convert_object_type.py +34 -13
- infrahub/graphql/mutations/ipam.py +21 -8
- infrahub/graphql/mutations/main.py +37 -153
- infrahub/graphql/mutations/profile.py +195 -0
- infrahub/graphql/mutations/proposed_change.py +2 -1
- infrahub/graphql/mutations/repository.py +22 -83
- infrahub/graphql/mutations/webhook.py +1 -1
- infrahub/graphql/registry.py +173 -0
- infrahub/graphql/schema.py +4 -1
- infrahub/lock.py +52 -26
- infrahub/locks/__init__.py +0 -0
- infrahub/locks/tasks.py +37 -0
- infrahub/patch/plan_writer.py +2 -2
- infrahub/profiles/__init__.py +0 -0
- infrahub/profiles/node_applier.py +101 -0
- infrahub/profiles/queries/__init__.py +0 -0
- infrahub/profiles/queries/get_profile_data.py +99 -0
- infrahub/profiles/tasks.py +63 -0
- infrahub/repositories/__init__.py +0 -0
- infrahub/repositories/create_repository.py +113 -0
- infrahub/tasks/registry.py +6 -4
- infrahub/webhook/models.py +1 -1
- infrahub/workflows/catalogue.py +38 -3
- infrahub/workflows/models.py +17 -2
- infrahub_sdk/branch.py +5 -8
- infrahub_sdk/client.py +364 -84
- infrahub_sdk/convert_object_type.py +61 -0
- infrahub_sdk/ctl/check.py +2 -3
- infrahub_sdk/ctl/cli_commands.py +16 -12
- infrahub_sdk/ctl/config.py +8 -2
- infrahub_sdk/ctl/generator.py +2 -3
- infrahub_sdk/ctl/repository.py +39 -1
- infrahub_sdk/ctl/schema.py +12 -1
- infrahub_sdk/ctl/utils.py +4 -0
- infrahub_sdk/ctl/validate.py +5 -3
- infrahub_sdk/diff.py +4 -5
- infrahub_sdk/exceptions.py +2 -0
- infrahub_sdk/graphql.py +7 -2
- infrahub_sdk/node/attribute.py +2 -0
- infrahub_sdk/node/node.py +28 -20
- infrahub_sdk/playback.py +1 -2
- infrahub_sdk/protocols.py +40 -6
- infrahub_sdk/pytest_plugin/plugin.py +7 -4
- infrahub_sdk/pytest_plugin/utils.py +40 -0
- infrahub_sdk/repository.py +1 -2
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/spec/object.py +43 -4
- infrahub_sdk/spec/range_expansion.py +118 -0
- infrahub_sdk/timestamp.py +18 -6
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/METADATA +6 -9
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/RECORD +102 -84
- infrahub_testcontainers/models.py +2 -2
- infrahub_testcontainers/performance_test.py +4 -4
- infrahub/core/convert_object_type/conversion.py +0 -134
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.progress import Progress
|
|
8
|
+
|
|
9
|
+
from infrahub.core.branch.models import Branch
|
|
10
|
+
from infrahub.core.initialization import initialization
|
|
11
|
+
from infrahub.core.manager import NodeManager
|
|
12
|
+
from infrahub.core.migrations.shared import MigrationResult
|
|
13
|
+
from infrahub.core.query import Query, QueryType
|
|
14
|
+
from infrahub.core.timestamp import Timestamp
|
|
15
|
+
from infrahub.lock import initialize_lock
|
|
16
|
+
from infrahub.log import get_logger
|
|
17
|
+
from infrahub.profiles.node_applier import NodeProfilesApplier
|
|
18
|
+
|
|
19
|
+
from ..shared import ArbitraryMigration
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from infrahub.core.node import Node
|
|
23
|
+
from infrahub.database import InfrahubDatabase
|
|
24
|
+
|
|
25
|
+
log = get_logger()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GetProfilesByBranchQuery(Query):
|
|
29
|
+
"""
|
|
30
|
+
Get CoreProfile UUIDs by which branches they have attribute updates on
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name = "get_profiles_by_branch"
|
|
34
|
+
type = QueryType.READ
|
|
35
|
+
insert_return = False
|
|
36
|
+
|
|
37
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
38
|
+
query = """
|
|
39
|
+
MATCH (profile:CoreProfile)-[:HAS_ATTRIBUTE]->(attr:Attribute)-[e:HAS_VALUE]->(:AttributeValue)
|
|
40
|
+
WITH DISTINCT profile.uuid AS profile_uuid, e.branch AS branch
|
|
41
|
+
RETURN profile_uuid, collect(branch) AS branches
|
|
42
|
+
"""
|
|
43
|
+
self.add_to_query(query)
|
|
44
|
+
self.return_labels = ["profile_uuid", "branches"]
|
|
45
|
+
|
|
46
|
+
def get_profile_ids_by_branch(self) -> dict[str, set[str]]:
|
|
47
|
+
"""Get dictionary of branch names to set of updated profile UUIDs"""
|
|
48
|
+
profiles_by_branch = defaultdict(set)
|
|
49
|
+
for result in self.get_results():
|
|
50
|
+
profile_uuid = result.get_as_type("profile_uuid", str)
|
|
51
|
+
branches = result.get_as_type("branches", list[str])
|
|
52
|
+
for branch in branches:
|
|
53
|
+
profiles_by_branch[branch].add(profile_uuid)
|
|
54
|
+
return profiles_by_branch
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GetNodesWithProfileUpdatesByBranchQuery(Query):
|
|
58
|
+
"""
|
|
59
|
+
Get Node UUIDs by which branches they have updated profiles on
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
name = "get_nodes_with_profile_updates_by_branch"
|
|
63
|
+
type = QueryType.READ
|
|
64
|
+
insert_return = False
|
|
65
|
+
|
|
66
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
67
|
+
query = """
|
|
68
|
+
MATCH (node:Node)-[e1:IS_RELATED]->(:Relationship {name: "node__profile"})
|
|
69
|
+
WHERE NOT node:CoreProfile
|
|
70
|
+
WITH DISTINCT node.uuid AS node_uuid, e1.branch AS branch
|
|
71
|
+
RETURN node_uuid, collect(branch) AS branches
|
|
72
|
+
"""
|
|
73
|
+
self.add_to_query(query)
|
|
74
|
+
self.return_labels = ["node_uuid", "branches"]
|
|
75
|
+
|
|
76
|
+
def get_node_ids_by_branch(self) -> dict[str, set[str]]:
|
|
77
|
+
"""Get dictionary of branch names to set of updated node UUIDs"""
|
|
78
|
+
nodes_by_branch = defaultdict(set)
|
|
79
|
+
for result in self.get_results():
|
|
80
|
+
node_uuid = result.get_as_type("node_uuid", str)
|
|
81
|
+
branches = result.get_as_type("branches", list[str])
|
|
82
|
+
for branch in branches:
|
|
83
|
+
nodes_by_branch[branch].add(node_uuid)
|
|
84
|
+
return nodes_by_branch
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Migration040(ArbitraryMigration):
|
|
88
|
+
"""
|
|
89
|
+
Save profile attribute values on each node using the profile in the database
|
|
90
|
+
For any profile that has updates on a given branch (including default branch)
|
|
91
|
+
- run NodeProfilesApplier.apply_profiles on each node related to the profile on that branch
|
|
92
|
+
For any node that has an updated relationship to a profile on a given branch
|
|
93
|
+
- run NodeProfilesApplier.apply_profiles on the node on that branch
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
name: str = "040_profile_attrs_in_db"
|
|
97
|
+
minimum_version: int = 39
|
|
98
|
+
|
|
99
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
100
|
+
super().__init__(*args, **kwargs)
|
|
101
|
+
self._appliers_by_branch: dict[str, NodeProfilesApplier] = {}
|
|
102
|
+
|
|
103
|
+
async def _get_profile_applier(self, db: InfrahubDatabase, branch_name: str) -> NodeProfilesApplier:
|
|
104
|
+
if branch_name not in self._appliers_by_branch:
|
|
105
|
+
branch = await Branch.get_by_name(db=db, name=branch_name)
|
|
106
|
+
self._appliers_by_branch[branch_name] = NodeProfilesApplier(db=db, branch=branch)
|
|
107
|
+
return self._appliers_by_branch[branch_name]
|
|
108
|
+
|
|
109
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
110
|
+
return MigrationResult()
|
|
111
|
+
|
|
112
|
+
async def execute(self, db: InfrahubDatabase) -> MigrationResult:
|
|
113
|
+
console = Console()
|
|
114
|
+
result = MigrationResult()
|
|
115
|
+
# load schemas from database into registry
|
|
116
|
+
initialize_lock()
|
|
117
|
+
await initialization(db=db)
|
|
118
|
+
|
|
119
|
+
console.print("Gathering profiles for each branch...", end="")
|
|
120
|
+
get_profiles_by_branch_query = await GetProfilesByBranchQuery.init(db=db)
|
|
121
|
+
await get_profiles_by_branch_query.execute(db=db)
|
|
122
|
+
profiles_ids_by_branch = get_profiles_by_branch_query.get_profile_ids_by_branch()
|
|
123
|
+
|
|
124
|
+
profiles_by_branch: dict[str, list[Node]] = {}
|
|
125
|
+
for branch_name, profile_ids in profiles_ids_by_branch.items():
|
|
126
|
+
profiles_map = await NodeManager.get_many(db=db, branch=branch_name, ids=list(profile_ids))
|
|
127
|
+
profiles_by_branch[branch_name] = list(profiles_map.values())
|
|
128
|
+
console.print("done")
|
|
129
|
+
|
|
130
|
+
node_ids_to_update_by_branch: dict[str, set[str]] = defaultdict(set)
|
|
131
|
+
total_size = sum(len(profiles) for profiles in profiles_by_branch.values())
|
|
132
|
+
with Progress() as progress:
|
|
133
|
+
gather_nodes_task = progress.add_task(
|
|
134
|
+
"Gathering affected objects for each profile on each branch...", total=total_size
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for branch_name, profiles in profiles_by_branch.items():
|
|
138
|
+
for profile in profiles:
|
|
139
|
+
node_relationship_manager = profile.get_relationship("related_nodes")
|
|
140
|
+
node_peers = await node_relationship_manager.get_db_peers(db=db)
|
|
141
|
+
node_ids_to_update_by_branch[branch_name].update({str(peer.peer_id) for peer in node_peers})
|
|
142
|
+
progress.update(gather_nodes_task, advance=1)
|
|
143
|
+
|
|
144
|
+
console.print("Identifying nodes with profile updates by branch...", end="")
|
|
145
|
+
get_nodes_with_profile_updates_by_branch_query = await GetNodesWithProfileUpdatesByBranchQuery.init(db=db)
|
|
146
|
+
await get_nodes_with_profile_updates_by_branch_query.execute(db=db)
|
|
147
|
+
nodes_ids_by_branch = get_nodes_with_profile_updates_by_branch_query.get_node_ids_by_branch()
|
|
148
|
+
for branch_name, node_ids in nodes_ids_by_branch.items():
|
|
149
|
+
node_ids_to_update_by_branch[branch_name].update(node_ids)
|
|
150
|
+
console.print("done")
|
|
151
|
+
|
|
152
|
+
right_now = Timestamp()
|
|
153
|
+
total_size = sum(len(node_ids) for node_ids in node_ids_to_update_by_branch.values())
|
|
154
|
+
with Progress() as progress:
|
|
155
|
+
apply_task = progress.add_task("Applying profiles to nodes...", total=total_size)
|
|
156
|
+
for branch_name, node_ids in node_ids_to_update_by_branch.items():
|
|
157
|
+
applier = await self._get_profile_applier(db=db, branch_name=branch_name)
|
|
158
|
+
for node_id in node_ids:
|
|
159
|
+
node = await NodeManager.get_one(db=db, branch=branch_name, id=node_id, at=right_now)
|
|
160
|
+
if node:
|
|
161
|
+
updated_field_names = await applier.apply_profiles(node=node)
|
|
162
|
+
if updated_field_names:
|
|
163
|
+
await node.save(db=db, fields=updated_field_names, at=right_now)
|
|
164
|
+
progress.update(apply_task, advance=1)
|
|
165
|
+
|
|
166
|
+
return result
|
infrahub/core/node/__init__.py
CHANGED
|
@@ -42,6 +42,7 @@ from infrahub.types import ATTRIBUTE_TYPES
|
|
|
42
42
|
from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
|
|
43
43
|
from ...graphql.models import OrderModel
|
|
44
44
|
from ...log import get_logger
|
|
45
|
+
from ..attribute import BaseAttribute
|
|
45
46
|
from ..query.relationship import RelationshipDeleteAllQuery
|
|
46
47
|
from ..relationship import RelationshipManager
|
|
47
48
|
from ..utils import update_relationships_to
|
|
@@ -53,8 +54,6 @@ if TYPE_CHECKING:
|
|
|
53
54
|
from infrahub.core.branch import Branch
|
|
54
55
|
from infrahub.database import InfrahubDatabase
|
|
55
56
|
|
|
56
|
-
from ..attribute import BaseAttribute
|
|
57
|
-
|
|
58
57
|
SchemaProtocol = TypeVar("SchemaProtocol")
|
|
59
58
|
|
|
60
59
|
# ---------------------------------------------------------------------------------------
|
|
@@ -100,6 +99,28 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
100
99
|
def get_updated_at(self) -> Timestamp | None:
|
|
101
100
|
return self._updated_at
|
|
102
101
|
|
|
102
|
+
def get_attribute(self, name: str) -> BaseAttribute:
|
|
103
|
+
attribute = getattr(self, name)
|
|
104
|
+
if not isinstance(attribute, BaseAttribute):
|
|
105
|
+
raise ValueError(f"{name} is not an attribute of {self.get_kind()}")
|
|
106
|
+
return attribute
|
|
107
|
+
|
|
108
|
+
def get_relationship(self, name: str) -> RelationshipManager:
|
|
109
|
+
relationship = getattr(self, name)
|
|
110
|
+
if not isinstance(relationship, RelationshipManager):
|
|
111
|
+
raise ValueError(f"{name} is not a relationship of {self.get_kind()}")
|
|
112
|
+
return relationship
|
|
113
|
+
|
|
114
|
+
def uses_profiles(self) -> bool:
|
|
115
|
+
for attr_name in self.get_schema().attribute_names:
|
|
116
|
+
try:
|
|
117
|
+
node_attr = self.get_attribute(attr_name)
|
|
118
|
+
except ValueError:
|
|
119
|
+
continue
|
|
120
|
+
if node_attr and node_attr.is_from_profile:
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
103
124
|
async def get_hfid(self, db: InfrahubDatabase, include_kind: bool = False) -> list[str] | None:
|
|
104
125
|
"""Return the Human friendly id of the node."""
|
|
105
126
|
if not self._schema.human_friendly_id:
|
infrahub/core/node/create.py
CHANGED
|
@@ -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_kind_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
|
|
|
@@ -76,6 +81,7 @@ async def handle_template_relationships(
|
|
|
76
81
|
template: CoreObjectTemplate,
|
|
77
82
|
fields: list,
|
|
78
83
|
constraint_runner: NodeConstraintRunner | None = None,
|
|
84
|
+
at: Timestamp | None = None,
|
|
79
85
|
) -> None:
|
|
80
86
|
if constraint_runner is None:
|
|
81
87
|
component_registry = get_component_registry()
|
|
@@ -103,7 +109,7 @@ async def handle_template_relationships(
|
|
|
103
109
|
current_template=template,
|
|
104
110
|
)
|
|
105
111
|
|
|
106
|
-
obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch)
|
|
112
|
+
obj_peer = await Node.init(schema=obj_peer_schema, db=db, branch=branch, at=at)
|
|
107
113
|
await obj_peer.new(db=db, **obj_peer_data)
|
|
108
114
|
await constraint_runner.check(node=obj_peer, field_filters=list(obj_peer_data))
|
|
109
115
|
await obj_peer.save(db=db)
|
|
@@ -115,6 +121,7 @@ async def handle_template_relationships(
|
|
|
115
121
|
obj=obj_peer,
|
|
116
122
|
template=template_relationship_peer,
|
|
117
123
|
fields=fields,
|
|
124
|
+
at=at,
|
|
118
125
|
)
|
|
119
126
|
|
|
120
127
|
|
|
@@ -125,43 +132,20 @@ async def get_profile_ids(db: InfrahubDatabase, obj: Node) -> set[str]:
|
|
|
125
132
|
return {pr.peer_id for pr in profile_rels}
|
|
126
133
|
|
|
127
134
|
|
|
128
|
-
async def refresh_for_profile_update(
|
|
129
|
-
db: InfrahubDatabase,
|
|
130
|
-
branch: Branch,
|
|
131
|
-
obj: Node,
|
|
132
|
-
schema: NonGenericSchemaTypes,
|
|
133
|
-
previous_profile_ids: set[str] | None = None,
|
|
134
|
-
) -> Node:
|
|
135
|
-
if not hasattr(obj, "profiles"):
|
|
136
|
-
return obj
|
|
137
|
-
current_profile_ids = await get_profile_ids(db=db, obj=obj)
|
|
138
|
-
if previous_profile_ids is None or previous_profile_ids != current_profile_ids:
|
|
139
|
-
refreshed_node = await NodeManager.get_one_by_id_or_default_filter(
|
|
140
|
-
db=db,
|
|
141
|
-
kind=schema.kind,
|
|
142
|
-
id=obj.get_id(),
|
|
143
|
-
branch=branch,
|
|
144
|
-
include_owner=True,
|
|
145
|
-
include_source=True,
|
|
146
|
-
)
|
|
147
|
-
refreshed_node._node_changelog = obj.node_changelog
|
|
148
|
-
return refreshed_node
|
|
149
|
-
return obj
|
|
150
|
-
|
|
151
|
-
|
|
152
135
|
async def _do_create_node(
|
|
153
136
|
node_class: type[Node],
|
|
137
|
+
node_constraint_runner: NodeConstraintRunner,
|
|
154
138
|
db: InfrahubDatabase,
|
|
155
|
-
data: dict,
|
|
156
139
|
schema: NonGenericSchemaTypes,
|
|
157
|
-
fields_to_validate: list,
|
|
158
140
|
branch: Branch,
|
|
159
|
-
|
|
141
|
+
fields_to_validate: list[str],
|
|
142
|
+
data: dict[str, Any],
|
|
143
|
+
at: Timestamp | None = None,
|
|
160
144
|
) -> Node:
|
|
161
145
|
obj = await node_class.init(db=db, schema=schema, branch=branch)
|
|
162
146
|
await obj.new(db=db, **data)
|
|
163
147
|
await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
|
|
164
|
-
await obj.save(db=db)
|
|
148
|
+
await obj.save(db=db, at=at)
|
|
165
149
|
|
|
166
150
|
object_template = await obj.get_object_template(db=db)
|
|
167
151
|
if object_template:
|
|
@@ -171,18 +155,62 @@ async def _do_create_node(
|
|
|
171
155
|
template=object_template,
|
|
172
156
|
obj=obj,
|
|
173
157
|
fields=fields_to_validate,
|
|
158
|
+
at=at,
|
|
174
159
|
)
|
|
175
160
|
return obj
|
|
176
161
|
|
|
177
162
|
|
|
163
|
+
async def _do_create_node_with_lock(
|
|
164
|
+
node_class: type[Node],
|
|
165
|
+
node_constraint_runner: NodeConstraintRunner,
|
|
166
|
+
db: InfrahubDatabase,
|
|
167
|
+
schema: NonGenericSchemaTypes,
|
|
168
|
+
branch: Branch,
|
|
169
|
+
fields_to_validate: list[str],
|
|
170
|
+
data: dict[str, Any],
|
|
171
|
+
at: Timestamp | None = None,
|
|
172
|
+
) -> Node:
|
|
173
|
+
schema_branch = registry.schema.get_schema_branch(name=branch.name)
|
|
174
|
+
lock_names = get_kind_lock_names_on_object_mutation(
|
|
175
|
+
kind=schema.kind, branch=branch, schema_branch=schema_branch, data=dict(data)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if lock_names:
|
|
179
|
+
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
|
|
180
|
+
return await _do_create_node(
|
|
181
|
+
node_class=node_class,
|
|
182
|
+
node_constraint_runner=node_constraint_runner,
|
|
183
|
+
db=db,
|
|
184
|
+
schema=schema,
|
|
185
|
+
branch=branch,
|
|
186
|
+
fields_to_validate=fields_to_validate,
|
|
187
|
+
data=data,
|
|
188
|
+
at=at,
|
|
189
|
+
)
|
|
190
|
+
return await _do_create_node(
|
|
191
|
+
node_class=node_class,
|
|
192
|
+
node_constraint_runner=node_constraint_runner,
|
|
193
|
+
db=db,
|
|
194
|
+
schema=schema,
|
|
195
|
+
branch=branch,
|
|
196
|
+
fields_to_validate=fields_to_validate,
|
|
197
|
+
data=data,
|
|
198
|
+
at=at,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
178
202
|
async def create_node(
|
|
179
|
-
data: dict,
|
|
203
|
+
data: dict[str, Any],
|
|
180
204
|
db: InfrahubDatabase,
|
|
181
205
|
branch: Branch,
|
|
182
|
-
schema:
|
|
206
|
+
schema: MainSchemaTypes,
|
|
207
|
+
at: Timestamp | None = None,
|
|
183
208
|
) -> Node:
|
|
184
209
|
"""Create a node in the database if constraint checks succeed."""
|
|
185
210
|
|
|
211
|
+
if isinstance(schema, GenericSchema):
|
|
212
|
+
raise ValueError(f"Node of generic schema `{schema.name=}` can not be instantiated.")
|
|
213
|
+
|
|
186
214
|
component_registry = get_component_registry()
|
|
187
215
|
node_constraint_runner = await component_registry.get_component(
|
|
188
216
|
NodeConstraintRunner, db=db.start_session() if not db.is_transaction else db, branch=branch
|
|
@@ -193,7 +221,7 @@ async def create_node(
|
|
|
193
221
|
|
|
194
222
|
fields_to_validate = list(data)
|
|
195
223
|
if db.is_transaction:
|
|
196
|
-
obj = await
|
|
224
|
+
obj = await _do_create_node_with_lock(
|
|
197
225
|
node_class=node_class,
|
|
198
226
|
node_constraint_runner=node_constraint_runner,
|
|
199
227
|
db=db,
|
|
@@ -201,10 +229,11 @@ async def create_node(
|
|
|
201
229
|
branch=branch,
|
|
202
230
|
fields_to_validate=fields_to_validate,
|
|
203
231
|
data=data,
|
|
232
|
+
at=at,
|
|
204
233
|
)
|
|
205
234
|
else:
|
|
206
235
|
async with db.start_transaction() as dbt:
|
|
207
|
-
obj = await
|
|
236
|
+
obj = await _do_create_node_with_lock(
|
|
208
237
|
node_class=node_class,
|
|
209
238
|
node_constraint_runner=node_constraint_runner,
|
|
210
239
|
db=dbt,
|
|
@@ -212,9 +241,12 @@ async def create_node(
|
|
|
212
241
|
branch=branch,
|
|
213
242
|
fields_to_validate=fields_to_validate,
|
|
214
243
|
data=data,
|
|
244
|
+
at=at,
|
|
215
245
|
)
|
|
216
246
|
|
|
217
247
|
if await get_profile_ids(db=db, obj=obj):
|
|
218
|
-
|
|
248
|
+
node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
|
|
249
|
+
await node_profiles_applier.apply_profiles(node=obj)
|
|
250
|
+
await obj.save(db=db)
|
|
219
251
|
|
|
220
252
|
return obj
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from infrahub.core.branch import Branch
|
|
5
|
+
from infrahub.core.constants.infrahubkind import GENERICGROUP, GRAPHQLQUERYGROUP
|
|
6
|
+
from infrahub.core.schema import GenericSchema
|
|
7
|
+
from infrahub.core.schema.schema_branch import SchemaBranch
|
|
8
|
+
|
|
9
|
+
KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED = [GENERICGROUP]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch) -> list[str]:
|
|
13
|
+
"""
|
|
14
|
+
Return kinds for which we want to lock during creating / updating an object of a given schema node.
|
|
15
|
+
Lock should be performed on schema kind and its generics having a uniqueness_constraint defined.
|
|
16
|
+
If a generic uniqueness constraint is the same as the node schema one,
|
|
17
|
+
it means node schema overrided this constraint, in which case we only need to lock on the generic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
node_schema = schema_branch.get(name=kind, duplicate=False)
|
|
21
|
+
|
|
22
|
+
schema_uc = None
|
|
23
|
+
kinds = []
|
|
24
|
+
if node_schema.uniqueness_constraints:
|
|
25
|
+
kinds.append(node_schema.kind)
|
|
26
|
+
schema_uc = node_schema.uniqueness_constraints
|
|
27
|
+
|
|
28
|
+
if isinstance(node_schema, GenericSchema):
|
|
29
|
+
return kinds
|
|
30
|
+
|
|
31
|
+
generics_kinds = node_schema.inherit_from
|
|
32
|
+
|
|
33
|
+
node_schema_kind_removed = False
|
|
34
|
+
for generic_kind in generics_kinds:
|
|
35
|
+
generic_uc = schema_branch.get(name=generic_kind, duplicate=False).uniqueness_constraints
|
|
36
|
+
if generic_uc:
|
|
37
|
+
kinds.append(generic_kind)
|
|
38
|
+
if not node_schema_kind_removed and generic_uc == schema_uc:
|
|
39
|
+
# Check whether we should remove original schema kind as it simply overrides uniqueness_constraint
|
|
40
|
+
# of a generic
|
|
41
|
+
kinds.pop(0)
|
|
42
|
+
node_schema_kind_removed = True
|
|
43
|
+
return kinds
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _should_kind_be_locked_on_any_branch(kind: str, schema_branch: SchemaBranch) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Check whether kind or any kind generic is in KINDS_TO_LOCK_ON_ANY_BRANCH.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
node_schema = schema_branch.get(name=kind, duplicate=False)
|
|
55
|
+
if isinstance(node_schema, GenericSchema):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
for generic_kind in node_schema.inherit_from:
|
|
59
|
+
if generic_kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _hash(value: str) -> str:
|
|
65
|
+
# Do not use builtin `hash` for lock names as due to randomization results would differ between
|
|
66
|
+
# different processes.
|
|
67
|
+
return hashlib.sha256(value.encode()).hexdigest()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_kind_lock_names_on_object_mutation(
|
|
71
|
+
kind: str, branch: Branch, schema_branch: SchemaBranch, data: dict[str, Any]
|
|
72
|
+
) -> list[str]:
|
|
73
|
+
"""
|
|
74
|
+
Return objects kind for which we want to avoid concurrent mutation (create/update). Except for some specific kinds,
|
|
75
|
+
concurrent mutations are only allowed on non-main branch as objects validations will be performed at least when merging in main branch.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
if not branch.is_default and not _should_kind_be_locked_on_any_branch(kind=kind, schema_branch=schema_branch):
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
if kind == GRAPHQLQUERYGROUP:
|
|
82
|
+
# Lock on name as well to improve performances
|
|
83
|
+
try:
|
|
84
|
+
name = data["name"].value
|
|
85
|
+
return [build_object_lock_name(kind + "." + _hash(name))]
|
|
86
|
+
except KeyError:
|
|
87
|
+
# We might reach here if we are updating a CoreGraphQLQueryGroup without updating the name,
|
|
88
|
+
# in which case we would not need to lock. This is not supposed to happen as current `update`
|
|
89
|
+
# logic first fetches the node with its name.
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
lock_kinds = _get_kinds_to_lock_on_object_mutation(kind, schema_branch)
|
|
93
|
+
lock_names = [build_object_lock_name(kind) for kind in lock_kinds]
|
|
94
|
+
return lock_names
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_object_lock_name(name: str) -> str:
|
|
98
|
+
return f"global.object.{name}"
|
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
|
infrahub/core/protocols.py
CHANGED
infrahub/core/query/attribute.py
CHANGED
|
@@ -133,7 +133,7 @@ class AttributeUpdateNodePropertyQuery(AttributeQuery):
|
|
|
133
133
|
def __init__(
|
|
134
134
|
self,
|
|
135
135
|
prop_name: str,
|
|
136
|
-
prop_id: str,
|
|
136
|
+
prop_id: str | None = None,
|
|
137
137
|
**kwargs: Any,
|
|
138
138
|
):
|
|
139
139
|
self.prop_name = prop_name
|
|
@@ -144,6 +144,8 @@ class AttributeUpdateNodePropertyQuery(AttributeQuery):
|
|
|
144
144
|
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
145
145
|
at = self.at or self.attr.at
|
|
146
146
|
|
|
147
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(at=at)
|
|
148
|
+
self.params.update(branch_params)
|
|
147
149
|
self.params["attr_uuid"] = self.attr.id
|
|
148
150
|
self.params["branch"] = self.branch.name
|
|
149
151
|
self.params["branch_level"] = self.branch.hierarchy_level
|
|
@@ -151,18 +153,34 @@ class AttributeUpdateNodePropertyQuery(AttributeQuery):
|
|
|
151
153
|
self.params["prop_name"] = self.prop_name
|
|
152
154
|
self.params["prop_id"] = self.prop_id
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
rel_label = f"HAS_{self.prop_name.upper()}"
|
|
155
157
|
|
|
156
|
-
|
|
158
|
+
if self.branch.is_default or self.branch.is_global:
|
|
159
|
+
node_query = """
|
|
160
|
+
MATCH (np:Node { uuid: $prop_id })-[r:IS_PART_OF]->(:Root)
|
|
161
|
+
WHERE r.branch IN $branch0
|
|
162
|
+
AND r.status = "active"
|
|
163
|
+
AND r.from <= $at AND (r.to IS NULL OR r.to > $at)
|
|
164
|
+
WITH np
|
|
165
|
+
LIMIT 1
|
|
157
166
|
"""
|
|
167
|
+
else:
|
|
168
|
+
node_query = """
|
|
169
|
+
MATCH (np:Node { uuid: $prop_id })-[r:IS_PART_OF]->(:Root)
|
|
170
|
+
WHERE %(branch_filter)s
|
|
171
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
172
|
+
LIMIT 1
|
|
173
|
+
WITH np
|
|
174
|
+
WHERE r.status = "active"
|
|
175
|
+
""" % {"branch_filter": branch_filter}
|
|
176
|
+
self.add_to_query(node_query)
|
|
177
|
+
|
|
178
|
+
attr_query = """
|
|
158
179
|
MATCH (a:Attribute { uuid: $attr_uuid })
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
% rel_name
|
|
163
|
-
)
|
|
180
|
+
CREATE (a)-[r:%(rel_label)s { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(np)
|
|
181
|
+
""" % {"rel_label": rel_label}
|
|
182
|
+
self.add_to_query(attr_query)
|
|
164
183
|
|
|
165
|
-
self.add_to_query(query)
|
|
166
184
|
self.return_labels = ["a", "np", "r"]
|
|
167
185
|
|
|
168
186
|
|
|
@@ -204,7 +222,6 @@ async def default_attribute_query_filter(
|
|
|
204
222
|
param_prefix: str | None = None,
|
|
205
223
|
db: InfrahubDatabase | None = None, # noqa: ARG001
|
|
206
224
|
partial_match: bool = False,
|
|
207
|
-
support_profiles: bool = False,
|
|
208
225
|
) -> tuple[list[QueryElement], dict[str, Any], list[str]]:
|
|
209
226
|
"""Generate Query String Snippet to filter the right node."""
|
|
210
227
|
attribute_value_label = GraphAttributeValueNode.get_default_label()
|
|
@@ -251,9 +268,6 @@ async def default_attribute_query_filter(
|
|
|
251
268
|
query_where.append(f"toString(av.{filter_name}) =~ ${param_prefix}_{filter_name}")
|
|
252
269
|
elif filter_name == "isnull":
|
|
253
270
|
query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
|
|
254
|
-
elif support_profiles:
|
|
255
|
-
query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
|
|
256
|
-
query_where.append(f"(av.{filter_name} = ${param_prefix}_{filter_name} OR av.is_default)")
|
|
257
271
|
else:
|
|
258
272
|
query_filter.append(
|
|
259
273
|
QueryNode(
|
|
@@ -271,8 +285,6 @@ async def default_attribute_query_filter(
|
|
|
271
285
|
if attribute_kind and attribute_kind == "List":
|
|
272
286
|
query_params[f"{param_prefix}_{filter_name}"] = build_regex_attrs(values=filter_value)
|
|
273
287
|
query_where.append(f"toString(av.value) =~ ${param_prefix}_{filter_name}")
|
|
274
|
-
elif support_profiles:
|
|
275
|
-
query_where.append(f"(av.value IN ${param_prefix}_value OR av.is_default)")
|
|
276
288
|
else:
|
|
277
289
|
query_where.append(f"av.value IN ${param_prefix}_value")
|
|
278
290
|
query_params[f"{param_prefix}_value"] = filter_value
|