infrahub-server 1.2.3__py3-none-any.whl → 1.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/cli/git_agent.py +4 -10
- infrahub/config.py +32 -0
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/constraint/node/runner.py +6 -5
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +68 -70
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +26 -0
- infrahub/core/migrations/schema/node_attribute_remove.py +16 -2
- infrahub/core/models.py +1 -1
- infrahub/core/node/constraints/grouped_uniqueness.py +6 -1
- infrahub/core/registry.py +18 -0
- infrahub/core/schema/basenode_schema.py +21 -1
- infrahub/core/schema/definitions/internal.py +2 -1
- infrahub/core/schema/generated/base_node_schema.py +1 -1
- infrahub/core/schema/manager.py +21 -0
- infrahub/core/schema/schema_branch.py +3 -4
- infrahub/database/__init__.py +10 -0
- infrahub/events/group_action.py +6 -1
- infrahub/events/node_action.py +5 -1
- infrahub/graphql/mutations/main.py +10 -12
- infrahub/message_bus/messages/__init__.py +0 -2
- infrahub/message_bus/operations/__init__.py +0 -1
- infrahub/message_bus/operations/event/__init__.py +2 -2
- infrahub/server.py +6 -11
- infrahub/services/adapters/cache/__init__.py +17 -0
- infrahub/services/adapters/cache/redis.py +11 -1
- infrahub/services/adapters/message_bus/__init__.py +20 -0
- infrahub/services/component.py +1 -2
- infrahub/tasks/registry.py +3 -7
- infrahub/workers/infrahub_async.py +4 -10
- infrahub_sdk/schema/__init__.py +10 -1
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/METADATA +1 -1
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/RECORD +39 -40
- infrahub_testcontainers/container.py +4 -0
- infrahub_testcontainers/helpers.py +1 -1
- infrahub/message_bus/messages/event_worker_newprimaryapi.py +0 -9
- infrahub/message_bus/operations/event/worker.py +0 -9
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/WHEEL +0 -0
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.4.dist-info}/entry_points.txt +0 -0
infrahub/cli/git_agent.py
CHANGED
|
@@ -19,10 +19,8 @@ from infrahub.git import initialize_repositories_directory
|
|
|
19
19
|
from infrahub.lock import initialize_lock
|
|
20
20
|
from infrahub.log import get_logger
|
|
21
21
|
from infrahub.services import InfrahubServices
|
|
22
|
-
from infrahub.services.adapters.cache
|
|
23
|
-
from infrahub.services.adapters.
|
|
24
|
-
from infrahub.services.adapters.message_bus.nats import NATSMessageBus
|
|
25
|
-
from infrahub.services.adapters.message_bus.rabbitmq import RabbitMQMessageBus
|
|
22
|
+
from infrahub.services.adapters.cache import InfrahubCache
|
|
23
|
+
from infrahub.services.adapters.message_bus import InfrahubMessageBus
|
|
26
24
|
from infrahub.services.adapters.workflow.local import WorkflowLocalExecution
|
|
27
25
|
from infrahub.services.adapters.workflow.worker import WorkflowWorkerExecution
|
|
28
26
|
from infrahub.trace import configure_trace
|
|
@@ -121,13 +119,9 @@ async def start(
|
|
|
121
119
|
|
|
122
120
|
component_type = ComponentType.GIT_AGENT
|
|
123
121
|
message_bus = config.OVERRIDE.message_bus or (
|
|
124
|
-
await
|
|
125
|
-
if config.SETTINGS.broker.driver == config.BrokerDriver.NATS
|
|
126
|
-
else await RabbitMQMessageBus.new(component_type=component_type)
|
|
127
|
-
)
|
|
128
|
-
cache = config.OVERRIDE.cache or (
|
|
129
|
-
await NATSCache.new() if config.SETTINGS.cache.driver == config.CacheDriver.NATS else RedisCache()
|
|
122
|
+
await InfrahubMessageBus.new_from_driver(component_type=component_type, driver=config.SETTINGS.broker.driver)
|
|
130
123
|
)
|
|
124
|
+
cache = config.OVERRIDE.cache or (await InfrahubCache.new_from_driver(driver=config.SETTINGS.cache.driver))
|
|
131
125
|
|
|
132
126
|
service = await InfrahubServices.new(
|
|
133
127
|
cache=cache,
|
infrahub/config.py
CHANGED
|
@@ -115,11 +115,43 @@ class BrokerDriver(str, Enum):
|
|
|
115
115
|
RabbitMQ = "rabbitmq"
|
|
116
116
|
NATS = "nats"
|
|
117
117
|
|
|
118
|
+
@property
|
|
119
|
+
def driver_module_path(self) -> str:
|
|
120
|
+
match self:
|
|
121
|
+
case BrokerDriver.NATS:
|
|
122
|
+
return "infrahub.services.adapters.message_bus.nats"
|
|
123
|
+
case BrokerDriver.RabbitMQ:
|
|
124
|
+
return "infrahub.services.adapters.message_bus.rabbitmq"
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def driver_class_name(self) -> str:
|
|
128
|
+
match self:
|
|
129
|
+
case BrokerDriver.NATS:
|
|
130
|
+
return "NATSMessageBus"
|
|
131
|
+
case BrokerDriver.RabbitMQ:
|
|
132
|
+
return "RabbitMQMessageBus"
|
|
133
|
+
|
|
118
134
|
|
|
119
135
|
class CacheDriver(str, Enum):
|
|
120
136
|
Redis = "redis"
|
|
121
137
|
NATS = "nats"
|
|
122
138
|
|
|
139
|
+
@property
|
|
140
|
+
def driver_module_path(self) -> str:
|
|
141
|
+
match self:
|
|
142
|
+
case CacheDriver.NATS:
|
|
143
|
+
return "infrahub.services.adapters.cache.nats"
|
|
144
|
+
case CacheDriver.Redis:
|
|
145
|
+
return "infrahub.services.adapters.cache.redis"
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def driver_class_name(self) -> str:
|
|
149
|
+
match self:
|
|
150
|
+
case CacheDriver.NATS:
|
|
151
|
+
return "NATSCache"
|
|
152
|
+
case CacheDriver.Redis:
|
|
153
|
+
return "RedisCache"
|
|
154
|
+
|
|
123
155
|
|
|
124
156
|
class WorkflowDriver(str, Enum):
|
|
125
157
|
LOCAL = "local"
|
|
@@ -348,6 +348,7 @@ RESTRICTED_NAMESPACES: list[str] = [
|
|
|
348
348
|
NODE_NAME_REGEX = r"^[A-Z][a-zA-Z0-9]+$"
|
|
349
349
|
DEFAULT_NAME_MIN_LENGTH = 2
|
|
350
350
|
NAME_REGEX = r"^[a-z0-9\_]+$"
|
|
351
|
+
NAME_REGEX_OR_EMPTY = r"^[a-z0-9\_]*$"
|
|
351
352
|
DEFAULT_DESCRIPTION_LENGTH = 128
|
|
352
353
|
|
|
353
354
|
DEFAULT_NAME_MAX_LENGTH = 32
|
|
@@ -23,10 +23,15 @@ class NodeConstraintRunner:
|
|
|
23
23
|
self.uniqueness_constraint = uniqueness_constraint
|
|
24
24
|
self.relationship_manager_constraints = relationship_manager_constraints
|
|
25
25
|
|
|
26
|
-
async def check(
|
|
26
|
+
async def check(
|
|
27
|
+
self, node: Node, field_filters: list[str] | None = None, skip_uniqueness_check: bool = False
|
|
28
|
+
) -> None:
|
|
27
29
|
async with self.db.start_session() as db:
|
|
28
30
|
await node.resolve_relationships(db=db)
|
|
29
31
|
|
|
32
|
+
if not skip_uniqueness_check:
|
|
33
|
+
await self.uniqueness_constraint.check(node, filters=field_filters)
|
|
34
|
+
|
|
30
35
|
for relationship_name in node.get_schema().relationship_names:
|
|
31
36
|
if field_filters and relationship_name not in field_filters:
|
|
32
37
|
continue
|
|
@@ -34,7 +39,3 @@ class NodeConstraintRunner:
|
|
|
34
39
|
await relationship_manager.fetch_relationship_ids(db=db, force_refresh=True)
|
|
35
40
|
for relationship_constraint in self.relationship_manager_constraints:
|
|
36
41
|
await relationship_constraint.check(relm=relationship_manager, node_schema=node.get_schema())
|
|
37
|
-
|
|
38
|
-
# If HFID constraint is the only constraint violated, all other constraints need to have ran before,
|
|
39
|
-
# as it means there is an existing node that we might want to update in the case of an upsert
|
|
40
|
-
await self.uniqueness_constraint.check(node, filters=field_filters)
|
infrahub/core/graph/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
GRAPH_VERSION =
|
|
1
|
+
GRAPH_VERSION = 25
|
|
@@ -26,6 +26,7 @@ from .m021_missing_hierarchy_merge import Migration021
|
|
|
26
26
|
from .m022_add_generate_template_attr import Migration022
|
|
27
27
|
from .m023_deduplicate_cardinality_one_relationships import Migration023
|
|
28
28
|
from .m024_missing_hierarchy_backfill import Migration024
|
|
29
|
+
from .m025_uniqueness_nulls import Migration025
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
from infrahub.core.root import Root
|
|
@@ -57,6 +58,7 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
|
|
|
57
58
|
Migration022,
|
|
58
59
|
Migration023,
|
|
59
60
|
Migration024,
|
|
61
|
+
Migration025,
|
|
60
62
|
]
|
|
61
63
|
|
|
62
64
|
|
|
@@ -21,81 +21,79 @@ if TYPE_CHECKING:
|
|
|
21
21
|
log = get_logger()
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
async def validate_nulls_in_uniqueness_constraints(db: InfrahubDatabase) -> MigrationResult:
|
|
25
|
+
"""
|
|
26
|
+
Validate any schema that include optional attributes in the uniqueness constraints
|
|
27
|
+
|
|
28
|
+
An update to uniqueness constraint validation now handles NULL values as unique instead of ignoring them
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
default_branch = registry.get_branch_from_registry()
|
|
32
|
+
build_component_registry()
|
|
33
|
+
component_registry = get_component_registry()
|
|
34
|
+
uniqueness_checker = await component_registry.get_component(UniquenessChecker, db=db, branch=default_branch)
|
|
35
|
+
non_unique_nodes_by_kind: dict[str, list[NonUniqueNode]] = defaultdict(list)
|
|
36
|
+
|
|
37
|
+
manager = SchemaManager()
|
|
38
|
+
registry.schema = manager
|
|
39
|
+
internal_schema_root = SchemaRoot(**internal_schema)
|
|
40
|
+
manager.register_schema(schema=internal_schema_root)
|
|
41
|
+
schema_branch = await manager.load_schema_from_db(db=db, branch=default_branch)
|
|
42
|
+
manager.set_schema_branch(name=default_branch.name, schema=schema_branch)
|
|
43
|
+
|
|
44
|
+
for schema_kind in schema_branch.node_names + schema_branch.generic_names_without_templates:
|
|
45
|
+
schema = schema_branch.get(name=schema_kind, duplicate=False)
|
|
46
|
+
if not isinstance(schema, NodeSchema | GenericSchema):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
uniqueness_constraint_paths = schema.get_unique_constraint_schema_attribute_paths(schema_branch=schema_branch)
|
|
50
|
+
includes_optional_attr: bool = False
|
|
51
|
+
|
|
52
|
+
for uniqueness_constraint_path in uniqueness_constraint_paths:
|
|
53
|
+
for schema_attribute_path in uniqueness_constraint_path.attributes_paths:
|
|
54
|
+
if schema_attribute_path.attribute_schema and schema_attribute_path.attribute_schema.optional is True:
|
|
55
|
+
includes_optional_attr = True
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
if not includes_optional_attr:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
non_unique_nodes = await uniqueness_checker.check_one_schema(schema=schema)
|
|
62
|
+
if non_unique_nodes:
|
|
63
|
+
non_unique_nodes_by_kind[schema_kind] = non_unique_nodes
|
|
64
|
+
|
|
65
|
+
if not non_unique_nodes_by_kind:
|
|
66
|
+
return MigrationResult()
|
|
67
|
+
|
|
68
|
+
error_strings = []
|
|
69
|
+
for schema_kind, non_unique_nodes in non_unique_nodes_by_kind.items():
|
|
70
|
+
display_label_map = await get_display_labels_per_kind(
|
|
71
|
+
db=db, kind=schema_kind, branch_name=default_branch.name, ids=[nun.node_id for nun in non_unique_nodes]
|
|
72
|
+
)
|
|
73
|
+
for non_unique_node in non_unique_nodes:
|
|
74
|
+
display_label = display_label_map.get(non_unique_node.node_id)
|
|
75
|
+
error_str = f"{display_label or ''}({non_unique_node.node_schema.kind} / {non_unique_node.node_id})"
|
|
76
|
+
error_str += " violates uniqueness constraints for the following attributes: "
|
|
77
|
+
attr_values = [
|
|
78
|
+
f"{attr.attribute_name}={attr.attribute_value}" for attr in non_unique_node.non_unique_attributes
|
|
79
|
+
]
|
|
80
|
+
error_str += ", ".join(attr_values)
|
|
81
|
+
error_strings.append(error_str)
|
|
82
|
+
if error_strings:
|
|
83
|
+
error_str = "For the following nodes, you must update the uniqueness_constraints on the schema of the node"
|
|
84
|
+
error_str += " to remove the attribute(s) with NULL values or update the data on the nodes to be unique"
|
|
85
|
+
error_str += " now that NULL values are considered during uniqueness validation"
|
|
86
|
+
return MigrationResult(errors=[error_str] + error_strings)
|
|
87
|
+
return MigrationResult()
|
|
88
|
+
|
|
89
|
+
|
|
24
90
|
class Migration018(InternalSchemaMigration):
|
|
25
91
|
name: str = "018_validate_nulls_in_uniqueness_constraints"
|
|
26
92
|
minimum_version: int = 17
|
|
27
93
|
migrations: Sequence[SchemaMigration] = []
|
|
28
94
|
|
|
29
95
|
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return result
|
|
96
|
+
return MigrationResult()
|
|
33
97
|
|
|
34
98
|
async def execute(self, db: InfrahubDatabase) -> MigrationResult:
|
|
35
|
-
|
|
36
|
-
Validate any schema that include optional attributes in the uniqueness constraints
|
|
37
|
-
|
|
38
|
-
An update to uniqueness constraint validation now handles NULL values as unique instead of ignoring them
|
|
39
|
-
"""
|
|
40
|
-
default_branch = registry.get_branch_from_registry()
|
|
41
|
-
build_component_registry()
|
|
42
|
-
component_registry = get_component_registry()
|
|
43
|
-
uniqueness_checker = await component_registry.get_component(UniquenessChecker, db=db, branch=default_branch)
|
|
44
|
-
non_unique_nodes_by_kind: dict[str, list[NonUniqueNode]] = defaultdict(list)
|
|
45
|
-
|
|
46
|
-
manager = SchemaManager()
|
|
47
|
-
registry.schema = manager
|
|
48
|
-
internal_schema_root = SchemaRoot(**internal_schema)
|
|
49
|
-
manager.register_schema(schema=internal_schema_root)
|
|
50
|
-
schema_branch = await manager.load_schema_from_db(db=db, branch=default_branch)
|
|
51
|
-
manager.set_schema_branch(name=default_branch.name, schema=schema_branch)
|
|
52
|
-
|
|
53
|
-
for schema_kind in schema_branch.node_names + schema_branch.generic_names_without_templates:
|
|
54
|
-
schema = schema_branch.get(name=schema_kind, duplicate=False)
|
|
55
|
-
if not isinstance(schema, NodeSchema | GenericSchema):
|
|
56
|
-
continue
|
|
57
|
-
|
|
58
|
-
uniqueness_constraint_paths = schema.get_unique_constraint_schema_attribute_paths(
|
|
59
|
-
schema_branch=schema_branch
|
|
60
|
-
)
|
|
61
|
-
includes_optional_attr: bool = False
|
|
62
|
-
|
|
63
|
-
for uniqueness_constraint_path in uniqueness_constraint_paths:
|
|
64
|
-
for schema_attribute_path in uniqueness_constraint_path.attributes_paths:
|
|
65
|
-
if (
|
|
66
|
-
schema_attribute_path.attribute_schema
|
|
67
|
-
and schema_attribute_path.attribute_schema.optional is True
|
|
68
|
-
):
|
|
69
|
-
includes_optional_attr = True
|
|
70
|
-
break
|
|
71
|
-
|
|
72
|
-
if not includes_optional_attr:
|
|
73
|
-
continue
|
|
74
|
-
|
|
75
|
-
non_unique_nodes = await uniqueness_checker.check_one_schema(schema=schema)
|
|
76
|
-
if non_unique_nodes:
|
|
77
|
-
non_unique_nodes_by_kind[schema_kind] = non_unique_nodes
|
|
78
|
-
|
|
79
|
-
if not non_unique_nodes_by_kind:
|
|
80
|
-
return MigrationResult()
|
|
81
|
-
|
|
82
|
-
error_strings = []
|
|
83
|
-
for schema_kind, non_unique_nodes in non_unique_nodes_by_kind.items():
|
|
84
|
-
display_label_map = await get_display_labels_per_kind(
|
|
85
|
-
db=db, kind=schema_kind, branch_name=default_branch.name, ids=[nun.node_id for nun in non_unique_nodes]
|
|
86
|
-
)
|
|
87
|
-
for non_unique_node in non_unique_nodes:
|
|
88
|
-
display_label = display_label_map.get(non_unique_node.node_id)
|
|
89
|
-
error_str = f"{display_label or ''}({non_unique_node.node_schema.kind} / {non_unique_node.node_id})"
|
|
90
|
-
error_str += " violates uniqueness constraints for the following attributes: "
|
|
91
|
-
attr_values = [
|
|
92
|
-
f"{attr.attribute_name}={attr.attribute_value}" for attr in non_unique_node.non_unique_attributes
|
|
93
|
-
]
|
|
94
|
-
error_str += ", ".join(attr_values)
|
|
95
|
-
error_strings.append(error_str)
|
|
96
|
-
if error_strings:
|
|
97
|
-
error_str = "For the following nodes, you must update the uniqueness_constraints on the schema of the node"
|
|
98
|
-
error_str += " to remove the attribute(s) with NULL values or update the data on the nodes to be unique"
|
|
99
|
-
error_str += " now that NULL values are considered during uniqueness validation"
|
|
100
|
-
return MigrationResult(errors=[error_str] + error_strings)
|
|
101
|
-
return MigrationResult()
|
|
99
|
+
return await validate_nulls_in_uniqueness_constraints(db=db)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Sequence
|
|
4
|
+
|
|
5
|
+
from infrahub.core.migrations.shared import MigrationResult
|
|
6
|
+
from infrahub.log import get_logger
|
|
7
|
+
|
|
8
|
+
from ..shared import InternalSchemaMigration, SchemaMigration
|
|
9
|
+
from .m018_uniqueness_nulls import validate_nulls_in_uniqueness_constraints
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from infrahub.database import InfrahubDatabase
|
|
13
|
+
|
|
14
|
+
log = get_logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Migration025(InternalSchemaMigration):
|
|
18
|
+
name: str = "025_validate_nulls_in_uniqueness_constraints"
|
|
19
|
+
minimum_version: int = 24
|
|
20
|
+
migrations: Sequence[SchemaMigration] = []
|
|
21
|
+
|
|
22
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
23
|
+
return MigrationResult()
|
|
24
|
+
|
|
25
|
+
async def execute(self, db: InfrahubDatabase) -> MigrationResult:
|
|
26
|
+
return await validate_nulls_in_uniqueness_constraints(db=db)
|
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Sequence
|
|
|
4
4
|
|
|
5
5
|
from infrahub.core.constants import RelationshipStatus
|
|
6
6
|
from infrahub.core.graph.schema import GraphAttributeRelationships
|
|
7
|
+
from infrahub.core.schema.generic_schema import GenericSchema
|
|
7
8
|
|
|
8
9
|
from ..query import AttributeMigrationQuery
|
|
9
10
|
from ..shared import AttributeSchemaMigration
|
|
@@ -22,8 +23,20 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
|
|
|
22
23
|
branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
|
|
23
24
|
self.params.update(branch_params)
|
|
24
25
|
|
|
26
|
+
attr_name = self.migration.schema_path.field_name
|
|
27
|
+
kinds_to_ignore = []
|
|
28
|
+
if isinstance(self.migration.new_node_schema, GenericSchema) and attr_name is not None:
|
|
29
|
+
for inheriting_schema_kind in self.migration.new_node_schema.used_by:
|
|
30
|
+
node_schema = db.schema.get_node_schema(
|
|
31
|
+
name=inheriting_schema_kind, branch=self.branch, duplicate=False
|
|
32
|
+
)
|
|
33
|
+
attr_schema = node_schema.get_attribute_or_none(name=attr_name)
|
|
34
|
+
if attr_schema and not attr_schema.inherited:
|
|
35
|
+
kinds_to_ignore.append(inheriting_schema_kind)
|
|
36
|
+
|
|
25
37
|
self.params["node_kind"] = self.migration.new_schema.kind
|
|
26
|
-
self.params["
|
|
38
|
+
self.params["kinds_to_ignore"] = kinds_to_ignore
|
|
39
|
+
self.params["attr_name"] = attr_name
|
|
27
40
|
self.params["current_time"] = self.at.to_string()
|
|
28
41
|
self.params["branch_name"] = self.branch.name
|
|
29
42
|
self.params["branch_support"] = self.migration.previous_attribute_schema.get_branch().value
|
|
@@ -60,7 +73,8 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
|
|
|
60
73
|
query = """
|
|
61
74
|
// Find all the active nodes
|
|
62
75
|
MATCH (node:%(node_kind)s)
|
|
63
|
-
WHERE
|
|
76
|
+
WHERE (size($kinds_to_ignore) = 0 OR NOT any(l IN labels(node) WHERE l IN $kinds_to_ignore))
|
|
77
|
+
AND exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name }))
|
|
64
78
|
CALL {
|
|
65
79
|
WITH node
|
|
66
80
|
MATCH (root:Root)<-[r:IS_PART_OF]-(node)
|
infrahub/core/models.py
CHANGED
|
@@ -529,7 +529,7 @@ class HashableModel(BaseModel):
|
|
|
529
529
|
|
|
530
530
|
return new_list
|
|
531
531
|
|
|
532
|
-
def update(self, other:
|
|
532
|
+
def update(self, other: HashableModel) -> Self:
|
|
533
533
|
"""Update the current object with the new value from the new one if they are defined.
|
|
534
534
|
|
|
535
535
|
Currently this method works for the following type of fields
|
|
@@ -220,8 +220,13 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
|
|
|
220
220
|
|
|
221
221
|
violations = []
|
|
222
222
|
for schema in schemas_to_check:
|
|
223
|
+
schema_filters = list(filters) if filters is not None else []
|
|
224
|
+
for attr_schema in schema.attributes:
|
|
225
|
+
if attr_schema.optional and attr_schema.unique and attr_schema.name not in schema_filters:
|
|
226
|
+
schema_filters.append(attr_schema.name)
|
|
227
|
+
|
|
223
228
|
schema_violations = await self._get_single_schema_violations(
|
|
224
|
-
node=node, node_schema=schema, at=at, filters=
|
|
229
|
+
node=node, node_schema=schema, at=at, filters=schema_filters
|
|
225
230
|
)
|
|
226
231
|
violations.extend(schema_violations)
|
|
227
232
|
|
infrahub/core/registry.py
CHANGED
|
@@ -220,5 +220,23 @@ class Registry:
|
|
|
220
220
|
and branch.active_schema_hash.main != default_branch.active_schema_hash.main
|
|
221
221
|
]
|
|
222
222
|
|
|
223
|
+
async def purge_inactive_branches(
|
|
224
|
+
self, db: InfrahubDatabase, active_branches: list[Branch] | None = None
|
|
225
|
+
) -> list[str]:
|
|
226
|
+
"""Return a list of branches that were purged from the registry."""
|
|
227
|
+
active_branches = active_branches or await self.branch_object.get_list(db=db)
|
|
228
|
+
active_branch_names = [branch.name for branch in active_branches]
|
|
229
|
+
purged_branches: set[str] = set()
|
|
230
|
+
|
|
231
|
+
for branch_name in list(registry.branch.keys()):
|
|
232
|
+
if branch_name not in active_branch_names:
|
|
233
|
+
del registry.branch[branch_name]
|
|
234
|
+
purged_branches.add(branch_name)
|
|
235
|
+
|
|
236
|
+
purged_branches.update(self.schema.purge_inactive_branches(active_branches=active_branch_names))
|
|
237
|
+
purged_branches.update(db.purge_inactive_schemas(active_branches=active_branch_names))
|
|
238
|
+
|
|
239
|
+
return sorted(purged_branches)
|
|
240
|
+
|
|
223
241
|
|
|
224
242
|
registry = Registry()
|
|
@@ -11,7 +11,7 @@ from infrahub_sdk.utils import compare_lists, intersection
|
|
|
11
11
|
from pydantic import field_validator
|
|
12
12
|
|
|
13
13
|
from infrahub.core.constants import RelationshipCardinality, RelationshipKind
|
|
14
|
-
from infrahub.core.models import HashableModelDiff
|
|
14
|
+
from infrahub.core.models import HashableModel, HashableModelDiff
|
|
15
15
|
|
|
16
16
|
from .attribute_schema import AttributeSchema
|
|
17
17
|
from .generated.base_node_schema import GeneratedBaseNodeSchema
|
|
@@ -27,6 +27,16 @@ if TYPE_CHECKING:
|
|
|
27
27
|
NODE_METADATA_ATTRIBUTES = ["_source", "_owner"]
|
|
28
28
|
INHERITED = "INHERITED"
|
|
29
29
|
|
|
30
|
+
OPTIONAL_TEXT_FIELDS = [
|
|
31
|
+
"default_filter",
|
|
32
|
+
"description",
|
|
33
|
+
"label",
|
|
34
|
+
"menu_placement",
|
|
35
|
+
"documentation",
|
|
36
|
+
"parent",
|
|
37
|
+
"children",
|
|
38
|
+
]
|
|
39
|
+
|
|
30
40
|
|
|
31
41
|
class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
32
42
|
_exclude_from_hash: list[str] = ["attributes", "relationships"]
|
|
@@ -480,6 +490,16 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
480
490
|
return UniquenessConstraintType.SUBSET_OF_HFID
|
|
481
491
|
return UniquenessConstraintType.STANDARD
|
|
482
492
|
|
|
493
|
+
def update(self, other: HashableModel) -> Self:
|
|
494
|
+
super().update(other=other)
|
|
495
|
+
|
|
496
|
+
# Allow to specify empty string to remove existing fields values
|
|
497
|
+
for field_name in OPTIONAL_TEXT_FIELDS:
|
|
498
|
+
if getattr(other, field_name, None) == "": # noqa: PLC1901
|
|
499
|
+
setattr(self, field_name, None)
|
|
500
|
+
|
|
501
|
+
return self
|
|
502
|
+
|
|
483
503
|
|
|
484
504
|
@dataclass
|
|
485
505
|
class SchemaUniquenessConstraintPath:
|
|
@@ -18,6 +18,7 @@ from infrahub.core.constants import (
|
|
|
18
18
|
DEFAULT_NAME_MIN_LENGTH,
|
|
19
19
|
DEFAULT_REL_IDENTIFIER_LENGTH,
|
|
20
20
|
NAME_REGEX,
|
|
21
|
+
NAME_REGEX_OR_EMPTY,
|
|
21
22
|
NAMESPACE_REGEX,
|
|
22
23
|
NODE_KIND_REGEX,
|
|
23
24
|
NODE_NAME_REGEX,
|
|
@@ -269,7 +270,7 @@ base_node_schema = SchemaNode(
|
|
|
269
270
|
SchemaAttribute(
|
|
270
271
|
name="default_filter",
|
|
271
272
|
kind="Text",
|
|
272
|
-
regex=str(
|
|
273
|
+
regex=str(NAME_REGEX_OR_EMPTY),
|
|
273
274
|
description="Default filter used to search for a node in addition to its ID. (deprecated: please use human_friendly_id instead)",
|
|
274
275
|
optional=True,
|
|
275
276
|
extra={"update": UpdateSupport.ALLOWED},
|
|
@@ -50,7 +50,7 @@ class GeneratedBaseNodeSchema(HashableModel):
|
|
|
50
50
|
default_filter: str | None = Field(
|
|
51
51
|
default=None,
|
|
52
52
|
description="Default filter used to search for a node in addition to its ID. (deprecated: please use human_friendly_id instead)",
|
|
53
|
-
pattern=r"^[a-z0-9\_]
|
|
53
|
+
pattern=r"^[a-z0-9\_]*$",
|
|
54
54
|
json_schema_extra={"update": "allowed"},
|
|
55
55
|
)
|
|
56
56
|
human_friendly_id: list[str] | None = Field(
|
infrahub/core/schema/manager.py
CHANGED
|
@@ -744,3 +744,24 @@ class SchemaManager(NodeManager):
|
|
|
744
744
|
"""Convert a schema_node object loaded from the database into GenericSchema object."""
|
|
745
745
|
node_data = await cls._prepare_node_data(schema_node=schema_node, db=db)
|
|
746
746
|
return GenericSchema(**node_data)
|
|
747
|
+
|
|
748
|
+
def purge_inactive_branches(self, active_branches: list[str]) -> list[str]:
|
|
749
|
+
"""Return non active branches that were purged."""
|
|
750
|
+
|
|
751
|
+
hashes_to_keep: set[str] = set()
|
|
752
|
+
for active_branch in active_branches:
|
|
753
|
+
if branch := self._branches.get(active_branch):
|
|
754
|
+
nodes = branch.get_all(include_internal=True, duplicate=False)
|
|
755
|
+
hashes_to_keep.update([node.get_hash() for node in nodes.values()])
|
|
756
|
+
|
|
757
|
+
removed_branches: list[str] = []
|
|
758
|
+
for branch_name in list(self._branches.keys()):
|
|
759
|
+
if branch_name not in active_branches:
|
|
760
|
+
del self._branches[branch_name]
|
|
761
|
+
removed_branches.append(branch_name)
|
|
762
|
+
|
|
763
|
+
for hash_key in list(self._cache.keys()):
|
|
764
|
+
if hash_key not in hashes_to_keep:
|
|
765
|
+
del self._cache[hash_key]
|
|
766
|
+
|
|
767
|
+
return removed_branches
|
|
@@ -1955,10 +1955,9 @@ class SchemaBranch:
|
|
|
1955
1955
|
)
|
|
1956
1956
|
)
|
|
1957
1957
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
] + template_schema.human_friendly_id
|
|
1958
|
+
parent_hfid = f"{relationship.name}__template_name__value"
|
|
1959
|
+
if relationship.kind == RelationshipKind.PARENT and parent_hfid not in template_schema.human_friendly_id:
|
|
1960
|
+
template_schema.human_friendly_id = [parent_hfid] + template_schema.human_friendly_id
|
|
1962
1961
|
template_schema.uniqueness_constraints[0].append(relationship.name)
|
|
1963
1962
|
|
|
1964
1963
|
def generate_object_template_from_node(
|
infrahub/database/__init__.py
CHANGED
|
@@ -205,6 +205,16 @@ class InfrahubDatabase:
|
|
|
205
205
|
def add_schema(self, schema: SchemaBranch, name: str | None = None) -> None:
|
|
206
206
|
self._schemas[name or schema.name] = schema
|
|
207
207
|
|
|
208
|
+
def purge_inactive_schemas(self, active_branches: list[str]) -> list[str]:
|
|
209
|
+
"""Return non active schema branches that were purged."""
|
|
210
|
+
removed_branches: list[str] = []
|
|
211
|
+
for branch_name in list(self._schemas.keys()):
|
|
212
|
+
if branch_name not in active_branches:
|
|
213
|
+
del self._schemas[branch_name]
|
|
214
|
+
removed_branches.append(branch_name)
|
|
215
|
+
|
|
216
|
+
return removed_branches
|
|
217
|
+
|
|
208
218
|
def start_session(self, read_only: bool = False, schemas: list[SchemaBranch] | None = None) -> InfrahubDatabase:
|
|
209
219
|
"""Create a new InfrahubDatabase object in Session mode."""
|
|
210
220
|
session_mode = InfrahubDatabaseSessionMode.WRITE
|
infrahub/events/group_action.py
CHANGED
|
@@ -2,7 +2,7 @@ from typing import ClassVar
|
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
|
-
from infrahub.core.constants import MutationAction
|
|
5
|
+
from infrahub.core.constants import InfrahubKind, MutationAction
|
|
6
6
|
|
|
7
7
|
from .constants import EVENT_NAMESPACE
|
|
8
8
|
from .models import EventNode, InfrahubEvent
|
|
@@ -21,6 +21,11 @@ class GroupMutatedEvent(InfrahubEvent):
|
|
|
21
21
|
|
|
22
22
|
def get_related(self) -> list[dict[str, str]]:
|
|
23
23
|
related = super().get_related()
|
|
24
|
+
|
|
25
|
+
if self.kind == InfrahubKind.GRAPHQLQUERYGROUP:
|
|
26
|
+
# Temporary workaround to avoid too large payloads for the related field
|
|
27
|
+
return related
|
|
28
|
+
|
|
24
29
|
related.append(
|
|
25
30
|
{
|
|
26
31
|
"prefect.resource.id": self.node_id,
|
infrahub/events/node_action.py
CHANGED
|
@@ -7,7 +7,7 @@ from infrahub.core.changelog.models import (
|
|
|
7
7
|
RelationshipCardinalityManyChangelog,
|
|
8
8
|
RelationshipCardinalityOneChangelog,
|
|
9
9
|
)
|
|
10
|
-
from infrahub.core.constants import DiffAction, MutationAction
|
|
10
|
+
from infrahub.core.constants import DiffAction, InfrahubKind, MutationAction
|
|
11
11
|
|
|
12
12
|
from .constants import EVENT_NAMESPACE
|
|
13
13
|
from .models import InfrahubEvent
|
|
@@ -24,6 +24,10 @@ class NodeMutatedEvent(InfrahubEvent):
|
|
|
24
24
|
|
|
25
25
|
def get_related(self) -> list[dict[str, str]]:
|
|
26
26
|
related = super().get_related()
|
|
27
|
+
if self.kind == InfrahubKind.GRAPHQLQUERYGROUP:
|
|
28
|
+
# Temporary workaround to avoid too large payloads for the related field
|
|
29
|
+
return related
|
|
30
|
+
|
|
27
31
|
for attribute in self.changelog.attributes.values():
|
|
28
32
|
related.append(
|
|
29
33
|
{
|
|
@@ -363,7 +363,7 @@ class InfrahubMutationMixin:
|
|
|
363
363
|
branch: Branch,
|
|
364
364
|
db: InfrahubDatabase,
|
|
365
365
|
obj: Node,
|
|
366
|
-
|
|
366
|
+
skip_uniqueness_check: bool = False,
|
|
367
367
|
) -> tuple[Node, Self]:
|
|
368
368
|
"""
|
|
369
369
|
Wrapper around mutate_update to potentially activate locking and call it within a database transaction.
|
|
@@ -378,11 +378,11 @@ class InfrahubMutationMixin:
|
|
|
378
378
|
if lock_names:
|
|
379
379
|
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
|
|
380
380
|
obj = await cls.mutate_update_object(
|
|
381
|
-
db=db, info=info, data=data, branch=branch, obj=obj,
|
|
381
|
+
db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
|
|
382
382
|
)
|
|
383
383
|
else:
|
|
384
384
|
obj = await cls.mutate_update_object(
|
|
385
|
-
db=db, info=info, data=data, branch=branch, obj=obj,
|
|
385
|
+
db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
|
|
386
386
|
)
|
|
387
387
|
result = await cls.mutate_update_to_graphql(db=db, info=info, obj=obj)
|
|
388
388
|
return obj, result
|
|
@@ -396,11 +396,11 @@ class InfrahubMutationMixin:
|
|
|
396
396
|
data=data,
|
|
397
397
|
branch=branch,
|
|
398
398
|
obj=obj,
|
|
399
|
-
|
|
399
|
+
skip_uniqueness_check=skip_uniqueness_check,
|
|
400
400
|
)
|
|
401
401
|
else:
|
|
402
402
|
obj = await cls.mutate_update_object(
|
|
403
|
-
db=dbt, info=info, data=data, branch=branch, obj=obj,
|
|
403
|
+
db=dbt, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
|
|
404
404
|
)
|
|
405
405
|
result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=obj)
|
|
406
406
|
return obj, result
|
|
@@ -434,7 +434,7 @@ class InfrahubMutationMixin:
|
|
|
434
434
|
data: InputObjectType,
|
|
435
435
|
branch: Branch,
|
|
436
436
|
obj: Node,
|
|
437
|
-
|
|
437
|
+
skip_uniqueness_check: bool = False,
|
|
438
438
|
) -> Node:
|
|
439
439
|
component_registry = get_component_registry()
|
|
440
440
|
node_constraint_runner = await component_registry.get_component(NodeConstraintRunner, db=db, branch=branch)
|
|
@@ -442,8 +442,9 @@ class InfrahubMutationMixin:
|
|
|
442
442
|
before_mutate_profile_ids = await cls._get_profile_ids(db=db, obj=obj)
|
|
443
443
|
await obj.from_graphql(db=db, data=data)
|
|
444
444
|
fields_to_validate = list(data)
|
|
445
|
-
|
|
446
|
-
|
|
445
|
+
await node_constraint_runner.check(
|
|
446
|
+
node=obj, field_filters=fields_to_validate, skip_uniqueness_check=skip_uniqueness_check
|
|
447
|
+
)
|
|
447
448
|
|
|
448
449
|
fields = list(data.keys())
|
|
449
450
|
for field_to_remove in ("id", "hfid"):
|
|
@@ -494,7 +495,6 @@ class InfrahubMutationMixin:
|
|
|
494
495
|
db = database or graphql_context.db
|
|
495
496
|
dict_data = dict(data)
|
|
496
497
|
node = None
|
|
497
|
-
run_constraint_checks = True
|
|
498
498
|
|
|
499
499
|
if "id" in dict_data:
|
|
500
500
|
node = await NodeManager.get_one(
|
|
@@ -506,7 +506,6 @@ class InfrahubMutationMixin:
|
|
|
506
506
|
db=db,
|
|
507
507
|
branch=branch,
|
|
508
508
|
obj=node,
|
|
509
|
-
run_constraint_checks=run_constraint_checks,
|
|
510
509
|
)
|
|
511
510
|
return updated_obj, mutation, False
|
|
512
511
|
|
|
@@ -525,7 +524,6 @@ class InfrahubMutationMixin:
|
|
|
525
524
|
db=db,
|
|
526
525
|
branch=branch,
|
|
527
526
|
obj=node,
|
|
528
|
-
run_constraint_checks=run_constraint_checks,
|
|
529
527
|
)
|
|
530
528
|
return updated_obj, mutation, False
|
|
531
529
|
|
|
@@ -545,7 +543,7 @@ class InfrahubMutationMixin:
|
|
|
545
543
|
db=db,
|
|
546
544
|
branch=branch,
|
|
547
545
|
obj=node,
|
|
548
|
-
|
|
546
|
+
skip_uniqueness_check=True,
|
|
549
547
|
)
|
|
550
548
|
return updated_obj, mutation, False
|
|
551
549
|
|