infrahub-server 1.2.3__py3-none-any.whl → 1.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/cli/db.py +308 -2
- infrahub/cli/git_agent.py +4 -10
- infrahub/config.py +32 -0
- infrahub/core/branch/tasks.py +50 -10
- 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 +4 -0
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +68 -70
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +26 -0
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +54 -0
- infrahub/core/migrations/schema/node_attribute_remove.py +16 -2
- infrahub/core/models.py +1 -1
- infrahub/core/node/__init__.py +4 -1
- infrahub/core/node/constraints/grouped_uniqueness.py +6 -1
- infrahub/core/node/resource_manager/number_pool.py +1 -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 -1
- infrahub/core/schema/schema_branch.py +8 -7
- infrahub/core/schema/schema_branch_computed.py +12 -1
- infrahub/database/__init__.py +10 -0
- infrahub/events/branch_action.py +3 -0
- infrahub/events/group_action.py +6 -1
- infrahub/events/node_action.py +5 -1
- infrahub/git/integrator.py +2 -2
- infrahub/graphql/mutations/main.py +10 -12
- infrahub/message_bus/messages/__init__.py +0 -4
- infrahub/message_bus/messages/request_proposedchange_pipeline.py +5 -0
- infrahub/message_bus/operations/__init__.py +0 -3
- infrahub/message_bus/operations/requests/proposed_change.py +29 -9
- infrahub/message_bus/types.py +2 -34
- infrahub/proposed_change/branch_diff.py +65 -0
- infrahub/proposed_change/tasks.py +12 -4
- 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/adapters/workflow/worker.py +1 -1
- infrahub/services/component.py +1 -2
- infrahub/tasks/registry.py +3 -7
- infrahub/workers/infrahub_async.py +4 -10
- infrahub/workflows/catalogue.py +10 -0
- infrahub_sdk/generator.py +1 -0
- infrahub_sdk/node.py +16 -4
- infrahub_sdk/schema/__init__.py +10 -1
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/METADATA +2 -2
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/RECORD +57 -60
- infrahub_testcontainers/container.py +4 -0
- infrahub_testcontainers/helpers.py +1 -1
- infrahub_testcontainers/models.py +2 -2
- infrahub_testcontainers/performance_test.py +4 -4
- infrahub/core/branch/flow_models.py +0 -0
- infrahub/message_bus/messages/event_branch_merge.py +0 -13
- infrahub/message_bus/messages/event_worker_newprimaryapi.py +0 -9
- infrahub/message_bus/operations/event/__init__.py +0 -3
- infrahub/message_bus/operations/event/branch.py +0 -61
- infrahub/message_bus/operations/event/worker.py +0 -9
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/WHEEL +0 -0
- {infrahub_server-1.2.3.dist-info → infrahub_server-1.2.5.dist-info}/entry_points.txt +0 -0
|
@@ -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)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
from typing import TYPE_CHECKING, Sequence
|
|
5
|
+
|
|
6
|
+
from infrahub.core.branch.models import Branch
|
|
7
|
+
from infrahub.core.initialization import initialization
|
|
8
|
+
from infrahub.core.ipam.reconciler import IpamReconciler
|
|
9
|
+
from infrahub.core.manager import NodeManager
|
|
10
|
+
from infrahub.core.migrations.shared import MigrationResult
|
|
11
|
+
from infrahub.core.timestamp import Timestamp
|
|
12
|
+
from infrahub.lock import initialize_lock
|
|
13
|
+
from infrahub.log import get_logger
|
|
14
|
+
|
|
15
|
+
from ..shared import InternalSchemaMigration, SchemaMigration
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from infrahub.database import InfrahubDatabase
|
|
19
|
+
|
|
20
|
+
log = get_logger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Migration026(InternalSchemaMigration):
|
|
24
|
+
name: str = "026_prefix_0000_fix"
|
|
25
|
+
minimum_version: int = 25
|
|
26
|
+
migrations: Sequence[SchemaMigration] = []
|
|
27
|
+
|
|
28
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
29
|
+
return MigrationResult()
|
|
30
|
+
|
|
31
|
+
async def execute(self, db: InfrahubDatabase) -> MigrationResult:
|
|
32
|
+
# load schemas from database into registry
|
|
33
|
+
initialize_lock()
|
|
34
|
+
await initialization(db=db)
|
|
35
|
+
|
|
36
|
+
at = Timestamp()
|
|
37
|
+
for branch in await Branch.get_list(db=db):
|
|
38
|
+
prefix_0000s = await NodeManager.query(
|
|
39
|
+
db=db, schema="BuiltinIPPrefix", branch=branch, filters={"prefix__values": ["0.0.0.0/0", "::/0"]}
|
|
40
|
+
)
|
|
41
|
+
if not prefix_0000s:
|
|
42
|
+
continue
|
|
43
|
+
ipam_reconciler = IpamReconciler(db=db, branch=branch)
|
|
44
|
+
for prefix in prefix_0000s:
|
|
45
|
+
ip_namespace = await prefix.ip_namespace.get_peer(db=db)
|
|
46
|
+
ip_network = ipaddress.ip_network(prefix.prefix.value)
|
|
47
|
+
await ipam_reconciler.reconcile(
|
|
48
|
+
ip_value=ip_network,
|
|
49
|
+
namespace=ip_namespace,
|
|
50
|
+
node_uuid=prefix.get_id(),
|
|
51
|
+
at=at,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return MigrationResult()
|
|
@@ -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
|
infrahub/core/node/__init__.py
CHANGED
|
@@ -259,7 +259,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
|
|
|
259
259
|
)
|
|
260
260
|
return
|
|
261
261
|
|
|
262
|
-
if
|
|
262
|
+
if (
|
|
263
|
+
number_pool.node.value in [self._schema.kind] + self._schema.inherit_from
|
|
264
|
+
and number_pool.node_attribute.value == attribute.name
|
|
265
|
+
):
|
|
263
266
|
try:
|
|
264
267
|
next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self)
|
|
265
268
|
except PoolExhaustedError:
|
|
@@ -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
|
|
|
@@ -50,7 +50,7 @@ class CoreNumberPool(Node):
|
|
|
50
50
|
taken=taken,
|
|
51
51
|
)
|
|
52
52
|
if next_number is None:
|
|
53
|
-
raise PoolExhaustedError("There are no more
|
|
53
|
+
raise PoolExhaustedError("There are no more values available in this pool.")
|
|
54
54
|
|
|
55
55
|
return next_number
|
|
56
56
|
|
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
|
@@ -155,7 +155,6 @@ class SchemaManager(NodeManager):
|
|
|
155
155
|
|
|
156
156
|
updated_schema = None
|
|
157
157
|
if update_db:
|
|
158
|
-
schema_diff = None
|
|
159
158
|
if diff:
|
|
160
159
|
schema_diff = await self.update_schema_to_db(schema=schema, db=db, branch=branch, diff=diff)
|
|
161
160
|
else:
|
|
@@ -744,3 +743,24 @@ class SchemaManager(NodeManager):
|
|
|
744
743
|
"""Convert a schema_node object loaded from the database into GenericSchema object."""
|
|
745
744
|
node_data = await cls._prepare_node_data(schema_node=schema_node, db=db)
|
|
746
745
|
return GenericSchema(**node_data)
|
|
746
|
+
|
|
747
|
+
def purge_inactive_branches(self, active_branches: list[str]) -> list[str]:
|
|
748
|
+
"""Return non active branches that were purged."""
|
|
749
|
+
|
|
750
|
+
hashes_to_keep: set[str] = set()
|
|
751
|
+
for active_branch in active_branches:
|
|
752
|
+
if branch := self._branches.get(active_branch):
|
|
753
|
+
nodes = branch.get_all(include_internal=True, duplicate=False)
|
|
754
|
+
hashes_to_keep.update([node.get_hash() for node in nodes.values()])
|
|
755
|
+
|
|
756
|
+
removed_branches: list[str] = []
|
|
757
|
+
for branch_name in list(self._branches.keys()):
|
|
758
|
+
if branch_name not in active_branches:
|
|
759
|
+
del self._branches[branch_name]
|
|
760
|
+
removed_branches.append(branch_name)
|
|
761
|
+
|
|
762
|
+
for hash_key in list(self._cache.keys()):
|
|
763
|
+
if hash_key not in hashes_to_keep:
|
|
764
|
+
del self._cache[hash_key]
|
|
765
|
+
|
|
766
|
+
return removed_branches
|
|
@@ -1005,9 +1005,11 @@ class SchemaBranch:
|
|
|
1005
1005
|
generic_schema = self.get_generic(name=name, duplicate=False)
|
|
1006
1006
|
for attribute in generic_schema.attributes:
|
|
1007
1007
|
if attribute.computed_attribute and attribute.computed_attribute.kind != ComputedAttributeKind.USER:
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1008
|
+
for inheriting_node in generic_schema.used_by:
|
|
1009
|
+
node_schema = self.get_node(name=inheriting_node, duplicate=False)
|
|
1010
|
+
self.computed_attributes.validate_generic_inheritance(
|
|
1011
|
+
node=node_schema, attribute=attribute, generic=generic_schema
|
|
1012
|
+
)
|
|
1011
1013
|
|
|
1012
1014
|
def _validate_computed_attribute(self, node: NodeSchema, attribute: AttributeSchema) -> None:
|
|
1013
1015
|
if not attribute.computed_attribute or attribute.computed_attribute.kind == ComputedAttributeKind.USER:
|
|
@@ -1955,10 +1957,9 @@ class SchemaBranch:
|
|
|
1955
1957
|
)
|
|
1956
1958
|
)
|
|
1957
1959
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
] + template_schema.human_friendly_id
|
|
1960
|
+
parent_hfid = f"{relationship.name}__template_name__value"
|
|
1961
|
+
if relationship.kind == RelationshipKind.PARENT and parent_hfid not in template_schema.human_friendly_id:
|
|
1962
|
+
template_schema.human_friendly_id = [parent_hfid] + template_schema.human_friendly_id
|
|
1962
1963
|
template_schema.uniqueness_constraints[0].append(relationship.name)
|
|
1963
1964
|
|
|
1964
1965
|
def generate_object_template_from_node(
|
|
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field
|
|
|
9
9
|
from infrahub.core.schema import AttributeSchema # noqa: TC001
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from infrahub.core.schema import NodeSchema, SchemaAttributePath
|
|
12
|
+
from infrahub.core.schema import GenericSchema, NodeSchema, SchemaAttributePath
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
@@ -90,6 +90,7 @@ class ComputedAttributes:
|
|
|
90
90
|
) -> None:
|
|
91
91
|
self._computed_python_transform_attribute_map: dict[str, list[AttributeSchema]] = transform_attribute_map or {}
|
|
92
92
|
self._computed_jinja2_attribute_map: dict[str, RegisteredNodeComputedAttribute] = jinja2_attribute_map or {}
|
|
93
|
+
self._defined_from_generic: dict[str, str] = {}
|
|
93
94
|
|
|
94
95
|
def duplicate(self) -> ComputedAttributes:
|
|
95
96
|
return self.__class__(
|
|
@@ -166,6 +167,16 @@ class ComputedAttributes:
|
|
|
166
167
|
schema_path.active_relationship_schema.name
|
|
167
168
|
].append(deepcopy(source_attribute))
|
|
168
169
|
|
|
170
|
+
def validate_generic_inheritance(
|
|
171
|
+
self, node: NodeSchema, attribute: AttributeSchema, generic: GenericSchema
|
|
172
|
+
) -> None:
|
|
173
|
+
attribute_key = f"{node.kind}__{attribute.name}"
|
|
174
|
+
if duplicate := self._defined_from_generic.get(attribute_key):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"{node.kind}: {attribute.name!r} is declared as a computed attribute from multiple generics {sorted([duplicate, generic.kind])}"
|
|
177
|
+
)
|
|
178
|
+
self._defined_from_generic[attribute_key] = generic.kind
|
|
179
|
+
|
|
169
180
|
def get_impacted_jinja2_targets(self, kind: str, updates: list[str] | None = None) -> list[ComputedAttributeTarget]:
|
|
170
181
|
if mapping := self._computed_jinja2_attribute_map.get(kind):
|
|
171
182
|
return mapping.get_targets(updates=updates)
|
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/branch_action.py
CHANGED
|
@@ -100,6 +100,9 @@ class BranchMergedEvent(InfrahubEvent):
|
|
|
100
100
|
|
|
101
101
|
return related
|
|
102
102
|
|
|
103
|
+
def get_messages(self) -> list[InfrahubMessage]:
|
|
104
|
+
return [RefreshRegistryBranches()]
|
|
105
|
+
|
|
103
106
|
|
|
104
107
|
class BranchRebasedEvent(InfrahubEvent):
|
|
105
108
|
"""Event generated when a branch has been rebased"""
|
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 in [InfrahubKind.GENERATORGROUP, 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 in [InfrahubKind.GENERATORGROUP, 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
|
{
|
infrahub/git/integrator.py
CHANGED
|
@@ -954,7 +954,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
|
|
|
954
954
|
source=self.id,
|
|
955
955
|
is_protected=True,
|
|
956
956
|
)
|
|
957
|
-
obj = await self.sdk.create(kind=CoreCheckDefinition, branch=branch_name,
|
|
957
|
+
obj = await self.sdk.create(kind=CoreCheckDefinition, branch=branch_name, data=create_payload)
|
|
958
958
|
await obj.save()
|
|
959
959
|
|
|
960
960
|
return obj
|
|
@@ -1012,7 +1012,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
|
|
|
1012
1012
|
source=str(self.id),
|
|
1013
1013
|
is_protected=True,
|
|
1014
1014
|
)
|
|
1015
|
-
obj = await self.sdk.create(kind=CoreTransformPython, branch=branch_name,
|
|
1015
|
+
obj = await self.sdk.create(kind=CoreTransformPython, branch=branch_name, data=create_payload)
|
|
1016
1016
|
await obj.save()
|
|
1017
1017
|
return obj
|
|
1018
1018
|
|