infrahub-server 1.5.0b0__py3-none-any.whl → 1.5.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/actions/tasks.py +8 -0
- infrahub/api/diff/diff.py +1 -1
- infrahub/cli/db.py +24 -0
- infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
- infrahub/core/attribute.py +3 -3
- infrahub/core/branch/tasks.py +2 -1
- infrahub/core/changelog/models.py +4 -12
- infrahub/core/constants/infrahubkind.py +1 -0
- infrahub/core/diff/model/path.py +4 -0
- infrahub/core/diff/payload_builder.py +1 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/ipam/utilization.py +1 -1
- infrahub/core/manager.py +6 -3
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
- infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
- infrahub/core/migrations/schema/node_attribute_add.py +5 -2
- infrahub/core/migrations/shared.py +5 -6
- infrahub/core/node/__init__.py +142 -40
- infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
- infrahub/core/node/node_property_attribute.py +230 -0
- infrahub/core/node/standard.py +1 -1
- infrahub/core/protocols.py +7 -1
- infrahub/core/query/node.py +14 -1
- infrahub/core/registry.py +2 -2
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/model.py +1 -1
- infrahub/core/schema/basenode_schema.py +42 -2
- infrahub/core/schema/definitions/core/__init__.py +2 -0
- infrahub/core/schema/definitions/core/generator.py +2 -0
- infrahub/core/schema/definitions/core/group.py +16 -2
- infrahub/core/schema/definitions/internal.py +14 -1
- infrahub/core/schema/generated/base_node_schema.py +6 -1
- infrahub/core/schema/node_schema.py +5 -2
- infrahub/core/schema/schema_branch.py +134 -0
- infrahub/core/schema/schema_branch_display.py +123 -0
- infrahub/core/schema/schema_branch_hfid.py +114 -0
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/determiner.py +12 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/core/validators/tasks.py +1 -1
- infrahub/display_labels/__init__.py +0 -0
- infrahub/display_labels/gather.py +48 -0
- infrahub/display_labels/models.py +240 -0
- infrahub/display_labels/tasks.py +186 -0
- infrahub/display_labels/triggers.py +22 -0
- infrahub/events/group_action.py +1 -1
- infrahub/events/node_action.py +1 -1
- infrahub/generators/constants.py +7 -0
- infrahub/generators/models.py +7 -0
- infrahub/generators/tasks.py +31 -15
- infrahub/git/integrator.py +22 -14
- infrahub/graphql/analyzer.py +1 -1
- infrahub/graphql/mutations/display_label.py +111 -0
- infrahub/graphql/mutations/generator.py +25 -7
- infrahub/graphql/mutations/hfid.py +118 -0
- infrahub/graphql/mutations/relationship.py +2 -2
- infrahub/graphql/mutations/resource_manager.py +2 -2
- infrahub/graphql/mutations/schema.py +5 -5
- infrahub/graphql/queries/resource_manager.py +1 -1
- infrahub/graphql/resolvers/resolver.py +2 -0
- infrahub/graphql/schema.py +4 -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 +185 -0
- infrahub/hfid/triggers.py +22 -0
- infrahub/lock.py +15 -4
- infrahub/middleware.py +26 -1
- infrahub/proposed_change/tasks.py +10 -1
- infrahub/server.py +16 -3
- infrahub/services/__init__.py +8 -5
- infrahub/trigger/catalogue.py +4 -0
- infrahub/trigger/models.py +2 -0
- infrahub/trigger/tasks.py +3 -0
- infrahub/workflows/catalogue.py +72 -0
- infrahub/workflows/initialization.py +16 -0
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/ctl/cli_commands.py +2 -0
- infrahub_sdk/ctl/generator.py +4 -0
- infrahub_sdk/ctl/graphql.py +184 -0
- infrahub_sdk/ctl/schema.py +6 -2
- 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} +81 -73
- infrahub_sdk/graphql/utils.py +40 -0
- infrahub_sdk/protocols.py +14 -0
- infrahub_sdk/schema/__init__.py +38 -0
- infrahub_sdk/schema/repository.py +8 -0
- infrahub_sdk/spec/object.py +84 -10
- infrahub_sdk/spec/range_expansion.py +1 -1
- infrahub_sdk/transforms.py +1 -1
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +5 -4
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +104 -79
- infrahub_testcontainers/container.py +1 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.5.0b0.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -9,7 +9,7 @@ from enum import Enum
|
|
|
9
9
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, overload
|
|
10
10
|
|
|
11
11
|
from infrahub_sdk.utils import compare_lists, intersection
|
|
12
|
-
from pydantic import field_validator
|
|
12
|
+
from pydantic import ConfigDict, field_validator
|
|
13
13
|
|
|
14
14
|
from infrahub.core.constants import HashableModelState, RelationshipCardinality, RelationshipKind
|
|
15
15
|
from infrahub.core.models import HashableModel, HashableModelDiff
|
|
@@ -19,6 +19,7 @@ from .generated.base_node_schema import GeneratedBaseNodeSchema
|
|
|
19
19
|
from .relationship_schema import RelationshipSchema
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
+
from pydantic.config import JsonDict
|
|
22
23
|
from typing_extensions import Self
|
|
23
24
|
|
|
24
25
|
from infrahub.core.schema import GenericSchema, NodeSchema
|
|
@@ -26,6 +27,7 @@ if TYPE_CHECKING:
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
NODE_METADATA_ATTRIBUTES = ["_source", "_owner"]
|
|
30
|
+
NODE_PROPERTY_ATTRIBUTES = ["display_label", "human_friendly_id"]
|
|
29
31
|
INHERITED = "INHERITED"
|
|
30
32
|
|
|
31
33
|
OPTIONAL_TEXT_FIELDS = [
|
|
@@ -39,10 +41,43 @@ OPTIONAL_TEXT_FIELDS = [
|
|
|
39
41
|
]
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
def _json_schema_extra(schema: JsonDict) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Mutate the generated JSON Schema in place to:
|
|
47
|
+
- allow `null` for `display_labels`
|
|
48
|
+
- mark the non-null branch as deprecated
|
|
49
|
+
"""
|
|
50
|
+
props = schema.get("properties")
|
|
51
|
+
if not isinstance(props, dict):
|
|
52
|
+
return
|
|
53
|
+
dl = props.get("display_labels")
|
|
54
|
+
if not isinstance(dl, dict):
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if "anyOf" in dl:
|
|
58
|
+
dl["anyOf"] = [
|
|
59
|
+
{
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"deprecationMessage": "display_labels are deprecated use display_label instead",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{"type": "null"},
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
42
70
|
class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
43
71
|
_exclude_from_hash: list[str] = ["attributes", "relationships"]
|
|
44
72
|
_sort_by: list[str] = ["namespace", "name"]
|
|
45
73
|
|
|
74
|
+
model_config = ConfigDict(extra="forbid", json_schema_extra=_json_schema_extra)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_schema_node(self) -> bool:
|
|
78
|
+
"""Tell if this node represent a part of the schema. Not to confuse this with `is_node_schema`."""
|
|
79
|
+
return self.namespace == "Schema"
|
|
80
|
+
|
|
46
81
|
@property
|
|
47
82
|
def is_node_schema(self) -> bool:
|
|
48
83
|
return False
|
|
@@ -240,6 +275,11 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
240
275
|
return None
|
|
241
276
|
|
|
242
277
|
def get_attribute(self, name: str) -> AttributeSchema:
|
|
278
|
+
if name == "human_friendly_id":
|
|
279
|
+
return AttributeSchema(name="human_friendly_id", kind="List", optional=True, branch=self.branch)
|
|
280
|
+
if name == "display_label":
|
|
281
|
+
return AttributeSchema(name="display_label", kind="Text", optional=True, branch=self.branch)
|
|
282
|
+
|
|
243
283
|
for item in self.attributes:
|
|
244
284
|
if item.name == name:
|
|
245
285
|
return item
|
|
@@ -329,7 +369,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
329
369
|
|
|
330
370
|
@property
|
|
331
371
|
def valid_input_names(self) -> list[str]:
|
|
332
|
-
return self.attribute_names + self.relationship_names + NODE_METADATA_ATTRIBUTES
|
|
372
|
+
return self.attribute_names + self.relationship_names + NODE_METADATA_ATTRIBUTES + NODE_PROPERTY_ATTRIBUTES
|
|
333
373
|
|
|
334
374
|
@property
|
|
335
375
|
def valid_local_names(self) -> list[str]:
|
|
@@ -29,6 +29,7 @@ from .core import core_node, core_task_target
|
|
|
29
29
|
from .generator import core_generator_definition, core_generator_instance
|
|
30
30
|
from .graphql_query import core_graphql_query
|
|
31
31
|
from .group import (
|
|
32
|
+
core_generator_aware_group,
|
|
32
33
|
core_generator_group,
|
|
33
34
|
core_graphql_query_group,
|
|
34
35
|
core_group,
|
|
@@ -128,6 +129,7 @@ core_models_mixed: dict[str, list] = {
|
|
|
128
129
|
core_group_action,
|
|
129
130
|
core_standard_group,
|
|
130
131
|
core_generator_group,
|
|
132
|
+
core_generator_aware_group,
|
|
131
133
|
core_graphql_query_group,
|
|
132
134
|
core_repository_group,
|
|
133
135
|
builtin_tag,
|
|
@@ -33,6 +33,8 @@ core_generator_definition = NodeSchema(
|
|
|
33
33
|
Attr(name="file_path", kind="Text"),
|
|
34
34
|
Attr(name="class_name", kind="Text"),
|
|
35
35
|
Attr(name="convert_query_response", kind="Boolean", optional=True, default_value=False),
|
|
36
|
+
Attr(name="execute_in_proposed_change", kind="Boolean", optional=True, default_value=True),
|
|
37
|
+
Attr(name="execute_after_merge", kind="Boolean", optional=True, default_value=True),
|
|
36
38
|
],
|
|
37
39
|
relationships=[
|
|
38
40
|
Rel(
|
|
@@ -70,10 +70,10 @@ core_standard_group = NodeSchema(
|
|
|
70
70
|
core_generator_group = NodeSchema(
|
|
71
71
|
name="GeneratorGroup",
|
|
72
72
|
namespace="Core",
|
|
73
|
-
description="Group of nodes that are created by a generator.",
|
|
73
|
+
description="Group of nodes that are created by a generator. (local)",
|
|
74
74
|
include_in_menu=False,
|
|
75
75
|
icon="mdi:state-machine",
|
|
76
|
-
label="Generator Group",
|
|
76
|
+
label="Generator Group (local)",
|
|
77
77
|
default_filter="name__value",
|
|
78
78
|
order_by=["name__value"],
|
|
79
79
|
display_labels=["name__value"],
|
|
@@ -82,6 +82,20 @@ core_generator_group = NodeSchema(
|
|
|
82
82
|
generate_profile=False,
|
|
83
83
|
)
|
|
84
84
|
|
|
85
|
+
core_generator_aware_group = NodeSchema(
|
|
86
|
+
name="GeneratorAwareGroup",
|
|
87
|
+
namespace="Core",
|
|
88
|
+
description="Group of nodes that are created by a generator. (Aware)",
|
|
89
|
+
include_in_menu=False,
|
|
90
|
+
icon="mdi:state-machine",
|
|
91
|
+
label="Generator Group (aware)",
|
|
92
|
+
default_filter="name__value",
|
|
93
|
+
order_by=["name__value"],
|
|
94
|
+
display_labels=["name__value"],
|
|
95
|
+
branch=BranchSupportType.AWARE,
|
|
96
|
+
inherit_from=[InfrahubKind.GENERICGROUP],
|
|
97
|
+
generate_profile=False,
|
|
98
|
+
)
|
|
85
99
|
|
|
86
100
|
core_graphql_query_group = NodeSchema(
|
|
87
101
|
name="GraphQLQueryGroup",
|
|
@@ -179,7 +179,9 @@ class SchemaNode(BaseModel):
|
|
|
179
179
|
default_filter: str | None = None
|
|
180
180
|
attributes: list[SchemaAttribute]
|
|
181
181
|
relationships: list[SchemaRelationship]
|
|
182
|
+
display_label: str | None = None
|
|
182
183
|
display_labels: list[str]
|
|
184
|
+
uniqueness_constraints: list[list[str]] | None = None
|
|
183
185
|
|
|
184
186
|
def to_dict(self) -> dict[str, Any]:
|
|
185
187
|
return {
|
|
@@ -194,7 +196,9 @@ class SchemaNode(BaseModel):
|
|
|
194
196
|
if attribute.name not in ["id", "attributes", "relationships"]
|
|
195
197
|
],
|
|
196
198
|
"relationships": [relationship.to_dict() for relationship in self.relationships],
|
|
199
|
+
"display_label": self.display_label,
|
|
197
200
|
"display_labels": self.display_labels,
|
|
201
|
+
"uniqueness_constraints": self.uniqueness_constraints,
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
def without_duplicates(self, other: SchemaNode) -> SchemaNode:
|
|
@@ -292,11 +296,18 @@ base_node_schema = SchemaNode(
|
|
|
292
296
|
optional=True,
|
|
293
297
|
extra={"update": UpdateSupport.ALLOWED},
|
|
294
298
|
),
|
|
299
|
+
SchemaAttribute(
|
|
300
|
+
name="display_label",
|
|
301
|
+
kind="Text",
|
|
302
|
+
description="Attribute or Jinja2 template to use to generate the display label",
|
|
303
|
+
optional=True,
|
|
304
|
+
extra={"update": UpdateSupport.ALLOWED},
|
|
305
|
+
),
|
|
295
306
|
SchemaAttribute(
|
|
296
307
|
name="display_labels",
|
|
297
308
|
kind="List",
|
|
298
309
|
internal_kind=str,
|
|
299
|
-
description="List of attributes to use to generate the display label",
|
|
310
|
+
description="List of attributes to use to generate the display label (deprecated)",
|
|
300
311
|
optional=True,
|
|
301
312
|
extra={"update": UpdateSupport.ALLOWED},
|
|
302
313
|
),
|
|
@@ -465,6 +476,7 @@ attribute_schema = SchemaNode(
|
|
|
465
476
|
include_in_menu=False,
|
|
466
477
|
default_filter=None,
|
|
467
478
|
display_labels=["name__value"],
|
|
479
|
+
uniqueness_constraints=[["name__value", "node"]],
|
|
468
480
|
attributes=[
|
|
469
481
|
SchemaAttribute(
|
|
470
482
|
name="id",
|
|
@@ -669,6 +681,7 @@ relationship_schema = SchemaNode(
|
|
|
669
681
|
include_in_menu=False,
|
|
670
682
|
default_filter=None,
|
|
671
683
|
display_labels=["name__value"],
|
|
684
|
+
uniqueness_constraints=[["name__value", "node"]],
|
|
672
685
|
attributes=[
|
|
673
686
|
SchemaAttribute(
|
|
674
687
|
name="id",
|
|
@@ -58,9 +58,14 @@ class GeneratedBaseNodeSchema(HashableModel):
|
|
|
58
58
|
description="Human friendly and unique identifier for the object.",
|
|
59
59
|
json_schema_extra={"update": "allowed"},
|
|
60
60
|
)
|
|
61
|
+
display_label: str | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description="Attribute or Jinja2 template to use to generate the display label",
|
|
64
|
+
json_schema_extra={"update": "allowed"},
|
|
65
|
+
)
|
|
61
66
|
display_labels: list[str] | None = Field(
|
|
62
67
|
default=None,
|
|
63
|
-
description="List of attributes to use to generate the display label",
|
|
68
|
+
description="List of attributes to use to generate the display label (deprecated)",
|
|
64
69
|
json_schema_extra={"update": "allowed"},
|
|
65
70
|
)
|
|
66
71
|
include_in_menu: bool | None = Field(
|
|
@@ -90,6 +90,7 @@ class NodeSchema(GeneratedNodeSchema):
|
|
|
90
90
|
|
|
91
91
|
properties_to_inherit = [
|
|
92
92
|
"human_friendly_id",
|
|
93
|
+
"display_label",
|
|
93
94
|
"display_labels",
|
|
94
95
|
"default_filter",
|
|
95
96
|
"menu_placement",
|
|
@@ -129,10 +130,12 @@ class NodeSchema(GeneratedNodeSchema):
|
|
|
129
130
|
item_idx = existing_inherited_relationships[relationship.name]
|
|
130
131
|
self.relationships[item_idx].update_from_generic(other=new_relationship)
|
|
131
132
|
|
|
132
|
-
def get_hierarchy_schema(
|
|
133
|
+
def get_hierarchy_schema(
|
|
134
|
+
self, db: InfrahubDatabase, branch: Branch | str | None = None, duplicate: bool = False
|
|
135
|
+
) -> GenericSchema:
|
|
133
136
|
if not self.hierarchy:
|
|
134
137
|
raise ValueError("The node is not part of a hierarchy")
|
|
135
|
-
schema = db.schema.get(name=self.hierarchy, branch=branch)
|
|
138
|
+
schema = db.schema.get(name=self.hierarchy, branch=branch, duplicate=duplicate)
|
|
136
139
|
if not isinstance(schema, GenericSchema):
|
|
137
140
|
raise TypeError
|
|
138
141
|
return schema
|
|
@@ -66,6 +66,8 @@ from ... import config
|
|
|
66
66
|
from ..constants.schema import PARENT_CHILD_IDENTIFIER
|
|
67
67
|
from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
|
|
68
68
|
from .schema_branch_computed import ComputedAttributes
|
|
69
|
+
from .schema_branch_display import DisplayLabels
|
|
70
|
+
from .schema_branch_hfid import HFIDs
|
|
69
71
|
|
|
70
72
|
log = get_logger()
|
|
71
73
|
|
|
@@ -77,6 +79,8 @@ class SchemaBranch:
|
|
|
77
79
|
name: str | None = None,
|
|
78
80
|
data: dict[str, dict[str, str]] | None = None,
|
|
79
81
|
computed_attributes: ComputedAttributes | None = None,
|
|
82
|
+
display_labels: DisplayLabels | None = None,
|
|
83
|
+
hfids: HFIDs | None = None,
|
|
80
84
|
):
|
|
81
85
|
self._cache: dict[str, NodeSchema | GenericSchema] = cache
|
|
82
86
|
self.name: str | None = name
|
|
@@ -85,6 +89,8 @@ class SchemaBranch:
|
|
|
85
89
|
self.profiles: dict[str, str] = {}
|
|
86
90
|
self.templates: dict[str, str] = {}
|
|
87
91
|
self.computed_attributes = computed_attributes or ComputedAttributes()
|
|
92
|
+
self.display_labels = display_labels or DisplayLabels()
|
|
93
|
+
self.hfids = hfids or HFIDs()
|
|
88
94
|
|
|
89
95
|
if data:
|
|
90
96
|
self.nodes = data.get("nodes", {})
|
|
@@ -270,6 +276,8 @@ class SchemaBranch:
|
|
|
270
276
|
data=copy.deepcopy(self.to_dict()),
|
|
271
277
|
cache=self._cache,
|
|
272
278
|
computed_attributes=self.computed_attributes.duplicate(),
|
|
279
|
+
display_labels=self.display_labels.duplicate(),
|
|
280
|
+
hfids=self.hfids.duplicate(),
|
|
273
281
|
)
|
|
274
282
|
|
|
275
283
|
def set(self, name: str, schema: MainSchemaTypes) -> str:
|
|
@@ -504,6 +512,9 @@ class SchemaBranch:
|
|
|
504
512
|
self.process_post_validation()
|
|
505
513
|
|
|
506
514
|
def process_pre_validation(self) -> None:
|
|
515
|
+
self.process_nodes_state()
|
|
516
|
+
self.process_attributes_state()
|
|
517
|
+
self.process_relationships_state()
|
|
507
518
|
self.generate_identifiers()
|
|
508
519
|
self.process_default_values()
|
|
509
520
|
self.process_deprecations()
|
|
@@ -529,6 +540,7 @@ class SchemaBranch:
|
|
|
529
540
|
self.validate_identifiers()
|
|
530
541
|
self.sync_uniqueness_constraints_and_unique_attributes()
|
|
531
542
|
self.validate_uniqueness_constraints()
|
|
543
|
+
self.validate_display_label()
|
|
532
544
|
self.validate_display_labels()
|
|
533
545
|
self.validate_order_by()
|
|
534
546
|
self.validate_default_filters()
|
|
@@ -752,6 +764,35 @@ class SchemaBranch:
|
|
|
752
764
|
element_name=element_name,
|
|
753
765
|
)
|
|
754
766
|
|
|
767
|
+
def validate_display_label(self) -> None:
|
|
768
|
+
self.display_labels = DisplayLabels()
|
|
769
|
+
for name in self.all_names:
|
|
770
|
+
node_schema = self.get(name=name, duplicate=False)
|
|
771
|
+
|
|
772
|
+
if node_schema.display_label is None and node_schema.display_labels:
|
|
773
|
+
update_candidate = self.get(name=name, duplicate=True)
|
|
774
|
+
if len(node_schema.display_labels) == 1:
|
|
775
|
+
# If the previous display_labels consist of a single attribute convert
|
|
776
|
+
# it to an attribute based display label
|
|
777
|
+
converted_display_label = node_schema.display_labels[0]
|
|
778
|
+
if "__" not in converted_display_label:
|
|
779
|
+
# Previously we allowed defining a raw attribute name as a component of a
|
|
780
|
+
# display_label, if this is the case we need to append '__value'
|
|
781
|
+
converted_display_label = f"{converted_display_label}__value"
|
|
782
|
+
update_candidate.display_label = converted_display_label
|
|
783
|
+
else:
|
|
784
|
+
# If the previous display label consists of multiple attributes
|
|
785
|
+
# convert it to a Jinja2 based display label
|
|
786
|
+
update_candidate.display_label = " ".join(
|
|
787
|
+
[f"{{{{ {display_label} }}}}" for display_label in node_schema.display_labels]
|
|
788
|
+
)
|
|
789
|
+
self.set(name=name, schema=update_candidate)
|
|
790
|
+
|
|
791
|
+
if not node_schema.display_label:
|
|
792
|
+
continue
|
|
793
|
+
|
|
794
|
+
self._validate_display_label(node=node_schema)
|
|
795
|
+
|
|
755
796
|
def validate_display_labels(self) -> None:
|
|
756
797
|
for name in self.all_names:
|
|
757
798
|
node_schema = self.get(name=name, duplicate=False)
|
|
@@ -849,6 +890,7 @@ class SchemaBranch:
|
|
|
849
890
|
return False
|
|
850
891
|
|
|
851
892
|
def validate_human_friendly_id(self) -> None:
|
|
893
|
+
self.hfids = HFIDs()
|
|
852
894
|
for name in self.generic_names_without_templates + self.node_names:
|
|
853
895
|
node_schema = self.get(name=name, duplicate=False)
|
|
854
896
|
|
|
@@ -860,7 +902,14 @@ class SchemaBranch:
|
|
|
860
902
|
# Mapping relationship identifiers -> list of attributes paths
|
|
861
903
|
rel_schemas_to_paths: dict[str, tuple[MainSchemaTypes, list[str]]] = {}
|
|
862
904
|
|
|
905
|
+
visited_paths: list[str] = []
|
|
863
906
|
for hfid_path in node_schema.human_friendly_id:
|
|
907
|
+
if config.SETTINGS.main.schema_strict_mode and hfid_path in visited_paths:
|
|
908
|
+
raise ValidationError(
|
|
909
|
+
f"HFID of {node_schema.kind} cannot use the same path more than once: {hfid_path}"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
visited_paths.append(hfid_path)
|
|
864
913
|
schema_path = self.validate_schema_path(
|
|
865
914
|
node_schema=node_schema,
|
|
866
915
|
path=hfid_path,
|
|
@@ -875,6 +924,11 @@ class SchemaBranch:
|
|
|
875
924
|
rel_schemas_to_paths[rel_identifier] = (schema_path.related_schema, [])
|
|
876
925
|
rel_schemas_to_paths[rel_identifier][1].append(schema_path.attribute_path_as_str)
|
|
877
926
|
|
|
927
|
+
if node_schema.is_node_schema and node_schema.namespace not in ["Schema", "Internal"]:
|
|
928
|
+
self.hfids.register_hfid_schema_path(
|
|
929
|
+
kind=node_schema.kind, schema_path=schema_path, hfid=node_schema.human_friendly_id
|
|
930
|
+
)
|
|
931
|
+
|
|
878
932
|
if config.SETTINGS.main.schema_strict_mode:
|
|
879
933
|
# For every relationship referred within hfid, check whether the combination of attributes is unique is the peer schema node
|
|
880
934
|
for related_schema, attrs_paths in rel_schemas_to_paths.values():
|
|
@@ -1137,6 +1191,50 @@ class SchemaBranch:
|
|
|
1137
1191
|
node=node_schema, attribute=attribute, generic=generic_schema
|
|
1138
1192
|
)
|
|
1139
1193
|
|
|
1194
|
+
def _validate_display_label(self, node: MainSchemaTypes) -> None:
|
|
1195
|
+
if not node.display_label:
|
|
1196
|
+
return
|
|
1197
|
+
|
|
1198
|
+
if not any(c in node.display_label for c in "{}"):
|
|
1199
|
+
schema_path = self.validate_schema_path(
|
|
1200
|
+
node_schema=node,
|
|
1201
|
+
path=node.display_label,
|
|
1202
|
+
allowed_path_types=SchemaElementPathType.ATTR_WITH_PROP,
|
|
1203
|
+
element_name="display_label - non Jinja2",
|
|
1204
|
+
)
|
|
1205
|
+
if schema_path.attribute_schema and node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
|
|
1206
|
+
self.display_labels.register_attribute_based_display_label(
|
|
1207
|
+
kind=node.kind, attribute_name=schema_path.attribute_schema.name
|
|
1208
|
+
)
|
|
1209
|
+
return
|
|
1210
|
+
|
|
1211
|
+
jinja_template = Jinja2Template(template=node.display_label)
|
|
1212
|
+
try:
|
|
1213
|
+
variables = jinja_template.get_variables()
|
|
1214
|
+
jinja_template.validate(restricted=config.SETTINGS.security.restrict_untrusted_jinja2_filters)
|
|
1215
|
+
except (JinjaTemplateOperationViolationError, JinjaTemplateError) as exc:
|
|
1216
|
+
raise ValueError(
|
|
1217
|
+
f"{node.kind}: display_label is set to a jinja2 template, but has an invalid template: {exc.message}"
|
|
1218
|
+
) from exc
|
|
1219
|
+
|
|
1220
|
+
allowed_path_types = (
|
|
1221
|
+
SchemaElementPathType.ATTR_WITH_PROP
|
|
1222
|
+
| SchemaElementPathType.REL_ONE_MANDATORY_ATTR_WITH_PROP
|
|
1223
|
+
| SchemaElementPathType.REL_ONE_ATTR_WITH_PROP
|
|
1224
|
+
)
|
|
1225
|
+
for variable in variables:
|
|
1226
|
+
schema_path = self.validate_schema_path(
|
|
1227
|
+
node_schema=node, path=variable, allowed_path_types=allowed_path_types, element_name="display_label"
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
if schema_path.is_type_attribute and schema_path.active_attribute_schema.name == "display_label":
|
|
1231
|
+
raise ValueError(f"{node.kind}: display_label the '{variable}' variable is a reference to itself")
|
|
1232
|
+
|
|
1233
|
+
if node.is_node_schema and node.namespace not in ["Internal", "Schema"]:
|
|
1234
|
+
self.display_labels.register_template_schema_path(
|
|
1235
|
+
kind=node.kind, schema_path=schema_path, template=node.display_label
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1140
1238
|
def _validate_computed_attribute(self, node: NodeSchema, attribute: AttributeSchema) -> None:
|
|
1141
1239
|
if not attribute.computed_attribute or attribute.computed_attribute.kind == ComputedAttributeKind.USER:
|
|
1142
1240
|
return
|
|
@@ -1612,6 +1710,42 @@ class SchemaBranch:
|
|
|
1612
1710
|
|
|
1613
1711
|
self.set(name=name, schema=node)
|
|
1614
1712
|
|
|
1713
|
+
def process_relationships_state(self) -> None:
|
|
1714
|
+
for name in self.node_names + self.generic_names_without_templates:
|
|
1715
|
+
node = self.get(name=name, duplicate=False)
|
|
1716
|
+
if node.id or (not node.id and not node.relationships):
|
|
1717
|
+
continue
|
|
1718
|
+
|
|
1719
|
+
filtered_relationships = [
|
|
1720
|
+
relationship for relationship in node.relationships if relationship.state != HashableModelState.ABSENT
|
|
1721
|
+
]
|
|
1722
|
+
if len(filtered_relationships) == len(node.relationships):
|
|
1723
|
+
continue
|
|
1724
|
+
updated_node = node.duplicate()
|
|
1725
|
+
updated_node.relationships = filtered_relationships
|
|
1726
|
+
self.set(name=name, schema=updated_node)
|
|
1727
|
+
|
|
1728
|
+
def process_attributes_state(self) -> None:
|
|
1729
|
+
for name in self.node_names + self.generic_names_without_templates:
|
|
1730
|
+
node = self.get(name=name, duplicate=False)
|
|
1731
|
+
if not node.attributes:
|
|
1732
|
+
continue
|
|
1733
|
+
|
|
1734
|
+
filtered_attributes = [
|
|
1735
|
+
attribute for attribute in node.attributes if attribute.state != HashableModelState.ABSENT
|
|
1736
|
+
]
|
|
1737
|
+
if len(filtered_attributes) == len(node.attributes):
|
|
1738
|
+
continue
|
|
1739
|
+
updated_node = node.duplicate()
|
|
1740
|
+
updated_node.attributes = filtered_attributes
|
|
1741
|
+
self.set(name=name, schema=updated_node)
|
|
1742
|
+
|
|
1743
|
+
def process_nodes_state(self) -> None:
|
|
1744
|
+
for name in self.node_names + self.generic_names_without_templates:
|
|
1745
|
+
node = self.get(name=name, duplicate=False)
|
|
1746
|
+
if not node.id and node.state == HashableModelState.ABSENT:
|
|
1747
|
+
self.delete(name=name)
|
|
1748
|
+
|
|
1615
1749
|
def _generate_weight_generics(self) -> None:
|
|
1616
1750
|
"""Generate order_weight for all generic schemas."""
|
|
1617
1751
|
for name in self.generic_names:
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
filter_key: str = "ids"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def fields(self) -> list[str]:
|
|
21
|
+
return sorted(list(self.attributes) + list(self.relationships))
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def has_related_components(self) -> bool:
|
|
25
|
+
"""Indicate if the associated template use variables from relationships"""
|
|
26
|
+
return len(self.relationships) > 0
|
|
27
|
+
|
|
28
|
+
def get_hash(self) -> str:
|
|
29
|
+
return hashlib.md5(self.template.encode(), usedforsecurity=False).hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RelationshipIdentifier:
|
|
34
|
+
kind: str
|
|
35
|
+
filter_key: str
|
|
36
|
+
template: str
|
|
37
|
+
|
|
38
|
+
def __hash__(self) -> int:
|
|
39
|
+
return hash(f"{self.kind}::{self.filter_key}::{self.template}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class RelationshipTriggers:
|
|
44
|
+
attributes: dict[str, set[RelationshipIdentifier]] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DisplayLabels:
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
template_based_display_labels: dict[str, TemplateLabel] | None = None,
|
|
51
|
+
template_relationship_triggers: dict[str, RelationshipTriggers] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._template_based_display_labels: dict[str, TemplateLabel] = template_based_display_labels or {}
|
|
54
|
+
self._template_relationship_triggers: dict[str, RelationshipTriggers] = template_relationship_triggers or {}
|
|
55
|
+
|
|
56
|
+
def duplicate(self) -> DisplayLabels:
|
|
57
|
+
"""Clone the current object."""
|
|
58
|
+
return self.__class__(
|
|
59
|
+
template_based_display_labels=deepcopy(self._template_based_display_labels),
|
|
60
|
+
template_relationship_triggers=deepcopy(self._template_relationship_triggers),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def register_attribute_based_display_label(self, kind: str, attribute_name: str) -> None:
|
|
64
|
+
"""Register nodes where the display label consists of a single defined attribute name."""
|
|
65
|
+
self._template_based_display_labels[kind] = TemplateLabel(
|
|
66
|
+
template=f"{{{{ {attribute_name}__value }}}}", attributes={attribute_name}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def register_template_schema_path(self, kind: str, schema_path: SchemaAttributePath, template: str) -> None:
|
|
70
|
+
"""Register Jinja2 template based display labels using the schema path of each impacted variable in the node."""
|
|
71
|
+
|
|
72
|
+
if kind not in self._template_based_display_labels:
|
|
73
|
+
self._template_based_display_labels[kind] = TemplateLabel(template=template)
|
|
74
|
+
|
|
75
|
+
if schema_path.is_type_attribute:
|
|
76
|
+
self._template_based_display_labels[kind].attributes.add(schema_path.active_attribute_schema.name)
|
|
77
|
+
elif schema_path.is_type_relationship and schema_path.related_schema:
|
|
78
|
+
self._template_based_display_labels[kind].relationships.add(schema_path.active_relationship_schema.name)
|
|
79
|
+
if schema_path.related_schema.kind not in self._template_relationship_triggers:
|
|
80
|
+
self._template_relationship_triggers[schema_path.related_schema.kind] = RelationshipTriggers()
|
|
81
|
+
if (
|
|
82
|
+
schema_path.active_attribute_schema.name
|
|
83
|
+
not in self._template_relationship_triggers[schema_path.related_schema.kind].attributes
|
|
84
|
+
):
|
|
85
|
+
self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
|
|
86
|
+
schema_path.active_attribute_schema.name
|
|
87
|
+
] = set()
|
|
88
|
+
self._template_relationship_triggers[schema_path.related_schema.kind].attributes[
|
|
89
|
+
schema_path.active_attribute_schema.name
|
|
90
|
+
].add(
|
|
91
|
+
RelationshipIdentifier(
|
|
92
|
+
kind=kind, filter_key=f"{schema_path.active_relationship_schema.name}__ids", template=template
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def targets_node(self, kind: str) -> bool:
|
|
97
|
+
"""Indicates if there is a display_label defined for the targeted node"""
|
|
98
|
+
return kind in self._template_based_display_labels
|
|
99
|
+
|
|
100
|
+
def get_template_node(self, kind: str) -> TemplateLabel:
|
|
101
|
+
"""Return node kinds together with their template definitions."""
|
|
102
|
+
return self._template_based_display_labels[kind]
|
|
103
|
+
|
|
104
|
+
def get_template_nodes(self) -> dict[str, TemplateLabel]:
|
|
105
|
+
"""Return node kinds together with their template definitions."""
|
|
106
|
+
return self._template_based_display_labels
|
|
107
|
+
|
|
108
|
+
def get_related_trigger_nodes(self) -> dict[str, RelationshipTriggers]:
|
|
109
|
+
"""Return node kinds that other nodes use within their templates for display_labels."""
|
|
110
|
+
return self._template_relationship_triggers
|
|
111
|
+
|
|
112
|
+
def get_related_template(self, related_kind: str, target_kind: str) -> TemplateLabel:
|
|
113
|
+
relationship_trigger = self._template_relationship_triggers[related_kind]
|
|
114
|
+
for applicable_kinds in relationship_trigger.attributes.values():
|
|
115
|
+
for relationship_identifier in applicable_kinds:
|
|
116
|
+
if target_kind == relationship_identifier.kind:
|
|
117
|
+
template_label = self.get_template_node(kind=target_kind)
|
|
118
|
+
template_label.filter_key = relationship_identifier.filter_key
|
|
119
|
+
return template_label
|
|
120
|
+
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Unable to find registered template for {target_kind} registered on related node {related_kind}"
|
|
123
|
+
)
|