infrahub-server 1.4.12__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/actions/tasks.py +208 -16
- infrahub/api/artifact.py +3 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/api/internal.py +2 -0
- infrahub/api/query.py +2 -0
- infrahub/api/schema.py +27 -3
- infrahub/auth.py +5 -5
- infrahub/cli/__init__.py +2 -0
- infrahub/cli/db.py +160 -157
- infrahub/cli/dev.py +118 -0
- infrahub/cli/tasks.py +46 -0
- infrahub/cli/upgrade.py +56 -9
- infrahub/computed_attribute/tasks.py +19 -7
- infrahub/config.py +7 -2
- infrahub/core/attribute.py +35 -24
- infrahub/core/branch/enums.py +1 -1
- infrahub/core/branch/models.py +9 -5
- infrahub/core/branch/needs_rebase_status.py +11 -0
- infrahub/core/branch/tasks.py +72 -10
- infrahub/core/changelog/models.py +2 -10
- infrahub/core/constants/__init__.py +4 -0
- infrahub/core/constants/infrahubkind.py +1 -0
- infrahub/core/convert_object_type/object_conversion.py +201 -0
- infrahub/core/convert_object_type/repository_conversion.py +89 -0
- infrahub/core/convert_object_type/schema_mapping.py +27 -3
- infrahub/core/diff/calculator.py +2 -2
- infrahub/core/diff/model/path.py +4 -0
- infrahub/core/diff/payload_builder.py +1 -1
- infrahub/core/diff/query/artifact.py +1 -0
- infrahub/core/diff/query/delete_query.py +9 -5
- infrahub/core/diff/query/field_summary.py +1 -0
- infrahub/core/diff/query/merge.py +39 -23
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +7 -4
- infrahub/core/manager.py +3 -81
- infrahub/core/migrations/__init__.py +3 -0
- infrahub/core/migrations/exceptions.py +4 -0
- infrahub/core/migrations/graph/__init__.py +13 -10
- infrahub/core/migrations/graph/load_schema_branch.py +21 -0
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
- infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
- infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +149 -0
- infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
- infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
- infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
- infrahub/core/migrations/query/__init__.py +7 -8
- infrahub/core/migrations/query/attribute_add.py +8 -6
- infrahub/core/migrations/query/attribute_remove.py +134 -0
- infrahub/core/migrations/runner.py +54 -0
- infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
- infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
- infrahub/core/migrations/schema/node_attribute_add.py +26 -5
- infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
- infrahub/core/migrations/schema/node_kind_update.py +2 -1
- infrahub/core/migrations/schema/node_remove.py +2 -1
- infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
- infrahub/core/migrations/shared.py +66 -19
- infrahub/core/models.py +2 -2
- infrahub/core/node/__init__.py +207 -54
- infrahub/core/node/create.py +53 -49
- infrahub/core/node/lock_utils.py +124 -0
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
- infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
- infrahub/core/node/resource_manager/number_pool.py +2 -1
- infrahub/core/node/standard.py +1 -1
- infrahub/core/property.py +11 -0
- infrahub/core/protocols.py +8 -1
- infrahub/core/query/attribute.py +82 -15
- infrahub/core/query/diff.py +61 -16
- infrahub/core/query/ipam.py +16 -4
- infrahub/core/query/node.py +92 -212
- infrahub/core/query/relationship.py +44 -26
- infrahub/core/query/subquery.py +0 -8
- infrahub/core/relationship/model.py +69 -24
- infrahub/core/schema/__init__.py +56 -0
- infrahub/core/schema/attribute_schema.py +4 -2
- infrahub/core/schema/basenode_schema.py +42 -2
- infrahub/core/schema/definitions/core/__init__.py +2 -0
- infrahub/core/schema/definitions/core/check.py +1 -1
- infrahub/core/schema/definitions/core/generator.py +2 -0
- infrahub/core/schema/definitions/core/group.py +16 -2
- infrahub/core/schema/definitions/core/repository.py +7 -0
- infrahub/core/schema/definitions/core/transform.py +1 -1
- infrahub/core/schema/definitions/internal.py +12 -3
- infrahub/core/schema/generated/attribute_schema.py +2 -2
- infrahub/core/schema/generated/base_node_schema.py +6 -1
- infrahub/core/schema/manager.py +3 -0
- infrahub/core/schema/node_schema.py +1 -0
- infrahub/core/schema/relationship_schema.py +0 -1
- infrahub/core/schema/schema_branch.py +295 -10
- infrahub/core/schema/schema_branch_display.py +135 -0
- infrahub/core/schema/schema_branch_hfid.py +120 -0
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/database/graph.py +21 -0
- infrahub/display_labels/__init__.py +0 -0
- infrahub/display_labels/gather.py +48 -0
- infrahub/display_labels/models.py +240 -0
- infrahub/display_labels/tasks.py +192 -0
- infrahub/display_labels/triggers.py +22 -0
- infrahub/events/branch_action.py +27 -1
- infrahub/events/group_action.py +1 -1
- infrahub/events/node_action.py +1 -1
- infrahub/generators/constants.py +7 -0
- infrahub/generators/models.py +38 -12
- infrahub/generators/tasks.py +34 -16
- infrahub/git/base.py +42 -2
- infrahub/git/integrator.py +22 -14
- infrahub/git/tasks.py +52 -2
- infrahub/graphql/analyzer.py +9 -0
- infrahub/graphql/api/dependencies.py +2 -4
- infrahub/graphql/api/endpoints.py +16 -6
- infrahub/graphql/app.py +2 -4
- infrahub/graphql/initialization.py +2 -3
- infrahub/graphql/manager.py +213 -137
- infrahub/graphql/middleware.py +12 -0
- infrahub/graphql/mutations/branch.py +16 -0
- infrahub/graphql/mutations/computed_attribute.py +110 -3
- infrahub/graphql/mutations/convert_object_type.py +44 -13
- infrahub/graphql/mutations/display_label.py +118 -0
- infrahub/graphql/mutations/generator.py +25 -7
- infrahub/graphql/mutations/hfid.py +125 -0
- infrahub/graphql/mutations/ipam.py +73 -41
- infrahub/graphql/mutations/main.py +61 -178
- infrahub/graphql/mutations/profile.py +195 -0
- infrahub/graphql/mutations/proposed_change.py +8 -1
- infrahub/graphql/mutations/relationship.py +2 -2
- infrahub/graphql/mutations/repository.py +22 -83
- infrahub/graphql/mutations/resource_manager.py +2 -2
- infrahub/graphql/mutations/webhook.py +1 -1
- infrahub/graphql/queries/resource_manager.py +1 -1
- infrahub/graphql/registry.py +173 -0
- infrahub/graphql/resolvers/resolver.py +2 -0
- infrahub/graphql/schema.py +8 -1
- infrahub/graphql/schema_sort.py +170 -0
- infrahub/graphql/types/branch.py +4 -1
- infrahub/graphql/types/enums.py +3 -0
- infrahub/groups/tasks.py +1 -1
- infrahub/hfid/__init__.py +0 -0
- infrahub/hfid/gather.py +48 -0
- infrahub/hfid/models.py +240 -0
- infrahub/hfid/tasks.py +191 -0
- infrahub/hfid/triggers.py +22 -0
- infrahub/lock.py +119 -42
- infrahub/locks/__init__.py +0 -0
- infrahub/locks/tasks.py +37 -0
- infrahub/message_bus/types.py +1 -0
- infrahub/patch/plan_writer.py +2 -2
- infrahub/permissions/constants.py +2 -0
- infrahub/profiles/__init__.py +0 -0
- infrahub/profiles/node_applier.py +101 -0
- infrahub/profiles/queries/__init__.py +0 -0
- infrahub/profiles/queries/get_profile_data.py +98 -0
- infrahub/profiles/tasks.py +63 -0
- infrahub/proposed_change/tasks.py +67 -14
- infrahub/repositories/__init__.py +0 -0
- infrahub/repositories/create_repository.py +113 -0
- infrahub/server.py +9 -1
- infrahub/services/__init__.py +8 -5
- infrahub/services/adapters/http/__init__.py +5 -0
- infrahub/services/adapters/workflow/worker.py +14 -3
- infrahub/task_manager/event.py +5 -0
- infrahub/task_manager/models.py +7 -0
- infrahub/task_manager/task.py +73 -0
- infrahub/tasks/registry.py +6 -4
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/setup.py +13 -4
- infrahub/trigger/tasks.py +6 -0
- infrahub/webhook/models.py +1 -1
- infrahub/workers/dependencies.py +3 -1
- infrahub/workers/infrahub_async.py +10 -2
- infrahub/workflows/catalogue.py +118 -3
- infrahub/workflows/initialization.py +21 -0
- infrahub/workflows/models.py +17 -2
- infrahub/workflows/utils.py +2 -1
- infrahub_sdk/branch.py +17 -8
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/client.py +376 -95
- infrahub_sdk/config.py +29 -2
- infrahub_sdk/convert_object_type.py +61 -0
- infrahub_sdk/ctl/branch.py +3 -0
- infrahub_sdk/ctl/check.py +2 -3
- infrahub_sdk/ctl/cli_commands.py +20 -12
- infrahub_sdk/ctl/config.py +8 -2
- infrahub_sdk/ctl/generator.py +6 -3
- infrahub_sdk/ctl/graphql.py +184 -0
- infrahub_sdk/ctl/repository.py +39 -1
- infrahub_sdk/ctl/schema.py +40 -10
- infrahub_sdk/ctl/task.py +110 -0
- infrahub_sdk/ctl/utils.py +4 -0
- infrahub_sdk/ctl/validate.py +5 -3
- infrahub_sdk/diff.py +4 -5
- infrahub_sdk/exceptions.py +2 -0
- infrahub_sdk/generator.py +7 -1
- infrahub_sdk/graphql/__init__.py +12 -0
- infrahub_sdk/graphql/constants.py +1 -0
- infrahub_sdk/graphql/plugin.py +85 -0
- infrahub_sdk/graphql/query.py +77 -0
- infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
- infrahub_sdk/graphql/utils.py +40 -0
- infrahub_sdk/node/attribute.py +2 -0
- infrahub_sdk/node/node.py +28 -20
- infrahub_sdk/node/relationship.py +1 -3
- infrahub_sdk/playback.py +1 -2
- infrahub_sdk/protocols.py +54 -6
- infrahub_sdk/pytest_plugin/plugin.py +7 -4
- infrahub_sdk/pytest_plugin/utils.py +40 -0
- infrahub_sdk/repository.py +1 -2
- infrahub_sdk/schema/__init__.py +70 -4
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/models.py +7 -0
- infrahub_sdk/spec/object.py +54 -6
- infrahub_sdk/spec/processors/__init__.py +0 -0
- infrahub_sdk/spec/processors/data_processor.py +10 -0
- infrahub_sdk/spec/processors/factory.py +34 -0
- infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
- infrahub_sdk/spec/range_expansion.py +118 -0
- infrahub_sdk/task/models.py +6 -4
- infrahub_sdk/timestamp.py +18 -6
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
- {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +233 -176
- infrahub_testcontainers/container.py +114 -2
- infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
- infrahub_testcontainers/docker-compose.test.yml +5 -0
- infrahub_testcontainers/models.py +2 -2
- infrahub_testcontainers/performance_test.py +4 -4
- infrahub/core/convert_object_type/conversion.py +0 -134
- {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.12.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -18,6 +18,7 @@ from infrahub.computed_attribute.constants import VALID_KINDS as VALID_COMPUTED_
|
|
|
18
18
|
from infrahub.core.constants import (
|
|
19
19
|
OBJECT_TEMPLATE_NAME_ATTR,
|
|
20
20
|
OBJECT_TEMPLATE_RELATIONSHIP_NAME,
|
|
21
|
+
PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
|
|
21
22
|
RESERVED_ATTR_GEN_NAMES,
|
|
22
23
|
RESERVED_ATTR_REL_NAMES,
|
|
23
24
|
RESTRICTED_NAMESPACES,
|
|
@@ -65,6 +66,8 @@ from ... import config
|
|
|
65
66
|
from ..constants.schema import PARENT_CHILD_IDENTIFIER
|
|
66
67
|
from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
|
|
67
68
|
from .schema_branch_computed import ComputedAttributes
|
|
69
|
+
from .schema_branch_display import DisplayLabels
|
|
70
|
+
from .schema_branch_hfid import HFIDs
|
|
68
71
|
|
|
69
72
|
log = get_logger()
|
|
70
73
|
|
|
@@ -76,6 +79,8 @@ class SchemaBranch:
|
|
|
76
79
|
name: str | None = None,
|
|
77
80
|
data: dict[str, dict[str, str]] | None = None,
|
|
78
81
|
computed_attributes: ComputedAttributes | None = None,
|
|
82
|
+
display_labels: DisplayLabels | None = None,
|
|
83
|
+
hfids: HFIDs | None = None,
|
|
79
84
|
):
|
|
80
85
|
self._cache: dict[str, NodeSchema | GenericSchema] = cache
|
|
81
86
|
self.name: str | None = name
|
|
@@ -84,6 +89,8 @@ class SchemaBranch:
|
|
|
84
89
|
self.profiles: dict[str, str] = {}
|
|
85
90
|
self.templates: dict[str, str] = {}
|
|
86
91
|
self.computed_attributes = computed_attributes or ComputedAttributes()
|
|
92
|
+
self.display_labels = display_labels or DisplayLabels()
|
|
93
|
+
self.hfids = hfids or HFIDs()
|
|
87
94
|
|
|
88
95
|
if data:
|
|
89
96
|
self.nodes = data.get("nodes", {})
|
|
@@ -277,6 +284,8 @@ class SchemaBranch:
|
|
|
277
284
|
data=copy.deepcopy(self.to_dict()),
|
|
278
285
|
cache=self._cache,
|
|
279
286
|
computed_attributes=self.computed_attributes.duplicate(),
|
|
287
|
+
display_labels=self.display_labels.duplicate(),
|
|
288
|
+
hfids=self.hfids.duplicate(),
|
|
280
289
|
)
|
|
281
290
|
|
|
282
291
|
def set(self, name: str, schema: MainSchemaTypes) -> str:
|
|
@@ -319,14 +328,23 @@ class SchemaBranch:
|
|
|
319
328
|
elif name in self.templates:
|
|
320
329
|
key = self.templates[name]
|
|
321
330
|
|
|
322
|
-
if key
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
331
|
+
if not key:
|
|
332
|
+
raise SchemaNotFoundError(
|
|
333
|
+
branch_name=self.name, identifier=name, message=f"Unable to find the schema {name!r} in the registry"
|
|
334
|
+
)
|
|
326
335
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
336
|
+
schema: MainSchemaTypes | None = None
|
|
337
|
+
try:
|
|
338
|
+
schema = self._cache[key]
|
|
339
|
+
except KeyError:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
if not schema:
|
|
343
|
+
raise ValueError(f"Schema {name!r} on branch {self.name} has incorrect hash: {key!r}")
|
|
344
|
+
|
|
345
|
+
if duplicate:
|
|
346
|
+
return schema.duplicate()
|
|
347
|
+
return schema
|
|
330
348
|
|
|
331
349
|
def get_node(self, name: str, duplicate: bool = True) -> NodeSchema:
|
|
332
350
|
"""Access a specific NodeSchema, defined by its kind."""
|
|
@@ -511,6 +529,9 @@ class SchemaBranch:
|
|
|
511
529
|
self.process_post_validation()
|
|
512
530
|
|
|
513
531
|
def process_pre_validation(self) -> None:
|
|
532
|
+
self.process_nodes_state()
|
|
533
|
+
self.process_attributes_state()
|
|
534
|
+
self.process_relationships_state()
|
|
514
535
|
self.generate_identifiers()
|
|
515
536
|
self.process_default_values()
|
|
516
537
|
self.process_deprecations()
|
|
@@ -537,11 +558,13 @@ class SchemaBranch:
|
|
|
537
558
|
self.sync_uniqueness_constraints_and_unique_attributes()
|
|
538
559
|
self.validate_uniqueness_constraints()
|
|
539
560
|
self.validate_display_labels()
|
|
561
|
+
self.validate_display_label()
|
|
540
562
|
self.validate_order_by()
|
|
541
563
|
self.validate_default_filters()
|
|
542
564
|
self.validate_parent_component()
|
|
543
565
|
self.validate_human_friendly_id()
|
|
544
566
|
self.validate_required_relationships()
|
|
567
|
+
self.validate_inherited_relationships_fields()
|
|
545
568
|
|
|
546
569
|
def process_post_validation(self) -> None:
|
|
547
570
|
self.cleanup_inherited_elements()
|
|
@@ -551,6 +574,7 @@ class SchemaBranch:
|
|
|
551
574
|
self.process_dropdowns()
|
|
552
575
|
self.process_relationships()
|
|
553
576
|
self.process_human_friendly_id()
|
|
577
|
+
self.register_human_friendly_id()
|
|
554
578
|
|
|
555
579
|
def _generate_identifier_string(self, node_kind: str, peer_kind: str) -> str:
|
|
556
580
|
return "__".join(sorted([node_kind, peer_kind])).lower()
|
|
@@ -759,6 +783,36 @@ class SchemaBranch:
|
|
|
759
783
|
element_name=element_name,
|
|
760
784
|
)
|
|
761
785
|
|
|
786
|
+
def validate_display_label(self) -> None:
|
|
787
|
+
self.display_labels = DisplayLabels()
|
|
788
|
+
for name in self.all_names:
|
|
789
|
+
node_schema = self.get(name=name, duplicate=False)
|
|
790
|
+
|
|
791
|
+
if node_schema.display_label is None and node_schema.display_labels:
|
|
792
|
+
update_candidate = self.get(name=name, duplicate=True)
|
|
793
|
+
if len(node_schema.display_labels) == 1:
|
|
794
|
+
# If the previous display_labels consist of a single attribute convert
|
|
795
|
+
# it to an attribute based display label
|
|
796
|
+
update_candidate.display_label = _format_display_label_component(
|
|
797
|
+
component=node_schema.display_labels[0]
|
|
798
|
+
)
|
|
799
|
+
else:
|
|
800
|
+
# If the previous display label consists of multiple attributes
|
|
801
|
+
# convert it to a Jinja2 based display label
|
|
802
|
+
update_candidate.display_label = " ".join(
|
|
803
|
+
[
|
|
804
|
+
f"{{{{ {_format_display_label_component(component=display_label)} }}}}"
|
|
805
|
+
for display_label in node_schema.display_labels
|
|
806
|
+
]
|
|
807
|
+
)
|
|
808
|
+
self.set(name=name, schema=update_candidate)
|
|
809
|
+
|
|
810
|
+
node_schema = self.get(name=name, duplicate=False)
|
|
811
|
+
if not node_schema.display_label:
|
|
812
|
+
continue
|
|
813
|
+
|
|
814
|
+
self._validate_display_label(node=node_schema)
|
|
815
|
+
|
|
762
816
|
def validate_display_labels(self) -> None:
|
|
763
817
|
for name in self.all_names:
|
|
764
818
|
node_schema = self.get(name=name, duplicate=False)
|
|
@@ -867,7 +921,14 @@ class SchemaBranch:
|
|
|
867
921
|
# Mapping relationship identifiers -> list of attributes paths
|
|
868
922
|
rel_schemas_to_paths: dict[str, tuple[MainSchemaTypes, list[str]]] = {}
|
|
869
923
|
|
|
924
|
+
visited_paths: list[str] = []
|
|
870
925
|
for hfid_path in node_schema.human_friendly_id:
|
|
926
|
+
if config.SETTINGS.main.schema_strict_mode and hfid_path in visited_paths:
|
|
927
|
+
raise ValidationError(
|
|
928
|
+
f"HFID of {node_schema.kind} cannot use the same path more than once: {hfid_path}"
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
visited_paths.append(hfid_path)
|
|
871
932
|
schema_path = self.validate_schema_path(
|
|
872
933
|
node_schema=node_schema,
|
|
873
934
|
path=hfid_path,
|
|
@@ -1144,6 +1205,50 @@ class SchemaBranch:
|
|
|
1144
1205
|
node=node_schema, attribute=attribute, generic=generic_schema
|
|
1145
1206
|
)
|
|
1146
1207
|
|
|
1208
|
+
def _validate_display_label(self, node: MainSchemaTypes) -> None:
|
|
1209
|
+
if not node.display_label:
|
|
1210
|
+
return
|
|
1211
|
+
|
|
1212
|
+
if not any(c in node.display_label for c in "{}"):
|
|
1213
|
+
schema_path = self.validate_schema_path(
|
|
1214
|
+
node_schema=node,
|
|
1215
|
+
path=node.display_label,
|
|
1216
|
+
allowed_path_types=SchemaElementPathType.ATTR_WITH_PROP,
|
|
1217
|
+
element_name="display_label - non Jinja2",
|
|
1218
|
+
)
|
|
1219
|
+
if schema_path.attribute_schema and node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
|
|
1220
|
+
self.display_labels.register_attribute_based_display_label(
|
|
1221
|
+
kind=node.kind, attribute_name=schema_path.attribute_schema.name
|
|
1222
|
+
)
|
|
1223
|
+
return
|
|
1224
|
+
|
|
1225
|
+
jinja_template = Jinja2Template(template=node.display_label)
|
|
1226
|
+
try:
|
|
1227
|
+
variables = jinja_template.get_variables()
|
|
1228
|
+
jinja_template.validate(restricted=config.SETTINGS.security.restrict_untrusted_jinja2_filters)
|
|
1229
|
+
except (JinjaTemplateOperationViolationError, JinjaTemplateError) as exc:
|
|
1230
|
+
raise ValueError(
|
|
1231
|
+
f"{node.kind}: display_label is set to a jinja2 template, but has an invalid template: {exc.message}"
|
|
1232
|
+
) from exc
|
|
1233
|
+
|
|
1234
|
+
allowed_path_types = (
|
|
1235
|
+
SchemaElementPathType.ATTR_WITH_PROP
|
|
1236
|
+
| SchemaElementPathType.REL_ONE_MANDATORY_ATTR_WITH_PROP
|
|
1237
|
+
| SchemaElementPathType.REL_ONE_ATTR_WITH_PROP
|
|
1238
|
+
)
|
|
1239
|
+
for variable in variables:
|
|
1240
|
+
schema_path = self.validate_schema_path(
|
|
1241
|
+
node_schema=node, path=variable, allowed_path_types=allowed_path_types, element_name="display_label"
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
if schema_path.is_type_attribute and schema_path.active_attribute_schema.name == "display_label":
|
|
1245
|
+
raise ValueError(f"{node.kind}: display_label the '{variable}' variable is a reference to itself")
|
|
1246
|
+
|
|
1247
|
+
if node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
|
|
1248
|
+
self.display_labels.register_template_schema_path(
|
|
1249
|
+
kind=node.kind, schema_path=schema_path, template=node.display_label
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1147
1252
|
def _validate_computed_attribute(self, node: NodeSchema, attribute: AttributeSchema) -> None:
|
|
1148
1253
|
if not attribute.computed_attribute or attribute.computed_attribute.kind == ComputedAttributeKind.USER:
|
|
1149
1254
|
return
|
|
@@ -1236,6 +1341,81 @@ class SchemaBranch:
|
|
|
1236
1341
|
f"{node.kind}: Relationship {rel.name!r} max_count must be 0 or greater than 1 when cardinality is MANY"
|
|
1237
1342
|
)
|
|
1238
1343
|
|
|
1344
|
+
def validate_inherited_relationships_fields(self) -> None:
|
|
1345
|
+
for name in self.node_names:
|
|
1346
|
+
node_schema = self.get(name=name, duplicate=False)
|
|
1347
|
+
if not node_schema.inherit_from:
|
|
1348
|
+
continue
|
|
1349
|
+
|
|
1350
|
+
self.validate_node_inherited_relationship_fields(node_schema)
|
|
1351
|
+
|
|
1352
|
+
def validate_node_inherited_relationship_fields(self, node_schema: NodeSchema) -> None:
|
|
1353
|
+
generics = [self.get(name=node_name, duplicate=False) for node_name in node_schema.inherit_from]
|
|
1354
|
+
relationship_names = [node.relationship_names for node in generics]
|
|
1355
|
+
related_relationship_names = set().union(
|
|
1356
|
+
*[
|
|
1357
|
+
set(relationship_name_a) & set(relationship_name_b)
|
|
1358
|
+
for index, relationship_name_a in enumerate(relationship_names)
|
|
1359
|
+
for relationship_name_b in relationship_names[index + 1 :]
|
|
1360
|
+
]
|
|
1361
|
+
)
|
|
1362
|
+
# Check that the relationship properties match
|
|
1363
|
+
# for every generic node in generics list having related relationship names
|
|
1364
|
+
for index, generic_a in enumerate(generics):
|
|
1365
|
+
for generic_b in generics[index + 1 :]:
|
|
1366
|
+
for relationship_name in related_relationship_names:
|
|
1367
|
+
try:
|
|
1368
|
+
relationship_a = generic_a.get_relationship(name=relationship_name)
|
|
1369
|
+
relationship_b = generic_b.get_relationship(name=relationship_name)
|
|
1370
|
+
except ValueError:
|
|
1371
|
+
continue
|
|
1372
|
+
|
|
1373
|
+
matched, _property = self._check_relationship_properties_match(
|
|
1374
|
+
relationship_a=relationship_a, relationship_b=relationship_b
|
|
1375
|
+
)
|
|
1376
|
+
if not matched:
|
|
1377
|
+
raise ValueError(
|
|
1378
|
+
f"{node_schema.kind} inherits from '{generic_a.kind}' & '{generic_b.kind}'"
|
|
1379
|
+
f" with different '{_property}' on the '{relationship_name}' relationship"
|
|
1380
|
+
)
|
|
1381
|
+
|
|
1382
|
+
def _check_relationship_properties_match(
|
|
1383
|
+
self, relationship_a: RelationshipSchema, relationship_b: RelationshipSchema
|
|
1384
|
+
) -> tuple[bool, str | None]:
|
|
1385
|
+
compulsorily_matching_properties = (
|
|
1386
|
+
"name",
|
|
1387
|
+
"peer",
|
|
1388
|
+
"kind",
|
|
1389
|
+
"identifier",
|
|
1390
|
+
"cardinality",
|
|
1391
|
+
"min_count",
|
|
1392
|
+
"max_count",
|
|
1393
|
+
"common_parent",
|
|
1394
|
+
"common_relatives",
|
|
1395
|
+
"optional",
|
|
1396
|
+
"branch",
|
|
1397
|
+
"direction",
|
|
1398
|
+
"on_delete",
|
|
1399
|
+
"read_only",
|
|
1400
|
+
"hierarchical",
|
|
1401
|
+
"allow_override",
|
|
1402
|
+
)
|
|
1403
|
+
for _property in compulsorily_matching_properties:
|
|
1404
|
+
if not hasattr(relationship_a, _property) or not hasattr(relationship_b, _property):
|
|
1405
|
+
continue
|
|
1406
|
+
|
|
1407
|
+
equal_delete_actions = (None, RelationshipDeleteBehavior.NO_ACTION)
|
|
1408
|
+
if (
|
|
1409
|
+
_property == "on_delete"
|
|
1410
|
+
and getattr(relationship_a, _property) in equal_delete_actions
|
|
1411
|
+
and getattr(relationship_b, _property) in equal_delete_actions
|
|
1412
|
+
):
|
|
1413
|
+
continue
|
|
1414
|
+
|
|
1415
|
+
if getattr(relationship_a, _property) != getattr(relationship_b, _property):
|
|
1416
|
+
return False, _property
|
|
1417
|
+
return True, None
|
|
1418
|
+
|
|
1239
1419
|
def process_dropdowns(self) -> None:
|
|
1240
1420
|
for name in self.all_names:
|
|
1241
1421
|
node = self.get(name=name, duplicate=False)
|
|
@@ -1364,6 +1544,34 @@ class SchemaBranch:
|
|
|
1364
1544
|
node.uniqueness_constraints = [hfid_uniqueness_constraint]
|
|
1365
1545
|
self.set(name=node.kind, schema=node)
|
|
1366
1546
|
|
|
1547
|
+
def register_human_friendly_id(self) -> None:
|
|
1548
|
+
"""Register HFID automations
|
|
1549
|
+
|
|
1550
|
+
Register the HFIDs after all processing and validation has been done.
|
|
1551
|
+
"""
|
|
1552
|
+
|
|
1553
|
+
self.hfids = HFIDs()
|
|
1554
|
+
for name in self.generic_names_without_templates + self.node_names:
|
|
1555
|
+
node_schema = self.get(name=name, duplicate=False)
|
|
1556
|
+
|
|
1557
|
+
if not node_schema.human_friendly_id:
|
|
1558
|
+
continue
|
|
1559
|
+
|
|
1560
|
+
allowed_types = SchemaElementPathType.ATTR_WITH_PROP | SchemaElementPathType.REL_ONE_MANDATORY_ATTR
|
|
1561
|
+
|
|
1562
|
+
for hfid_path in node_schema.human_friendly_id:
|
|
1563
|
+
schema_path = self.validate_schema_path(
|
|
1564
|
+
node_schema=node_schema,
|
|
1565
|
+
path=hfid_path,
|
|
1566
|
+
allowed_path_types=allowed_types,
|
|
1567
|
+
element_name="human_friendly_id",
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1570
|
+
if node_schema.is_node_schema and node_schema.namespace not in ["Schema", "Internal"]:
|
|
1571
|
+
self.hfids.register_hfid_schema_path(
|
|
1572
|
+
kind=node_schema.kind, schema_path=schema_path, hfid=node_schema.human_friendly_id
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1367
1575
|
def process_hierarchy(self) -> None:
|
|
1368
1576
|
for name in self.nodes.keys():
|
|
1369
1577
|
node = self.get_node(name=name, duplicate=False)
|
|
@@ -1619,10 +1827,48 @@ class SchemaBranch:
|
|
|
1619
1827
|
|
|
1620
1828
|
self.set(name=name, schema=node)
|
|
1621
1829
|
|
|
1830
|
+
def process_relationships_state(self) -> None:
|
|
1831
|
+
for name in self.node_names + self.generic_names_without_templates:
|
|
1832
|
+
node = self.get(name=name, duplicate=False)
|
|
1833
|
+
if node.id or (not node.id and not node.relationships):
|
|
1834
|
+
continue
|
|
1835
|
+
|
|
1836
|
+
filtered_relationships = [
|
|
1837
|
+
relationship for relationship in node.relationships if relationship.state != HashableModelState.ABSENT
|
|
1838
|
+
]
|
|
1839
|
+
if len(filtered_relationships) == len(node.relationships):
|
|
1840
|
+
continue
|
|
1841
|
+
updated_node = node.duplicate()
|
|
1842
|
+
updated_node.relationships = filtered_relationships
|
|
1843
|
+
self.set(name=name, schema=updated_node)
|
|
1844
|
+
|
|
1845
|
+
def process_attributes_state(self) -> None:
|
|
1846
|
+
for name in self.node_names + self.generic_names_without_templates:
|
|
1847
|
+
node = self.get(name=name, duplicate=False)
|
|
1848
|
+
if not node.attributes:
|
|
1849
|
+
continue
|
|
1850
|
+
|
|
1851
|
+
filtered_attributes = [
|
|
1852
|
+
attribute for attribute in node.attributes if attribute.state != HashableModelState.ABSENT
|
|
1853
|
+
]
|
|
1854
|
+
if len(filtered_attributes) == len(node.attributes):
|
|
1855
|
+
continue
|
|
1856
|
+
updated_node = node.duplicate()
|
|
1857
|
+
updated_node.attributes = filtered_attributes
|
|
1858
|
+
self.set(name=name, schema=updated_node)
|
|
1859
|
+
|
|
1860
|
+
def process_nodes_state(self) -> None:
|
|
1861
|
+
for name in self.node_names + self.generic_names_without_templates:
|
|
1862
|
+
node = self.get(name=name, duplicate=False)
|
|
1863
|
+
if not node.id and node.state == HashableModelState.ABSENT:
|
|
1864
|
+
self.delete(name=name)
|
|
1865
|
+
|
|
1622
1866
|
def _generate_weight_generics(self) -> None:
|
|
1623
1867
|
"""Generate order_weight for all generic schemas."""
|
|
1624
1868
|
for name in self.generic_names:
|
|
1625
1869
|
node = self.get(name=name, duplicate=False)
|
|
1870
|
+
if node.namespace == "Template":
|
|
1871
|
+
continue
|
|
1626
1872
|
|
|
1627
1873
|
items_to_update = [item for item in node.attributes + node.relationships if not item.order_weight]
|
|
1628
1874
|
|
|
@@ -1686,10 +1932,36 @@ class SchemaBranch:
|
|
|
1686
1932
|
|
|
1687
1933
|
self.set(name=name, schema=template)
|
|
1688
1934
|
|
|
1935
|
+
def _generate_generics_templates_weight(self) -> None:
|
|
1936
|
+
"""Generate order_weight for generic templates.
|
|
1937
|
+
|
|
1938
|
+
The order of the fields for the template must respect the order of the node.
|
|
1939
|
+
"""
|
|
1940
|
+
for name in self.generic_names:
|
|
1941
|
+
generic_node = self.get(name=name, duplicate=False)
|
|
1942
|
+
try:
|
|
1943
|
+
template = self.get(name=self._get_object_template_kind(node_kind=generic_node.kind), duplicate=True)
|
|
1944
|
+
except SchemaNotFoundError:
|
|
1945
|
+
continue
|
|
1946
|
+
|
|
1947
|
+
generic_node_weights = {
|
|
1948
|
+
item.name: item.order_weight
|
|
1949
|
+
for item in generic_node.attributes + generic_node.relationships
|
|
1950
|
+
if item.order_weight is not None
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
for item in template.attributes + template.relationships:
|
|
1954
|
+
if item.order_weight:
|
|
1955
|
+
continue
|
|
1956
|
+
item.order_weight = generic_node_weights[item.name] if item.name in generic_node_weights else None
|
|
1957
|
+
|
|
1958
|
+
self.set(name=template.kind, schema=template)
|
|
1959
|
+
|
|
1689
1960
|
def generate_weight(self) -> None:
|
|
1690
1961
|
self._generate_weight_generics()
|
|
1691
1962
|
self._generate_weight_nodes_profiles()
|
|
1692
1963
|
self._generate_weight_templates()
|
|
1964
|
+
self._generate_generics_templates_weight()
|
|
1693
1965
|
|
|
1694
1966
|
def cleanup_inherited_elements(self) -> None:
|
|
1695
1967
|
for name in self.node_names:
|
|
@@ -1942,7 +2214,7 @@ class SchemaBranch:
|
|
|
1942
2214
|
|
|
1943
2215
|
profiles_rel_settings: dict[str, Any] = {
|
|
1944
2216
|
"name": "profiles",
|
|
1945
|
-
"identifier":
|
|
2217
|
+
"identifier": PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
|
|
1946
2218
|
"peer": InfrahubKind.PROFILE,
|
|
1947
2219
|
"kind": RelationshipKind.PROFILE,
|
|
1948
2220
|
"cardinality": RelationshipCardinality.MANY,
|
|
@@ -2005,7 +2277,7 @@ class SchemaBranch:
|
|
|
2005
2277
|
relationships=[
|
|
2006
2278
|
RelationshipSchema(
|
|
2007
2279
|
name="related_nodes",
|
|
2008
|
-
identifier=
|
|
2280
|
+
identifier=PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
|
|
2009
2281
|
peer=node.kind,
|
|
2010
2282
|
kind=RelationshipKind.PROFILE,
|
|
2011
2283
|
cardinality=RelationshipCardinality.MANY,
|
|
@@ -2015,7 +2287,7 @@ class SchemaBranch:
|
|
|
2015
2287
|
)
|
|
2016
2288
|
|
|
2017
2289
|
for node_attr in node.attributes:
|
|
2018
|
-
if
|
|
2290
|
+
if not node_attr.support_profiles:
|
|
2019
2291
|
continue
|
|
2020
2292
|
attr_schema_class = get_attribute_schema_class_for_kind(kind=node_attr.kind)
|
|
2021
2293
|
attr = attr_schema_class(
|
|
@@ -2320,3 +2592,16 @@ class SchemaBranch:
|
|
|
2320
2592
|
updated_used_by_node = set(chain(template_schema_kinds, set(core_node_schema.used_by)))
|
|
2321
2593
|
core_node_schema.used_by = sorted(updated_used_by_node)
|
|
2322
2594
|
self.set(name=InfrahubKind.NODE, schema=core_node_schema)
|
|
2595
|
+
|
|
2596
|
+
|
|
2597
|
+
def _format_display_label_component(component: str) -> str:
|
|
2598
|
+
"""Return correct format for display_label.
|
|
2599
|
+
|
|
2600
|
+
Previously both the format of 'name' and 'name__value' was
|
|
2601
|
+
supported this function ensures that the proper 'name__value'
|
|
2602
|
+
format is used
|
|
2603
|
+
"""
|
|
2604
|
+
if "__" in component:
|
|
2605
|
+
return component
|
|
2606
|
+
|
|
2607
|
+
return f"{component}__value"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from infrahub.core.schema import SchemaAttributePath
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TemplateLabel:
|
|
14
|
+
template: str
|
|
15
|
+
attributes: set[str] = field(default_factory=set)
|
|
16
|
+
relationships: set[str] = field(default_factory=set)
|
|
17
|
+
relationship_fields: dict[str, set[str]] = field(default_factory=dict)
|
|
18
|
+
filter_key: str = "ids"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def fields(self) -> list[str]:
|
|
22
|
+
return sorted(list(self.attributes) + list(self.relationships))
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def has_related_components(self) -> bool:
|
|
26
|
+
"""Indicate if the associated template use variables from relationships"""
|
|
27
|
+
return len(self.relationships) > 0
|
|
28
|
+
|
|
29
|
+
def get_hash(self) -> str:
|
|
30
|
+
return hashlib.md5(self.template.encode(), usedforsecurity=False).hexdigest()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class RelationshipIdentifier:
|
|
35
|
+
kind: str
|
|
36
|
+
filter_key: str
|
|
37
|
+
template: str
|
|
38
|
+
|
|
39
|
+
def __hash__(self) -> int:
|
|
40
|
+
return hash(f"{self.kind}::{self.filter_key}::{self.template}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class RelationshipTriggers:
|
|
45
|
+
attributes: dict[str, set[RelationshipIdentifier]] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DisplayLabels:
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
template_based_display_labels: dict[str, TemplateLabel] | None = None,
|
|
52
|
+
template_relationship_triggers: dict[str, RelationshipTriggers] | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._template_based_display_labels: dict[str, TemplateLabel] = template_based_display_labels or {}
|
|
55
|
+
self._template_relationship_triggers: dict[str, RelationshipTriggers] = template_relationship_triggers or {}
|
|
56
|
+
|
|
57
|
+
def duplicate(self) -> DisplayLabels:
|
|
58
|
+
"""Clone the current object."""
|
|
59
|
+
return self.__class__(
|
|
60
|
+
template_based_display_labels=deepcopy(self._template_based_display_labels),
|
|
61
|
+
template_relationship_triggers=deepcopy(self._template_relationship_triggers),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def register_attribute_based_display_label(self, kind: str, attribute_name: str) -> None:
|
|
65
|
+
"""Register nodes where the display label consists of a single defined attribute name."""
|
|
66
|
+
self._template_based_display_labels[kind] = TemplateLabel(
|
|
67
|
+
template=f"{{{{ {attribute_name}__value }}}}", attributes={attribute_name}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def register_template_schema_path(self, kind: str, schema_path: SchemaAttributePath, template: str) -> None:
|
|
71
|
+
"""Register Jinja2 template based display labels using the schema path of each impacted variable in the node."""
|
|
72
|
+
|
|
73
|
+
if kind not in self._template_based_display_labels:
|
|
74
|
+
self._template_based_display_labels[kind] = TemplateLabel(template=template)
|
|
75
|
+
|
|
76
|
+
if schema_path.is_type_attribute:
|
|
77
|
+
self._template_based_display_labels[kind].attributes.add(schema_path.active_attribute_schema.name)
|
|
78
|
+
elif schema_path.is_type_relationship and schema_path.related_schema:
|
|
79
|
+
self._template_based_display_labels[kind].relationships.add(schema_path.active_relationship_schema.name)
|
|
80
|
+
if (
|
|
81
|
+
schema_path.active_relationship_schema.name
|
|
82
|
+
not in self._template_based_display_labels[kind].relationship_fields
|
|
83
|
+
):
|
|
84
|
+
self._template_based_display_labels[kind].relationship_fields[
|
|
85
|
+
schema_path.active_relationship_schema.name
|
|
86
|
+
] = set()
|
|
87
|
+
self._template_based_display_labels[kind].relationship_fields[
|
|
88
|
+
schema_path.active_relationship_schema.name
|
|
89
|
+
].add(schema_path.active_attribute_schema.name)
|
|
90
|
+
|
|
91
|
+
if schema_path.related_schema.kind not in self._template_relationship_triggers:
|
|
92
|
+
self._template_relationship_triggers[schema_path.related_schema.kind] = RelationshipTriggers()
|
|
93
|
+
if (
|
|
94
|
+
schema_path.active_attribute_schema.name
|
|
95
|
+
not in self._template_relationship_triggers[schema_path.related_schema.kind].attributes
|
|
96
|
+
):
|
|
97
|
+
self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
|
|
98
|
+
schema_path.active_attribute_schema.name
|
|
99
|
+
] = set()
|
|
100
|
+
self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
|
|
101
|
+
schema_path.active_attribute_schema.name
|
|
102
|
+
].add(
|
|
103
|
+
RelationshipIdentifier(
|
|
104
|
+
kind=kind, filter_key=f"{schema_path.active_relationship_schema.name}__ids", template=template
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def targets_node(self, kind: str) -> bool:
|
|
109
|
+
"""Indicates if there is a display_label defined for the targeted node"""
|
|
110
|
+
return kind in self._template_based_display_labels
|
|
111
|
+
|
|
112
|
+
def get_template_node(self, kind: str) -> TemplateLabel:
|
|
113
|
+
"""Return node kinds together with their template definitions."""
|
|
114
|
+
return self._template_based_display_labels[kind]
|
|
115
|
+
|
|
116
|
+
def get_template_nodes(self) -> dict[str, TemplateLabel]:
|
|
117
|
+
"""Return node kinds together with their template definitions."""
|
|
118
|
+
return self._template_based_display_labels
|
|
119
|
+
|
|
120
|
+
def get_related_trigger_nodes(self) -> dict[str, RelationshipTriggers]:
|
|
121
|
+
"""Return node kinds that other nodes use within their templates for display_labels."""
|
|
122
|
+
return self._template_relationship_triggers
|
|
123
|
+
|
|
124
|
+
def get_related_template(self, related_kind: str, target_kind: str) -> TemplateLabel:
|
|
125
|
+
relationship_trigger = self._template_relationship_triggers[related_kind]
|
|
126
|
+
for applicable_kinds in relationship_trigger.attributes.values():
|
|
127
|
+
for relationship_identifier in applicable_kinds:
|
|
128
|
+
if target_kind == relationship_identifier.kind:
|
|
129
|
+
template_label = self.get_template_node(kind=target_kind)
|
|
130
|
+
template_label.filter_key = relationship_identifier.filter_key
|
|
131
|
+
return template_label
|
|
132
|
+
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Unable to find registered template for {target_kind} registered on related node {related_kind}"
|
|
135
|
+
)
|