infrahub-server 1.3.0b1__py3-none-any.whl → 1.3.0b3__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/constants.py +87 -0
- infrahub/actions/gather.py +3 -3
- infrahub/actions/models.py +10 -8
- infrahub/actions/parsers.py +6 -6
- infrahub/actions/schema.py +46 -37
- infrahub/actions/tasks.py +4 -11
- infrahub/branch/__init__.py +0 -0
- infrahub/branch/tasks.py +29 -0
- infrahub/branch/triggers.py +22 -0
- infrahub/cli/db.py +2 -2
- infrahub/computed_attribute/gather.py +3 -1
- infrahub/computed_attribute/tasks.py +23 -29
- infrahub/core/constants/__init__.py +5 -0
- infrahub/core/constants/database.py +1 -0
- infrahub/core/convert_object_type/conversion.py +1 -1
- infrahub/core/diff/query/save.py +67 -40
- infrahub/core/diff/query/time_range_query.py +0 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/migrations/graph/__init__.py +6 -0
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +0 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +662 -0
- infrahub/core/migrations/graph/m030_illegal_edges.py +82 -0
- infrahub/core/migrations/query/attribute_add.py +13 -9
- infrahub/core/migrations/query/relationship_duplicate.py +0 -1
- infrahub/core/migrations/schema/node_remove.py +0 -1
- infrahub/core/node/__init__.py +2 -0
- infrahub/core/node/base.py +1 -1
- infrahub/core/path.py +1 -1
- infrahub/core/protocols.py +4 -3
- infrahub/core/query/node.py +1 -1
- infrahub/core/query/relationship.py +2 -2
- infrahub/core/query/standard_node.py +19 -5
- infrahub/core/relationship/constraints/peer_relatives.py +72 -0
- infrahub/core/relationship/model.py +1 -1
- infrahub/core/schema/attribute_schema.py +26 -6
- infrahub/core/schema/basenode_schema.py +2 -2
- infrahub/core/schema/definitions/core/resource_pool.py +9 -0
- infrahub/core/schema/definitions/internal.py +9 -1
- infrahub/core/schema/generated/attribute_schema.py +4 -4
- infrahub/core/schema/generated/relationship_schema.py +6 -1
- infrahub/core/schema/manager.py +4 -2
- infrahub/core/schema/schema_branch.py +14 -5
- infrahub/core/validators/tasks.py +1 -1
- infrahub/database/__init__.py +1 -1
- infrahub/database/validation.py +100 -0
- infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
- infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
- infrahub/dependencies/builder/diff/deserializer.py +1 -1
- infrahub/dependencies/registry.py +2 -0
- infrahub/events/models.py +1 -1
- infrahub/graphql/mutations/main.py +1 -1
- infrahub/graphql/mutations/resource_manager.py +13 -13
- infrahub/graphql/resolvers/many_relationship.py +1 -1
- infrahub/graphql/resolvers/resolver.py +2 -2
- infrahub/graphql/resolvers/single_relationship.py +1 -1
- infrahub/menu/menu.py +5 -4
- infrahub/message_bus/operations/refresh/registry.py +3 -3
- infrahub/patch/queries/delete_duplicated_edges.py +40 -29
- infrahub/pools/registration.py +22 -0
- infrahub/pools/tasks.py +56 -0
- infrahub/proposed_change/tasks.py +8 -8
- infrahub/schema/__init__.py +0 -0
- infrahub/schema/tasks.py +27 -0
- infrahub/schema/triggers.py +23 -0
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +5 -4
- infrahub/trigger/setup.py +26 -2
- infrahub/trigger/tasks.py +1 -1
- infrahub/webhook/tasks.py +6 -9
- infrahub/workflows/catalogue.py +27 -1
- {infrahub_server-1.3.0b1.dist-info → infrahub_server-1.3.0b3.dist-info}/METADATA +1 -1
- {infrahub_server-1.3.0b1.dist-info → infrahub_server-1.3.0b3.dist-info}/RECORD +80 -67
- infrahub_testcontainers/container.py +239 -64
- infrahub_testcontainers/docker-compose-cluster.test.yml +321 -0
- infrahub_testcontainers/docker-compose.test.yml +1 -0
- infrahub_testcontainers/helpers.py +15 -1
- infrahub_testcontainers/plugin.py +9 -0
- infrahub/patch/queries/consolidate_duplicated_nodes.py +0 -106
- {infrahub_server-1.3.0b1.dist-info → infrahub_server-1.3.0b3.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.3.0b1.dist-info → infrahub_server-1.3.0b3.dist-info}/WHEEL +0 -0
- {infrahub_server-1.3.0b1.dist-info → infrahub_server-1.3.0b3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
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 DeletePosthumousEdges(Query):
|
|
17
|
+
name = "delete_posthumous_edges_query"
|
|
18
|
+
type = QueryType.WRITE
|
|
19
|
+
insert_return = False
|
|
20
|
+
|
|
21
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
22
|
+
query = """
|
|
23
|
+
// ------------
|
|
24
|
+
// find deleted nodes
|
|
25
|
+
// ------------
|
|
26
|
+
MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
|
|
27
|
+
WHERE e.status = "deleted" OR e.to IS NOT NULL
|
|
28
|
+
WITH DISTINCT n, e.branch AS delete_branch, e.branch_level AS delete_branch_level, CASE
|
|
29
|
+
WHEN e.status = "deleted" THEN e.from
|
|
30
|
+
ELSE e.to
|
|
31
|
+
END AS delete_time
|
|
32
|
+
// ------------
|
|
33
|
+
// find the edges added to the deleted node after the delete time
|
|
34
|
+
// ------------
|
|
35
|
+
MATCH (n)-[added_e]-(peer)
|
|
36
|
+
WHERE added_e.from > delete_time
|
|
37
|
+
AND type(added_e) <> "IS_PART_OF"
|
|
38
|
+
// if the node was deleted on a branch (delete_branch_level > 1), and then updated on main/global (added_e.branch_level = 1), we can ignore it
|
|
39
|
+
AND added_e.branch_level >= delete_branch_level
|
|
40
|
+
AND (added_e.branch = delete_branch OR delete_branch_level = 1)
|
|
41
|
+
WITH DISTINCT n, delete_branch, delete_time, added_e, peer
|
|
42
|
+
// ------------
|
|
43
|
+
// get the branched_from for the branch on which the node was deleted
|
|
44
|
+
// ------------
|
|
45
|
+
CALL (added_e) {
|
|
46
|
+
MATCH (b:Branch {name: added_e.branch})
|
|
47
|
+
RETURN b.branched_from AS added_e_branched_from
|
|
48
|
+
}
|
|
49
|
+
// ------------
|
|
50
|
+
// account for the following situations, given that the edge update time is after the node delete time
|
|
51
|
+
// - deleted on main/global, updated on branch
|
|
52
|
+
// - illegal if the delete is before branch.branched_from
|
|
53
|
+
// - deleted on branch, updated on branch
|
|
54
|
+
// - illegal
|
|
55
|
+
// ------------
|
|
56
|
+
WITH n, delete_branch, delete_time, added_e, peer
|
|
57
|
+
WHERE delete_branch = added_e.branch
|
|
58
|
+
OR delete_time < added_e_branched_from
|
|
59
|
+
DELETE added_e
|
|
60
|
+
// --------------
|
|
61
|
+
// the peer _should_ only be an Attribute, but I want to make sure we don't
|
|
62
|
+
// inadvertently delete Root or an AttributeValue or a Boolean
|
|
63
|
+
// --------------
|
|
64
|
+
WITH peer
|
|
65
|
+
WHERE "Attribute" IN labels(peer)
|
|
66
|
+
DETACH DELETE peer
|
|
67
|
+
"""
|
|
68
|
+
self.add_to_query(query)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Migration030(GraphMigration):
|
|
72
|
+
"""
|
|
73
|
+
Edges could have been added to Nodes after the Node was deleted, so we need to hard-delete those illegal edges
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name: str = "030_delete_illegal_edges"
|
|
77
|
+
minimum_version: int = 29
|
|
78
|
+
queries: Sequence[type[Query]] = [DeletePosthumousEdges]
|
|
79
|
+
|
|
80
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
81
|
+
result = MigrationResult()
|
|
82
|
+
return result
|
|
@@ -62,28 +62,32 @@ class AttributeAddQuery(Query):
|
|
|
62
62
|
WITH av, is_protected_value, is_visible_value
|
|
63
63
|
MATCH p = (n:%(node_kind)s)
|
|
64
64
|
CALL (n) {
|
|
65
|
-
MATCH (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
MATCH (:Root)<-[r:IS_PART_OF]-(n)
|
|
66
|
+
WHERE %(branch_filter)s
|
|
67
|
+
WITH n, r AS is_part_of_e
|
|
68
|
+
OPTIONAL MATCH (n)-[r:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name })
|
|
69
|
+
WHERE %(branch_filter)s
|
|
70
|
+
WITH is_part_of_e, r AS has_attr_e
|
|
71
|
+
RETURN is_part_of_e, has_attr_e
|
|
72
|
+
ORDER BY has_attr_e.branch_level DESC, has_attr_e.from ASC, is_part_of_e.branch_level DESC, is_part_of_e.from ASC
|
|
70
73
|
LIMIT 1
|
|
71
74
|
}
|
|
72
|
-
WITH
|
|
73
|
-
WHERE
|
|
75
|
+
WITH n, is_part_of_e, has_attr_e, av, is_protected_value, is_visible_value
|
|
76
|
+
WHERE is_part_of_e.status = "active" AND (has_attr_e IS NULL OR has_attr_e.status = "deleted")
|
|
74
77
|
CREATE (a:Attribute { name: $attr_name, branch_support: $branch_support })
|
|
75
78
|
CREATE (n)-[:HAS_ATTRIBUTE $rel_props ]->(a)
|
|
76
79
|
CREATE (a)-[:HAS_VALUE $rel_props ]->(av)
|
|
77
80
|
CREATE (a)-[:IS_PROTECTED $rel_props]->(is_protected_value)
|
|
78
81
|
CREATE (a)-[:IS_VISIBLE $rel_props]->(is_visible_value)
|
|
79
82
|
%(uuid_generation)s
|
|
80
|
-
FOREACH (i in CASE WHEN
|
|
81
|
-
SET
|
|
83
|
+
FOREACH (i in CASE WHEN has_attr_e.status = "deleted" THEN [1] ELSE [] END |
|
|
84
|
+
SET has_attr_e.to = $current_time
|
|
82
85
|
)
|
|
83
86
|
""" % {
|
|
84
87
|
"branch_filter": branch_filter,
|
|
85
88
|
"node_kind": self.node_kind,
|
|
86
89
|
"uuid_generation": db.render_uuid_generation(node_label="a", node_attr="uuid"),
|
|
87
90
|
}
|
|
91
|
+
|
|
88
92
|
self.add_to_query(query)
|
|
89
93
|
self.return_labels = ["n.uuid", "a.uuid"]
|
infrahub/core/node/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ from infrahub.core.constants import (
|
|
|
16
16
|
BranchSupportType,
|
|
17
17
|
ComputedAttributeKind,
|
|
18
18
|
InfrahubKind,
|
|
19
|
+
NumberPoolType,
|
|
19
20
|
RelationshipCardinality,
|
|
20
21
|
RelationshipKind,
|
|
21
22
|
)
|
|
@@ -328,6 +329,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
328
329
|
node_attribute=attribute.schema.name,
|
|
329
330
|
start_range=number_pool_parameters.start_range,
|
|
330
331
|
end_range=number_pool_parameters.end_range,
|
|
332
|
+
pool_type=NumberPoolType.SCHEMA.value,
|
|
331
333
|
)
|
|
332
334
|
await number_pool.save(db=db)
|
|
333
335
|
# Do a lookup of the number pool to get the correct mapped type from the registry
|
infrahub/core/node/base.py
CHANGED
|
@@ -52,7 +52,7 @@ class BaseNodeOptions(BaseOptions):
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class ObjectNodeMeta(BaseNodeMeta):
|
|
55
|
-
def __new__(mcs, name_, bases, namespace, **options):
|
|
55
|
+
def __new__(mcs, name_, bases, namespace, **options):
|
|
56
56
|
# Note: it's safe to pass options as keyword arguments as they are still type-checked by NodeOptions.
|
|
57
57
|
|
|
58
58
|
# We create this type, to then overload it with the dataclass attrs
|
infrahub/core/path.py
CHANGED
|
@@ -125,7 +125,7 @@ class SchemaPath(InfrahubPath):
|
|
|
125
125
|
if self.field_name:
|
|
126
126
|
identifier += f"/{self.field_name}"
|
|
127
127
|
|
|
128
|
-
if self.property_name and
|
|
128
|
+
if self.property_name and self.path_type != SchemaPathType.NODE:
|
|
129
129
|
identifier += f"/{self.property_name}"
|
|
130
130
|
|
|
131
131
|
return identifier
|
infrahub/core/protocols.py
CHANGED
|
@@ -403,12 +403,12 @@ class CoreGraphQLQueryGroup(CoreGroup):
|
|
|
403
403
|
|
|
404
404
|
|
|
405
405
|
class CoreGroupAction(CoreAction):
|
|
406
|
-
|
|
406
|
+
member_action: Dropdown
|
|
407
407
|
group: RelationshipManager
|
|
408
408
|
|
|
409
409
|
|
|
410
410
|
class CoreGroupTriggerRule(CoreTriggerRule):
|
|
411
|
-
|
|
411
|
+
member_update: Dropdown
|
|
412
412
|
group: RelationshipManager
|
|
413
413
|
|
|
414
414
|
|
|
@@ -440,7 +440,7 @@ class CoreNodeTriggerAttributeMatch(CoreNodeTriggerMatch):
|
|
|
440
440
|
|
|
441
441
|
class CoreNodeTriggerRelationshipMatch(CoreNodeTriggerMatch):
|
|
442
442
|
relationship_name: String
|
|
443
|
-
|
|
443
|
+
modification_type: Dropdown
|
|
444
444
|
peer: StringOptional
|
|
445
445
|
|
|
446
446
|
|
|
@@ -455,6 +455,7 @@ class CoreNumberPool(CoreResourcePool, LineageSource):
|
|
|
455
455
|
node_attribute: String
|
|
456
456
|
start_range: Integer
|
|
457
457
|
end_range: Integer
|
|
458
|
+
pool_type: Enum
|
|
458
459
|
|
|
459
460
|
|
|
460
461
|
class CoreObjectPermission(CoreBasePermission):
|
infrahub/core/query/node.py
CHANGED
|
@@ -1469,7 +1469,7 @@ class NodeGetHierarchyQuery(Query):
|
|
|
1469
1469
|
|
|
1470
1470
|
clean_filters = extract_field_filters(field_name=self.direction.value, filters=self.filters)
|
|
1471
1471
|
|
|
1472
|
-
if clean_filters and "id" in clean_filters or "ids" in clean_filters:
|
|
1472
|
+
if (clean_filters and "id" in clean_filters) or "ids" in clean_filters:
|
|
1473
1473
|
where_clause.append("peer.uuid IN $peer_ids")
|
|
1474
1474
|
self.params["peer_ids"] = clean_filters.get("ids", [])
|
|
1475
1475
|
if clean_filters.get("id", None):
|
|
@@ -676,7 +676,7 @@ class RelationshipGetPeerQuery(Query):
|
|
|
676
676
|
where_clause = ['all(r IN rels WHERE r.status = "active")']
|
|
677
677
|
clean_filters = extract_field_filters(field_name=self.schema.name, filters=self.filters)
|
|
678
678
|
|
|
679
|
-
if clean_filters and "id" in clean_filters or "ids" in clean_filters:
|
|
679
|
+
if (clean_filters and "id" in clean_filters) or "ids" in clean_filters:
|
|
680
680
|
where_clause.append("peer.uuid IN $peer_ids")
|
|
681
681
|
self.params["peer_ids"] = clean_filters.get("ids", [])
|
|
682
682
|
if clean_filters.get("id", None):
|
|
@@ -1035,7 +1035,7 @@ class RelationshipDeleteAllQuery(Query):
|
|
|
1035
1035
|
self.node_id = node_id
|
|
1036
1036
|
super().__init__(**kwargs)
|
|
1037
1037
|
|
|
1038
|
-
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
|
|
1038
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:
|
|
1039
1039
|
self.params["source_id"] = kwargs["node_id"]
|
|
1040
1040
|
self.params["branch"] = self.branch.name
|
|
1041
1041
|
|
|
@@ -3,28 +3,42 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from infrahub.core.query import Query, QueryType
|
|
6
|
+
from infrahub.exceptions import InitializationError
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
8
11
|
from infrahub.core.node.standard import StandardNode
|
|
9
12
|
from infrahub.database import InfrahubDatabase
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class StandardNodeQuery(Query):
|
|
13
16
|
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
self,
|
|
18
|
+
node: StandardNode | None = None,
|
|
19
|
+
node_id: UUID | None = None,
|
|
20
|
+
node_db_id: str | None = None,
|
|
21
|
+
**kwargs: Any,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._node = node
|
|
17
24
|
self.node_id = node_id
|
|
18
25
|
self.node_db_id = node_db_id
|
|
19
26
|
|
|
20
|
-
if not self.node_id and self.
|
|
27
|
+
if not self.node_id and self._node:
|
|
21
28
|
self.node_id = self.node.uuid
|
|
22
29
|
|
|
23
|
-
if not self.node_db_id and self.
|
|
30
|
+
if not self.node_db_id and self._node:
|
|
24
31
|
self.node_db_id = self.node.id
|
|
25
32
|
|
|
26
33
|
super().__init__(**kwargs)
|
|
27
34
|
|
|
35
|
+
@property
|
|
36
|
+
def node(self) -> StandardNode:
|
|
37
|
+
if self._node:
|
|
38
|
+
return self._node
|
|
39
|
+
|
|
40
|
+
raise InitializationError("The query is not initialized with a node")
|
|
41
|
+
|
|
28
42
|
|
|
29
43
|
class RootNodeCreateQuery(StandardNodeQuery):
|
|
30
44
|
name = "standard_node_create"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Mapping
|
|
5
|
+
|
|
6
|
+
from infrahub.core.constants import RelationshipCardinality
|
|
7
|
+
from infrahub.exceptions import ValidationError
|
|
8
|
+
|
|
9
|
+
from .interface import RelationshipManagerConstraintInterface
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from infrahub.core.branch import Branch
|
|
13
|
+
from infrahub.core.node import Node
|
|
14
|
+
from infrahub.core.schema import MainSchemaTypes, NonGenericSchemaTypes
|
|
15
|
+
from infrahub.database import InfrahubDatabase
|
|
16
|
+
|
|
17
|
+
from ..model import RelationshipManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class NodeToValidate:
|
|
22
|
+
uuid: str
|
|
23
|
+
relative_uuids: set[str]
|
|
24
|
+
schema: NonGenericSchemaTypes
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RelationshipPeerRelativesConstraint(RelationshipManagerConstraintInterface):
|
|
28
|
+
def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
|
|
29
|
+
self.db = db
|
|
30
|
+
self.branch = branch
|
|
31
|
+
|
|
32
|
+
async def _check_relationship_peers_relatives(
|
|
33
|
+
self,
|
|
34
|
+
relm: RelationshipManager,
|
|
35
|
+
node_schema: MainSchemaTypes,
|
|
36
|
+
peers: Mapping[str, Node],
|
|
37
|
+
relationship_name: str,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Validate that all peers of a given `relm` have the same set of relatives (aka peers) for the given `relationship_name`."""
|
|
40
|
+
nodes_to_validate: list[NodeToValidate] = []
|
|
41
|
+
|
|
42
|
+
for peer in peers.values():
|
|
43
|
+
peer_schema = peer.get_schema()
|
|
44
|
+
peer_relm: RelationshipManager = getattr(peer, relationship_name)
|
|
45
|
+
peer_relm_peers = await peer_relm.get_peers(db=self.db)
|
|
46
|
+
|
|
47
|
+
nodes_to_validate.append(
|
|
48
|
+
NodeToValidate(
|
|
49
|
+
uuid=peer.id, relative_uuids={n.id for n in peer_relm_peers.values()}, schema=peer_schema
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
relative_uuids = nodes_to_validate[0].relative_uuids
|
|
54
|
+
for node in nodes_to_validate[1:]:
|
|
55
|
+
if node.relative_uuids != relative_uuids:
|
|
56
|
+
raise ValidationError(
|
|
57
|
+
f"All the elements of the '{relm.name}' relationship on node {node.uuid} ({node_schema.kind}) must have the same set of peers "
|
|
58
|
+
f"for their '{node.schema.kind}.{relationship_name}' relationship"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None:
|
|
62
|
+
if relm.schema.cardinality != RelationshipCardinality.MANY or not relm.schema.common_relatives:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
peers = await relm.get_peers(db=self.db)
|
|
66
|
+
if not peers:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
for rel_name in relm.schema.common_relatives:
|
|
70
|
+
await self._check_relationship_peers_relatives(
|
|
71
|
+
relm=relm, node_schema=node_schema, peers=peers, relationship_name=rel_name
|
|
72
|
+
)
|
|
@@ -494,7 +494,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
494
494
|
peer_fields = {
|
|
495
495
|
key: value
|
|
496
496
|
for key, value in fields.items()
|
|
497
|
-
if not key.startswith(PREFIX_PROPERTY) or
|
|
497
|
+
if not key.startswith(PREFIX_PROPERTY) or key != "__typename"
|
|
498
498
|
}
|
|
499
499
|
rel_fields = {
|
|
500
500
|
key.replace(PREFIX_PROPERTY, ""): value
|
|
@@ -30,12 +30,6 @@ if TYPE_CHECKING:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def get_attribute_schema_class_for_kind(kind: str) -> type[AttributeSchema]:
|
|
33
|
-
attribute_schema_class_by_kind: dict[str, type[AttributeSchema]] = {
|
|
34
|
-
"NumberPool": NumberPoolSchema,
|
|
35
|
-
"Text": TextAttributeSchema,
|
|
36
|
-
"TextArea": TextAttributeSchema,
|
|
37
|
-
"Number": NumberAttributeSchema,
|
|
38
|
-
}
|
|
39
33
|
return attribute_schema_class_by_kind.get(kind, AttributeSchema)
|
|
40
34
|
|
|
41
35
|
|
|
@@ -43,6 +37,24 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
43
37
|
_sort_by: list[str] = ["name"]
|
|
44
38
|
_enum_class: type[enum.Enum] | None = None
|
|
45
39
|
|
|
40
|
+
@classmethod
|
|
41
|
+
def model_json_schema(cls, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
42
|
+
schema = super().model_json_schema(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
# Build conditional schema based on attribute_schema_class_by_kind mapping
|
|
45
|
+
# This override allows people using the Yaml language server to get the correct mappings
|
|
46
|
+
# for the parameters when selecting the appropriate kind
|
|
47
|
+
schema["allOf"] = []
|
|
48
|
+
for kind, schema_class in attribute_schema_class_by_kind.items():
|
|
49
|
+
schema["allOf"].append(
|
|
50
|
+
{
|
|
51
|
+
"if": {"properties": {"kind": {"const": kind}}},
|
|
52
|
+
"then": {"properties": {"parameters": {"$ref": f"#/definitions/{schema_class.__name__}"}}},
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return schema
|
|
57
|
+
|
|
46
58
|
@property
|
|
47
59
|
def is_attribute(self) -> bool:
|
|
48
60
|
return True
|
|
@@ -247,3 +259,11 @@ class NumberAttributeSchema(AttributeSchema):
|
|
|
247
259
|
description="Extra parameters specific to number attributes",
|
|
248
260
|
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
249
261
|
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
attribute_schema_class_by_kind: dict[str, type[AttributeSchema]] = {
|
|
265
|
+
"NumberPool": NumberPoolSchema,
|
|
266
|
+
"Text": TextAttributeSchema,
|
|
267
|
+
"TextArea": TextAttributeSchema,
|
|
268
|
+
"Number": NumberAttributeSchema,
|
|
269
|
+
}
|
|
@@ -386,7 +386,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
386
386
|
if not self.display_labels:
|
|
387
387
|
return None
|
|
388
388
|
|
|
389
|
-
fields: dict[str, str |
|
|
389
|
+
fields: dict[str, str | dict[str, None] | None] = {}
|
|
390
390
|
for item in self.display_labels:
|
|
391
391
|
fields.update(self.convert_path_to_graphql_fields(path=item))
|
|
392
392
|
return fields
|
|
@@ -401,7 +401,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
401
401
|
if not self.human_friendly_id:
|
|
402
402
|
return None
|
|
403
403
|
|
|
404
|
-
fields: dict[str, str |
|
|
404
|
+
fields: dict[str, str | dict[str, None] | None] = {}
|
|
405
405
|
for item in self.human_friendly_id:
|
|
406
406
|
fields.update(self.convert_path_to_graphql_fields(path=item))
|
|
407
407
|
return fields
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from infrahub.core.constants import (
|
|
2
2
|
BranchSupportType,
|
|
3
3
|
InfrahubKind,
|
|
4
|
+
NumberPoolType,
|
|
4
5
|
)
|
|
5
6
|
from infrahub.core.constants import RelationshipCardinality as Cardinality
|
|
6
7
|
from infrahub.core.constants import RelationshipKind as RelKind
|
|
@@ -186,5 +187,13 @@ core_number_pool = NodeSchema(
|
|
|
186
187
|
Attr(
|
|
187
188
|
name="end_range", kind="Number", optional=False, description="The end range for the pool", order_weight=6000
|
|
188
189
|
),
|
|
190
|
+
Attr(
|
|
191
|
+
name="pool_type",
|
|
192
|
+
kind="Text",
|
|
193
|
+
description="Defines how this number pool was created",
|
|
194
|
+
default_value=NumberPoolType.USER.value,
|
|
195
|
+
enum=NumberPoolType.available_types(),
|
|
196
|
+
read_only=True,
|
|
197
|
+
),
|
|
189
198
|
],
|
|
190
199
|
)
|
|
@@ -82,7 +82,7 @@ class SchemaAttribute(BaseModel):
|
|
|
82
82
|
|
|
83
83
|
@property
|
|
84
84
|
def optional_in_model(self) -> bool:
|
|
85
|
-
if self.optional and self.default_value is None and self.default_factory is None or self.default_to_none:
|
|
85
|
+
if (self.optional and self.default_value is None and self.default_factory is None) or self.default_to_none:
|
|
86
86
|
return True
|
|
87
87
|
|
|
88
88
|
return False
|
|
@@ -754,6 +754,14 @@ relationship_schema = SchemaNode(
|
|
|
754
754
|
optional=True,
|
|
755
755
|
extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
|
|
756
756
|
),
|
|
757
|
+
SchemaAttribute(
|
|
758
|
+
name="common_relatives",
|
|
759
|
+
kind="List",
|
|
760
|
+
internal_kind=str,
|
|
761
|
+
optional=True,
|
|
762
|
+
description="List of relationship names on the peer schema for which all objects must share the same set of peers.",
|
|
763
|
+
extra={"update": UpdateSupport.VALIDATE_CONSTRAINT},
|
|
764
|
+
),
|
|
757
765
|
SchemaAttribute(
|
|
758
766
|
name="order_weight",
|
|
759
767
|
kind="Number",
|
|
@@ -9,10 +9,10 @@ from pydantic import Field
|
|
|
9
9
|
from infrahub.core.constants import AllowOverrideType, BranchSupportType, HashableModelState
|
|
10
10
|
from infrahub.core.models import HashableModel
|
|
11
11
|
from infrahub.core.schema.attribute_parameters import (
|
|
12
|
-
AttributeParameters,
|
|
13
|
-
NumberAttributeParameters,
|
|
14
|
-
NumberPoolParameters,
|
|
15
|
-
TextAttributeParameters,
|
|
12
|
+
AttributeParameters,
|
|
13
|
+
NumberAttributeParameters,
|
|
14
|
+
NumberPoolParameters,
|
|
15
|
+
TextAttributeParameters,
|
|
16
16
|
)
|
|
17
17
|
from infrahub.core.schema.computed_attribute import ComputedAttribute # noqa: TC001
|
|
18
18
|
from infrahub.core.schema.dropdown import DropdownChoice # noqa: TC001
|
|
@@ -12,7 +12,7 @@ from infrahub.core.constants import (
|
|
|
12
12
|
RelationshipDeleteBehavior,
|
|
13
13
|
RelationshipDirection,
|
|
14
14
|
RelationshipKind,
|
|
15
|
-
)
|
|
15
|
+
)
|
|
16
16
|
from infrahub.core.models import HashableModel
|
|
17
17
|
|
|
18
18
|
|
|
@@ -73,6 +73,11 @@ class GeneratedRelationshipSchema(HashableModel):
|
|
|
73
73
|
description="Defines the maximum objects allowed on the other side of the relationship.",
|
|
74
74
|
json_schema_extra={"update": "validate_constraint"},
|
|
75
75
|
)
|
|
76
|
+
common_relatives: list[str] | None = Field(
|
|
77
|
+
default=None,
|
|
78
|
+
description="List of relationship names on the peer schema for which all objects must share the same set of peers.",
|
|
79
|
+
json_schema_extra={"update": "validate_constraint"},
|
|
80
|
+
)
|
|
76
81
|
order_weight: int | None = Field(
|
|
77
82
|
default=None,
|
|
78
83
|
description="Number used to order the relationship in the frontend (table and view). Lowest value will be ordered first.",
|
infrahub/core/schema/manager.py
CHANGED
|
@@ -471,7 +471,7 @@ class SchemaManager(NodeManager):
|
|
|
471
471
|
if diff_attributes:
|
|
472
472
|
for item in node.local_attributes:
|
|
473
473
|
# if item is in changed and has no ID, then it is being overridden from a generic and must be added
|
|
474
|
-
if item.name in diff_attributes.added or item.name in diff_attributes.changed and item.id is None:
|
|
474
|
+
if item.name in diff_attributes.added or (item.name in diff_attributes.changed and item.id is None):
|
|
475
475
|
created_item = await self.create_attribute_in_db(
|
|
476
476
|
schema=attribute_schema, item=item, branch=branch, db=db, parent=obj
|
|
477
477
|
)
|
|
@@ -491,7 +491,9 @@ class SchemaManager(NodeManager):
|
|
|
491
491
|
if diff_relationships:
|
|
492
492
|
for item in node.local_relationships:
|
|
493
493
|
# if item is in changed and has no ID, then it is being overridden from a generic and must be added
|
|
494
|
-
if item.name in diff_relationships.added or
|
|
494
|
+
if item.name in diff_relationships.added or (
|
|
495
|
+
item.name in diff_relationships.changed and item.id is None
|
|
496
|
+
):
|
|
495
497
|
created_rel = await self.create_relationship_in_db(
|
|
496
498
|
schema=relationship_schema, item=item, branch=branch, db=db, parent=obj
|
|
497
499
|
)
|
|
@@ -91,7 +91,7 @@ class SchemaBranch:
|
|
|
91
91
|
self.templates = data.get("templates", {})
|
|
92
92
|
|
|
93
93
|
@classmethod
|
|
94
|
-
def validate(cls, data: Any) -> Self:
|
|
94
|
+
def validate(cls, data: Any) -> Self:
|
|
95
95
|
if isinstance(data, cls):
|
|
96
96
|
return data
|
|
97
97
|
if isinstance(data, dict):
|
|
@@ -453,7 +453,7 @@ class SchemaBranch:
|
|
|
453
453
|
if isinstance(node, NodeSchema | ProfileSchema | TemplateSchema):
|
|
454
454
|
return node.generate_fields_for_display_label()
|
|
455
455
|
|
|
456
|
-
fields: dict[str, str |
|
|
456
|
+
fields: dict[str, str | dict[str, None] | None] = {}
|
|
457
457
|
if isinstance(node, GenericSchema):
|
|
458
458
|
for child_node_name in node.used_by:
|
|
459
459
|
child_node = self.get(name=child_node_name, duplicate=False)
|
|
@@ -997,6 +997,13 @@ class SchemaBranch:
|
|
|
997
997
|
raise ValueError(
|
|
998
998
|
f"{node.kind}: Relationship {rel.name!r} is referring an invalid peer {rel.peer!r}"
|
|
999
999
|
) from None
|
|
1000
|
+
if rel.common_relatives:
|
|
1001
|
+
peer_schema = self.get(name=rel.peer, duplicate=False)
|
|
1002
|
+
for common_relatives_rel_name in rel.common_relatives:
|
|
1003
|
+
if common_relatives_rel_name not in peer_schema.relationship_names:
|
|
1004
|
+
raise ValueError(
|
|
1005
|
+
f"{node.kind}: Relationship {rel.name!r} set 'common_relatives' with invalid relationship from '{rel.peer}'"
|
|
1006
|
+
) from None
|
|
1000
1007
|
|
|
1001
1008
|
def validate_attribute_parameters(self) -> None:
|
|
1002
1009
|
for name in self.generics.keys():
|
|
@@ -2015,7 +2022,11 @@ class SchemaBranch:
|
|
|
2015
2022
|
)
|
|
2016
2023
|
|
|
2017
2024
|
parent_hfid = f"{relationship.name}__template_name__value"
|
|
2018
|
-
if
|
|
2025
|
+
if (
|
|
2026
|
+
not isinstance(template_schema, GenericSchema)
|
|
2027
|
+
and relationship.kind == RelationshipKind.PARENT
|
|
2028
|
+
and parent_hfid not in template_schema.human_friendly_id
|
|
2029
|
+
):
|
|
2019
2030
|
template_schema.human_friendly_id = [parent_hfid] + template_schema.human_friendly_id
|
|
2020
2031
|
template_schema.uniqueness_constraints[0].append(relationship.name)
|
|
2021
2032
|
|
|
@@ -2051,7 +2062,6 @@ class SchemaBranch:
|
|
|
2051
2062
|
include_in_menu=False,
|
|
2052
2063
|
display_labels=["template_name__value"],
|
|
2053
2064
|
human_friendly_id=["template_name__value"],
|
|
2054
|
-
uniqueness_constraints=[["template_name__value"]],
|
|
2055
2065
|
attributes=[template_name_attr],
|
|
2056
2066
|
)
|
|
2057
2067
|
|
|
@@ -2070,7 +2080,6 @@ class SchemaBranch:
|
|
|
2070
2080
|
human_friendly_id=["template_name__value"],
|
|
2071
2081
|
uniqueness_constraints=[["template_name__value"]],
|
|
2072
2082
|
inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind],
|
|
2073
|
-
default_filter="template_name__value",
|
|
2074
2083
|
attributes=[template_name_attr],
|
|
2075
2084
|
relationships=[
|
|
2076
2085
|
RelationshipSchema(
|
|
@@ -9,7 +9,7 @@ from prefect.logging import get_run_logger
|
|
|
9
9
|
|
|
10
10
|
from infrahub.core.branch import Branch # noqa: TC001
|
|
11
11
|
from infrahub.core.path import SchemaPath # noqa: TC001
|
|
12
|
-
from infrahub.core.schema import GenericSchema, NodeSchema
|
|
12
|
+
from infrahub.core.schema import GenericSchema, NodeSchema
|
|
13
13
|
from infrahub.core.validators.aggregated_checker import AggregatedConstraintChecker
|
|
14
14
|
from infrahub.core.validators.model import (
|
|
15
15
|
SchemaConstraintValidatorRequest,
|
infrahub/database/__init__.py
CHANGED
|
@@ -339,7 +339,7 @@ class InfrahubDatabase:
|
|
|
339
339
|
CONNECTION_POOL_USAGE.labels(self._driver._pool.address).set(float(connpool_usage))
|
|
340
340
|
|
|
341
341
|
if config.SETTINGS.database.max_concurrent_queries:
|
|
342
|
-
while connpool_usage > config.SETTINGS.database.max_concurrent_queries:
|
|
342
|
+
while connpool_usage > config.SETTINGS.database.max_concurrent_queries:
|
|
343
343
|
await asyncio.sleep(config.SETTINGS.database.max_concurrent_queries_delay)
|
|
344
344
|
connpool_usage = self._driver._pool.in_use_connection_count(self._driver._pool.address)
|
|
345
345
|
|