infrahub-server 1.4.10__py3-none-any.whl → 1.5.0b1__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 +208 -16
- infrahub/api/artifact.py +3 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/api/query.py +2 -0
- infrahub/api/schema.py +3 -0
- infrahub/auth.py +5 -5
- infrahub/cli/db.py +26 -2
- infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
- infrahub/config.py +7 -2
- infrahub/core/attribute.py +25 -22
- infrahub/core/branch/models.py +2 -2
- infrahub/core/branch/needs_rebase_status.py +11 -0
- infrahub/core/branch/tasks.py +4 -3
- infrahub/core/changelog/models.py +4 -12
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/constants/infrahubkind.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/model/path.py +4 -0
- infrahub/core/diff/payload_builder.py +1 -1
- infrahub/core/diff/query/artifact.py +1 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +2 -2
- infrahub/core/ipam/utilization.py +1 -1
- infrahub/core/manager.py +9 -84
- infrahub/core/migrations/graph/__init__.py +6 -0
- infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
- infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
- infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
- infrahub/core/migrations/schema/node_attribute_add.py +5 -2
- infrahub/core/migrations/shared.py +5 -6
- infrahub/core/node/__init__.py +165 -42
- infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
- infrahub/core/node/create.py +67 -35
- infrahub/core/node/lock_utils.py +98 -0
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/standard.py +1 -1
- infrahub/core/property.py +11 -0
- infrahub/core/protocols.py +8 -1
- infrahub/core/query/attribute.py +27 -15
- infrahub/core/query/node.py +61 -185
- infrahub/core/query/relationship.py +43 -26
- infrahub/core/query/subquery.py +0 -8
- infrahub/core/registry.py +2 -2
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/model.py +60 -20
- infrahub/core/schema/attribute_schema.py +0 -2
- infrahub/core/schema/basenode_schema.py +42 -2
- infrahub/core/schema/definitions/core/__init__.py +2 -0
- infrahub/core/schema/definitions/core/generator.py +2 -0
- infrahub/core/schema/definitions/core/group.py +16 -2
- infrahub/core/schema/definitions/core/repository.py +7 -0
- infrahub/core/schema/definitions/internal.py +14 -1
- infrahub/core/schema/generated/base_node_schema.py +6 -1
- infrahub/core/schema/node_schema.py +5 -2
- infrahub/core/schema/relationship_schema.py +0 -1
- infrahub/core/schema/schema_branch.py +137 -2
- infrahub/core/schema/schema_branch_display.py +123 -0
- infrahub/core/schema/schema_branch_hfid.py +114 -0
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/determiner.py +12 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/core/validators/tasks.py +1 -1
- infrahub/display_labels/__init__.py +0 -0
- infrahub/display_labels/gather.py +48 -0
- infrahub/display_labels/models.py +240 -0
- infrahub/display_labels/tasks.py +186 -0
- infrahub/display_labels/triggers.py +22 -0
- infrahub/events/group_action.py +1 -1
- infrahub/events/node_action.py +1 -1
- infrahub/generators/constants.py +7 -0
- infrahub/generators/models.py +38 -12
- infrahub/generators/tasks.py +34 -16
- infrahub/git/base.py +38 -1
- infrahub/git/integrator.py +22 -14
- infrahub/graphql/analyzer.py +1 -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/display_label.py +111 -0
- infrahub/graphql/mutations/generator.py +25 -7
- infrahub/graphql/mutations/hfid.py +118 -0
- 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/relationship.py +2 -2
- infrahub/graphql/mutations/repository.py +22 -83
- infrahub/graphql/mutations/resource_manager.py +2 -2
- infrahub/graphql/mutations/schema.py +5 -5
- infrahub/graphql/mutations/webhook.py +1 -1
- infrahub/graphql/queries/resource_manager.py +1 -1
- infrahub/graphql/registry.py +173 -0
- infrahub/graphql/resolvers/resolver.py +2 -0
- infrahub/graphql/schema.py +8 -1
- infrahub/groups/tasks.py +1 -1
- infrahub/hfid/__init__.py +0 -0
- infrahub/hfid/gather.py +48 -0
- infrahub/hfid/models.py +240 -0
- infrahub/hfid/tasks.py +185 -0
- infrahub/hfid/triggers.py +22 -0
- infrahub/lock.py +67 -30
- infrahub/locks/__init__.py +0 -0
- infrahub/locks/tasks.py +37 -0
- infrahub/middleware.py +26 -1
- 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/proposed_change/tasks.py +10 -1
- infrahub/repositories/__init__.py +0 -0
- infrahub/repositories/create_repository.py +113 -0
- infrahub/server.py +16 -3
- infrahub/services/__init__.py +8 -5
- infrahub/tasks/registry.py +6 -4
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/tasks.py +3 -0
- infrahub/webhook/models.py +1 -1
- infrahub/workflows/catalogue.py +110 -3
- infrahub/workflows/initialization.py +16 -0
- infrahub/workflows/models.py +17 -2
- infrahub_sdk/branch.py +5 -8
- infrahub_sdk/checks.py +1 -1
- 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 +18 -12
- infrahub_sdk/ctl/config.py +8 -2
- infrahub_sdk/ctl/generator.py +6 -3
- infrahub_sdk/ctl/graphql.py +184 -0
- infrahub_sdk/ctl/repository.py +39 -1
- infrahub_sdk/ctl/schema.py +18 -3
- 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/generator.py +7 -1
- infrahub_sdk/graphql/__init__.py +12 -0
- infrahub_sdk/graphql/constants.py +1 -0
- infrahub_sdk/graphql/plugin.py +85 -0
- infrahub_sdk/graphql/query.py +77 -0
- infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
- infrahub_sdk/graphql/utils.py +40 -0
- infrahub_sdk/node/attribute.py +2 -0
- infrahub_sdk/node/node.py +28 -20
- infrahub_sdk/playback.py +1 -2
- infrahub_sdk/protocols.py +54 -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/__init__.py +38 -0
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/object.py +120 -7
- infrahub_sdk/spec/range_expansion.py +118 -0
- infrahub_sdk/timestamp.py +18 -6
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +9 -11
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +177 -134
- infrahub_testcontainers/container.py +1 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- 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.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
infrahub/core/node/__init__.py
CHANGED
|
@@ -42,10 +42,12 @@ 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
|
|
48
49
|
from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
|
|
50
|
+
from .node_property_attribute import DisplayLabel, HumanFriendlyIdentifier
|
|
49
51
|
|
|
50
52
|
if TYPE_CHECKING:
|
|
51
53
|
from typing_extensions import Self
|
|
@@ -53,8 +55,6 @@ if TYPE_CHECKING:
|
|
|
53
55
|
from infrahub.core.branch import Branch
|
|
54
56
|
from infrahub.database import InfrahubDatabase
|
|
55
57
|
|
|
56
|
-
from ..attribute import BaseAttribute
|
|
57
|
-
|
|
58
58
|
SchemaProtocol = TypeVar("SchemaProtocol")
|
|
59
59
|
|
|
60
60
|
# ---------------------------------------------------------------------------------------
|
|
@@ -80,6 +80,29 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
80
80
|
_meta.default_filter = default_filter
|
|
81
81
|
super().__init_subclass_with_meta__(_meta=_meta, **options)
|
|
82
82
|
|
|
83
|
+
def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
|
|
84
|
+
self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
|
|
85
|
+
self._branch: Branch = branch
|
|
86
|
+
self._at: Timestamp = at
|
|
87
|
+
self._existing: bool = False
|
|
88
|
+
|
|
89
|
+
self._updated_at: Timestamp | None = None
|
|
90
|
+
self.id: str = None
|
|
91
|
+
self.db_id: str = None
|
|
92
|
+
|
|
93
|
+
self._source: Node | None = None
|
|
94
|
+
self._owner: Node | None = None
|
|
95
|
+
self._is_protected: bool = None
|
|
96
|
+
self._computed_jinja2_attributes: list[str] = []
|
|
97
|
+
|
|
98
|
+
self._display_label: DisplayLabel | None = None
|
|
99
|
+
self._human_friendly_id: HumanFriendlyIdentifier | None = None
|
|
100
|
+
|
|
101
|
+
# Lists of attributes and relationships names
|
|
102
|
+
self._attributes: list[str] = []
|
|
103
|
+
self._relationships: list[str] = []
|
|
104
|
+
self._node_changelog: NodeChangelog | None = None
|
|
105
|
+
|
|
83
106
|
def get_schema(self) -> NonGenericSchemaTypes:
|
|
84
107
|
return self._schema
|
|
85
108
|
|
|
@@ -100,16 +123,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
100
123
|
def get_updated_at(self) -> Timestamp | None:
|
|
101
124
|
return self._updated_at
|
|
102
125
|
|
|
126
|
+
def get_attribute(self, name: str) -> BaseAttribute:
|
|
127
|
+
attribute = getattr(self, name)
|
|
128
|
+
if not isinstance(attribute, BaseAttribute):
|
|
129
|
+
raise ValueError(f"{name} is not an attribute of {self.get_kind()}")
|
|
130
|
+
return attribute
|
|
131
|
+
|
|
132
|
+
def get_relationship(self, name: str) -> RelationshipManager:
|
|
133
|
+
relationship = getattr(self, name)
|
|
134
|
+
if not isinstance(relationship, RelationshipManager):
|
|
135
|
+
raise ValueError(f"{name} is not a relationship of {self.get_kind()}")
|
|
136
|
+
return relationship
|
|
137
|
+
|
|
138
|
+
def uses_profiles(self) -> bool:
|
|
139
|
+
for attr_name in self.get_schema().attribute_names:
|
|
140
|
+
try:
|
|
141
|
+
node_attr = self.get_attribute(attr_name)
|
|
142
|
+
except ValueError:
|
|
143
|
+
continue
|
|
144
|
+
if node_attr and node_attr.is_from_profile:
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
103
148
|
async def get_hfid(self, db: InfrahubDatabase, include_kind: bool = False) -> list[str] | None:
|
|
104
149
|
"""Return the Human friendly id of the node."""
|
|
105
150
|
if not self._schema.human_friendly_id:
|
|
106
151
|
return None
|
|
107
152
|
|
|
108
|
-
hfid_values
|
|
153
|
+
hfid_values: list[str] | None = None
|
|
154
|
+
if self._human_friendly_id:
|
|
155
|
+
hfid_values = self._human_friendly_id.get_value(node=self, at=self._at)
|
|
156
|
+
if not hfid_values:
|
|
157
|
+
hfid_values = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
|
|
158
|
+
|
|
109
159
|
hfid = [value for value in hfid_values if value is not None]
|
|
110
|
-
if include_kind
|
|
111
|
-
return [self.get_kind()] + hfid
|
|
112
|
-
return hfid
|
|
160
|
+
return [self.get_kind()] + hfid if include_kind else hfid
|
|
113
161
|
|
|
114
162
|
async def get_hfid_as_string(self, db: InfrahubDatabase, include_kind: bool = False) -> str | None:
|
|
115
163
|
"""Return the Human friendly id of the node in string format separated with a dunder (__) ."""
|
|
@@ -118,6 +166,37 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
118
166
|
return None
|
|
119
167
|
return "__".join(hfid)
|
|
120
168
|
|
|
169
|
+
def has_human_friendly_id(self) -> bool:
|
|
170
|
+
return self._human_friendly_id is not None
|
|
171
|
+
|
|
172
|
+
async def add_human_friendly_id(self, db: InfrahubDatabase) -> None:
|
|
173
|
+
if not self._schema.human_friendly_id or self._human_friendly_id:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
self._human_friendly_id = HumanFriendlyIdentifier(
|
|
177
|
+
node_schema=self._schema, template=self._schema.human_friendly_id
|
|
178
|
+
)
|
|
179
|
+
await self._human_friendly_id.compute(db=db, node=self)
|
|
180
|
+
|
|
181
|
+
async def get_display_label(self, db: InfrahubDatabase) -> str:
|
|
182
|
+
if self._display_label:
|
|
183
|
+
if isinstance(self._display_label._value, str):
|
|
184
|
+
return self._display_label._value
|
|
185
|
+
if self._display_label._value:
|
|
186
|
+
return self._display_label._value.value
|
|
187
|
+
|
|
188
|
+
return await self.render_display_label(db=db)
|
|
189
|
+
|
|
190
|
+
def has_display_label(self) -> bool:
|
|
191
|
+
return self._display_label is not None
|
|
192
|
+
|
|
193
|
+
async def add_display_label(self, db: InfrahubDatabase) -> None:
|
|
194
|
+
if not self._schema.display_label or self._display_label:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
self._display_label = DisplayLabel(node_schema=self._schema, template=self._schema.display_label)
|
|
198
|
+
await self._display_label.compute(db=db, node=self)
|
|
199
|
+
|
|
121
200
|
async def get_path_value(self, db: InfrahubDatabase, path: str) -> str:
|
|
122
201
|
schema_path = self._schema.parse_schema_path(
|
|
123
202
|
path=path, schema=db.schema.get_schema_branch(name=self._branch.name)
|
|
@@ -176,30 +255,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
176
255
|
return self._branch
|
|
177
256
|
|
|
178
257
|
def __repr__(self) -> str:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return f"{self.get_kind()}(ID: {str(self.id)})"
|
|
183
|
-
|
|
184
|
-
def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
|
|
185
|
-
self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
|
|
186
|
-
self._branch: Branch = branch
|
|
187
|
-
self._at: Timestamp = at
|
|
188
|
-
self._existing: bool = False
|
|
189
|
-
|
|
190
|
-
self._updated_at: Timestamp | None = None
|
|
191
|
-
self.id: str = None
|
|
192
|
-
self.db_id: str = None
|
|
193
|
-
|
|
194
|
-
self._source: Node | None = None
|
|
195
|
-
self._owner: Node | None = None
|
|
196
|
-
self._is_protected: bool = None
|
|
197
|
-
self._computed_jinja2_attributes: list[str] = []
|
|
198
|
-
|
|
199
|
-
# Lists of attributes and relationships names
|
|
200
|
-
self._attributes: list[str] = []
|
|
201
|
-
self._relationships: list[str] = []
|
|
202
|
-
self._node_changelog: NodeChangelog | None = None
|
|
258
|
+
v = f"{self.get_kind()}(ID: {str(self.id)})"
|
|
259
|
+
return v if self._existing else f"{v}[NEW]"
|
|
203
260
|
|
|
204
261
|
@property
|
|
205
262
|
def node_changelog(self) -> NodeChangelog:
|
|
@@ -712,12 +769,26 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
712
769
|
)
|
|
713
770
|
self._updated_at = Timestamp(updated_at)
|
|
714
771
|
|
|
772
|
+
if not self._schema.is_schema_node:
|
|
773
|
+
if hfid := kwargs.pop("human_friendly_id", None):
|
|
774
|
+
self._human_friendly_id = HumanFriendlyIdentifier(
|
|
775
|
+
node_schema=self._schema, template=self._schema.human_friendly_id, value=hfid
|
|
776
|
+
)
|
|
777
|
+
if display_label := kwargs.pop("display_label", None):
|
|
778
|
+
self._display_label = DisplayLabel(
|
|
779
|
+
node_schema=self._schema, template=self._schema.display_label, value=display_label
|
|
780
|
+
)
|
|
781
|
+
|
|
715
782
|
await self._process_fields(db=db, fields=kwargs)
|
|
716
783
|
return self
|
|
717
784
|
|
|
718
785
|
async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> NodeChangelog:
|
|
719
786
|
create_at = Timestamp(at)
|
|
720
787
|
|
|
788
|
+
if not self._schema.is_schema_node:
|
|
789
|
+
await self.add_human_friendly_id(db=db)
|
|
790
|
+
await self.add_display_label(db=db)
|
|
791
|
+
|
|
721
792
|
query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at)
|
|
722
793
|
await query.execute(db=db)
|
|
723
794
|
|
|
@@ -729,6 +800,13 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
729
800
|
new_ids = query.get_ids()
|
|
730
801
|
node_changelog = NodeChangelog(node_id=self.get_id(), node_kind=self.get_kind(), display_label="")
|
|
731
802
|
|
|
803
|
+
if self._human_friendly_id:
|
|
804
|
+
node_changelog.create_attribute(
|
|
805
|
+
attribute=self._human_friendly_id.get_node_attribute(node=self, at=create_at)
|
|
806
|
+
)
|
|
807
|
+
if self._display_label:
|
|
808
|
+
node_changelog.create_attribute(attribute=self._display_label.get_node_attribute(node=self, at=create_at))
|
|
809
|
+
|
|
732
810
|
# Go over the list of Attribute and assign the new IDs one by one
|
|
733
811
|
for name in self._attributes:
|
|
734
812
|
attr: BaseAttribute = getattr(self, name)
|
|
@@ -741,12 +819,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
741
819
|
relm: RelationshipManager = getattr(self, name)
|
|
742
820
|
for rel in relm._relationships:
|
|
743
821
|
identifier = f"{rel.schema.identifier}::{rel.peer_id}"
|
|
744
|
-
|
|
745
822
|
rel.id, rel.db_id = new_ids[identifier]
|
|
746
|
-
|
|
747
823
|
node_changelog.create_relationship(relationship=rel)
|
|
748
824
|
|
|
749
|
-
node_changelog.display_label = await self.
|
|
825
|
+
node_changelog.display_label = await self.get_display_label(db=db)
|
|
750
826
|
return node_changelog
|
|
751
827
|
|
|
752
828
|
async def _update(
|
|
@@ -782,19 +858,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
782
858
|
if parent := await rel.get_parent(db=db):
|
|
783
859
|
node_changelog.add_parent_from_relationship(parent=parent)
|
|
784
860
|
|
|
785
|
-
|
|
861
|
+
# Update the HFID if one of its variables is being updated
|
|
862
|
+
if self._human_friendly_id and (
|
|
863
|
+
(fields and "human_friendly_id" in fields) or self._human_friendly_id.needs_update(fields=fields)
|
|
864
|
+
):
|
|
865
|
+
await self._human_friendly_id.compute(db=db, node=self)
|
|
866
|
+
updated_attribute = await self._human_friendly_id.get_node_attribute(node=self, at=update_at).save(
|
|
867
|
+
at=update_at, db=db
|
|
868
|
+
)
|
|
869
|
+
if updated_attribute:
|
|
870
|
+
node_changelog.add_attribute(attribute=updated_attribute)
|
|
871
|
+
|
|
872
|
+
# Update the display label if one of its variables is being updated
|
|
873
|
+
if self._display_label and (
|
|
874
|
+
(fields and "display_label" in fields) or self._display_label.needs_update(fields=fields)
|
|
875
|
+
):
|
|
876
|
+
await self._display_label.compute(db=db, node=self)
|
|
877
|
+
self._display_label.get_node_attribute(node=self, at=update_at).get_create_data(node_schema=self._schema)
|
|
878
|
+
updated_attribute = await self._display_label.get_node_attribute(node=self, at=update_at).save(
|
|
879
|
+
at=update_at, db=db
|
|
880
|
+
)
|
|
881
|
+
if updated_attribute:
|
|
882
|
+
node_changelog.add_attribute(attribute=updated_attribute)
|
|
883
|
+
|
|
884
|
+
node_changelog.display_label = await self.get_display_label(db=db)
|
|
786
885
|
return node_changelog
|
|
787
886
|
|
|
788
887
|
async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> Self:
|
|
789
888
|
"""Create or Update the Node in the database."""
|
|
790
|
-
|
|
791
889
|
save_at = Timestamp(at)
|
|
792
890
|
|
|
793
891
|
if self._existing:
|
|
794
892
|
self._node_changelog = await self._update(at=save_at, db=db, fields=fields)
|
|
795
|
-
|
|
893
|
+
else:
|
|
894
|
+
self._node_changelog = await self._create(at=save_at, db=db)
|
|
796
895
|
|
|
797
|
-
self._node_changelog = await self._create(at=save_at, db=db)
|
|
798
896
|
return self
|
|
799
897
|
|
|
800
898
|
async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
|
|
@@ -803,13 +901,24 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
803
901
|
delete_at = Timestamp(at)
|
|
804
902
|
|
|
805
903
|
node_changelog = NodeChangelog(
|
|
806
|
-
node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.
|
|
904
|
+
node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.get_display_label(db=db)
|
|
807
905
|
)
|
|
808
906
|
# Go over the list of Attribute and update them one by one
|
|
809
907
|
for name in self._attributes:
|
|
810
908
|
attr: BaseAttribute = getattr(self, name)
|
|
811
|
-
deleted_attribute
|
|
812
|
-
|
|
909
|
+
if deleted_attribute := await attr.delete(at=delete_at, db=db):
|
|
910
|
+
node_changelog.add_attribute(attribute=deleted_attribute)
|
|
911
|
+
|
|
912
|
+
if self._human_friendly_id:
|
|
913
|
+
if deleted_attribute := await self._human_friendly_id.get_node_attribute(node=self, at=delete_at).delete(
|
|
914
|
+
at=delete_at, db=db
|
|
915
|
+
):
|
|
916
|
+
node_changelog.add_attribute(attribute=deleted_attribute)
|
|
917
|
+
|
|
918
|
+
if self._display_label:
|
|
919
|
+
if deleted_attribute := await self._display_label.get_node_attribute(node=self, at=delete_at).delete(
|
|
920
|
+
at=delete_at, db=db
|
|
921
|
+
):
|
|
813
922
|
node_changelog.add_attribute(attribute=deleted_attribute)
|
|
814
923
|
|
|
815
924
|
branch = self.get_branch_based_on_support_type()
|
|
@@ -877,7 +986,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
877
986
|
continue
|
|
878
987
|
|
|
879
988
|
if field_name == "display_label":
|
|
880
|
-
response[field_name] = await self.
|
|
989
|
+
response[field_name] = await self.get_display_label(db=db)
|
|
881
990
|
continue
|
|
882
991
|
|
|
883
992
|
if field_name == "hfid":
|
|
@@ -980,6 +1089,20 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
980
1089
|
return repr(self)
|
|
981
1090
|
return display_label.strip()
|
|
982
1091
|
|
|
1092
|
+
async def set_human_friendly_id(self, value: list[str] | None) -> None:
|
|
1093
|
+
"""Set the human friendly ID of this node if one is set. `save()` must be called to commit the change in the database."""
|
|
1094
|
+
if self._human_friendly_id is None:
|
|
1095
|
+
return
|
|
1096
|
+
|
|
1097
|
+
self._human_friendly_id.set_value(value=value, manually_assigned=True)
|
|
1098
|
+
|
|
1099
|
+
async def set_display_label(self, value: str | None) -> None:
|
|
1100
|
+
"""Set the display label of this node if one is set. `save()` must be called to commit the change in the database."""
|
|
1101
|
+
if self._display_label is None:
|
|
1102
|
+
return
|
|
1103
|
+
|
|
1104
|
+
self._display_label.set_value(value=value, manually_assigned=True)
|
|
1105
|
+
|
|
983
1106
|
def _get_parent_relationship_name(self) -> str | None:
|
|
984
1107
|
"""Return the name of the parent relationship is one is present"""
|
|
985
1108
|
for relationship in self._schema.relationships:
|
|
@@ -989,7 +1112,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
989
1112
|
return None
|
|
990
1113
|
|
|
991
1114
|
async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
|
|
992
|
-
object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
|
|
1115
|
+
object_template: RelationshipManager | None = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
|
|
993
1116
|
return (
|
|
994
1117
|
await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
|
|
995
1118
|
)
|
|
@@ -29,7 +29,9 @@ class NodeAttributeUniquenessConstraint(NodeConstraintInterface):
|
|
|
29
29
|
attr = getattr(node, unique_attr.name)
|
|
30
30
|
if unique_attr.inherited:
|
|
31
31
|
for generic_parent_schema_name in node_schema.inherit_from:
|
|
32
|
-
generic_parent_schema = self.db.schema.get(
|
|
32
|
+
generic_parent_schema = self.db.schema.get(
|
|
33
|
+
generic_parent_schema_name, branch=self.branch, duplicate=False
|
|
34
|
+
)
|
|
33
35
|
parent_attr = generic_parent_schema.get_attribute_or_none(unique_attr.name)
|
|
34
36
|
if parent_attr is None:
|
|
35
37
|
continue
|
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}"
|