infrahub-server 1.6.0b0__py3-none-any.whl → 1.6.2__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/api/oauth2.py +33 -6
- infrahub/api/oidc.py +36 -6
- infrahub/auth.py +11 -0
- infrahub/auth_pkce.py +41 -0
- infrahub/config.py +9 -3
- infrahub/core/branch/models.py +3 -2
- infrahub/core/branch/tasks.py +6 -1
- infrahub/core/changelog/models.py +2 -2
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
- infrahub/core/manager.py +36 -31
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +30 -12
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +606 -0
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +161 -0
- infrahub/core/models.py +5 -6
- infrahub/core/node/__init__.py +16 -13
- infrahub/core/node/create.py +36 -8
- infrahub/core/node/proposed_change.py +5 -3
- infrahub/core/node/standard.py +1 -1
- infrahub/core/protocols.py +1 -7
- infrahub/core/query/attribute.py +1 -1
- infrahub/core/query/node.py +9 -5
- infrahub/core/relationship/model.py +21 -4
- infrahub/core/schema/generic_schema.py +1 -1
- infrahub/core/schema/manager.py +8 -3
- infrahub/core/schema/schema_branch.py +35 -16
- infrahub/core/validators/attribute/choices.py +2 -2
- infrahub/core/validators/determiner.py +3 -6
- infrahub/database/__init__.py +1 -1
- infrahub/git/base.py +2 -3
- infrahub/git/models.py +13 -0
- infrahub/git/tasks.py +23 -19
- infrahub/git/utils.py +16 -9
- infrahub/graphql/app.py +6 -6
- infrahub/graphql/loaders/peers.py +6 -0
- infrahub/graphql/mutations/action.py +15 -7
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/profile.py +8 -1
- infrahub/graphql/mutations/repository.py +3 -3
- infrahub/graphql/mutations/schema.py +4 -4
- infrahub/graphql/mutations/webhook.py +2 -2
- infrahub/graphql/queries/resource_manager.py +2 -3
- infrahub/graphql/queries/search.py +2 -3
- infrahub/graphql/resolvers/ipam.py +20 -0
- infrahub/graphql/resolvers/many_relationship.py +12 -11
- infrahub/graphql/resolvers/resolver.py +6 -2
- infrahub/graphql/resolvers/single_relationship.py +1 -11
- infrahub/log.py +1 -1
- infrahub/message_bus/messages/__init__.py +0 -12
- infrahub/profiles/node_applier.py +9 -0
- infrahub/proposed_change/branch_diff.py +1 -1
- infrahub/proposed_change/tasks.py +1 -1
- infrahub/repositories/create_repository.py +3 -3
- infrahub/task_manager/models.py +1 -1
- infrahub/task_manager/task.py +5 -5
- infrahub/trigger/setup.py +6 -9
- infrahub/utils.py +18 -0
- infrahub/validators/tasks.py +1 -1
- infrahub/workers/infrahub_async.py +7 -6
- infrahub_sdk/client.py +113 -1
- infrahub_sdk/ctl/AGENTS.md +67 -0
- infrahub_sdk/ctl/branch.py +175 -1
- infrahub_sdk/ctl/check.py +3 -3
- infrahub_sdk/ctl/cli_commands.py +9 -9
- infrahub_sdk/ctl/generator.py +2 -2
- infrahub_sdk/ctl/graphql.py +1 -2
- infrahub_sdk/ctl/importer.py +1 -2
- infrahub_sdk/ctl/repository.py +6 -49
- infrahub_sdk/ctl/task.py +2 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/ctl/validate.py +1 -2
- infrahub_sdk/diff.py +80 -3
- infrahub_sdk/graphql/constants.py +14 -1
- infrahub_sdk/graphql/renderers.py +5 -1
- infrahub_sdk/node/attribute.py +0 -1
- infrahub_sdk/node/constants.py +3 -1
- infrahub_sdk/node/node.py +303 -3
- infrahub_sdk/node/related_node.py +1 -2
- infrahub_sdk/node/relationship.py +1 -2
- infrahub_sdk/protocols_base.py +0 -1
- infrahub_sdk/pytest_plugin/AGENTS.md +67 -0
- infrahub_sdk/schema/__init__.py +0 -3
- infrahub_sdk/timestamp.py +7 -7
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/METADATA +2 -3
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/RECORD +91 -86
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/WHEEL +1 -1
- infrahub_testcontainers/container.py +2 -2
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from infrahub_sdk.template import Jinja2Template
|
|
6
|
+
from rich.progress import Progress, TaskID
|
|
7
|
+
|
|
8
|
+
from infrahub.core import registry
|
|
9
|
+
from infrahub.core.branch import Branch
|
|
10
|
+
from infrahub.core.constants import GLOBAL_BRANCH_NAME, NULL_VALUE, BranchSupportType
|
|
11
|
+
from infrahub.core.initialization import get_root_node
|
|
12
|
+
from infrahub.core.migrations.shared import MigrationRequiringRebase, MigrationResult, get_migration_console
|
|
13
|
+
from infrahub.core.query import Query, QueryType
|
|
14
|
+
|
|
15
|
+
from .load_schema_branch import get_or_load_schema_branch
|
|
16
|
+
from .m044_backfill_hfid_display_label_in_db import (
|
|
17
|
+
DefaultBranchNodeCount,
|
|
18
|
+
GetPathDetailsBranchQuery,
|
|
19
|
+
GetPathDetailsDefaultBranch,
|
|
20
|
+
GetResultMapQuery,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from infrahub.core.schema import AttributeSchema, MainSchemaTypes
|
|
25
|
+
from infrahub.core.schema.basenode_schema import SchemaAttributePath
|
|
26
|
+
from infrahub.core.schema.schema_branch import SchemaBranch
|
|
27
|
+
from infrahub.database import InfrahubDatabase
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
console = get_migration_console()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_jinja2_template(display_label: str) -> bool:
|
|
34
|
+
return any(c in display_label for c in "{}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_jinja2_variables(template_str: str) -> list[str]:
|
|
38
|
+
return Jinja2Template(template=template_str).get_variables()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _render_display_label(display_label: str, variable_names: list[str], values: list[Any]) -> str | None:
|
|
42
|
+
if not _is_jinja2_template(display_label):
|
|
43
|
+
return values[0] if values and values[0] is not None else None
|
|
44
|
+
|
|
45
|
+
variables = dict(zip(variable_names, values, strict=False))
|
|
46
|
+
jinja_template = Jinja2Template(template=display_label)
|
|
47
|
+
return await jinja_template.render(variables=variables)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UpdateAttributeValuesQuery(Query):
|
|
51
|
+
"""
|
|
52
|
+
Update the values of the given attribute schema for the input node-id-to-value map.
|
|
53
|
+
|
|
54
|
+
This version only expires existing values when they're different from the new value,
|
|
55
|
+
making it safe to run idempotently without clearing correct existing values.
|
|
56
|
+
|
|
57
|
+
This code is adapted from m044_backfill_hfid_display_label_in_db.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name = "update_attribute_values"
|
|
61
|
+
type = QueryType.WRITE
|
|
62
|
+
insert_return = False
|
|
63
|
+
|
|
64
|
+
def __init__(self, attribute_schema: AttributeSchema, values_by_id_map: dict[str, Any], **kwargs: Any) -> None:
|
|
65
|
+
super().__init__(**kwargs)
|
|
66
|
+
self.attribute_name = attribute_schema.name
|
|
67
|
+
self.is_branch_agnostic = attribute_schema.get_branch() is BranchSupportType.AGNOSTIC
|
|
68
|
+
self.values_by_id_map = values_by_id_map
|
|
69
|
+
|
|
70
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
71
|
+
self.params = {
|
|
72
|
+
"node_uuids": list(self.values_by_id_map.keys()),
|
|
73
|
+
"attribute_name": self.attribute_name,
|
|
74
|
+
"values_by_id": self.values_by_id_map,
|
|
75
|
+
"default_branch": registry.default_branch,
|
|
76
|
+
"global_branch": GLOBAL_BRANCH_NAME,
|
|
77
|
+
"branch": GLOBAL_BRANCH_NAME if self.is_branch_agnostic else self.branch.name,
|
|
78
|
+
"branch_level": 1 if self.is_branch_agnostic else self.branch.hierarchy_level,
|
|
79
|
+
"at": self.at.to_string(),
|
|
80
|
+
}
|
|
81
|
+
branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
|
|
82
|
+
self.params.update(branch_filter_params)
|
|
83
|
+
|
|
84
|
+
if self.branch.name in [registry.default_branch, GLOBAL_BRANCH_NAME]:
|
|
85
|
+
update_value_query = """
|
|
86
|
+
// ------------
|
|
87
|
+
// Find the Nodes and Attributes we need to update
|
|
88
|
+
// ------------
|
|
89
|
+
MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
|
|
90
|
+
WHERE n.uuid IN $node_uuids
|
|
91
|
+
AND e.branch IN [$default_branch, $global_branch]
|
|
92
|
+
AND e.to IS NULL
|
|
93
|
+
AND e.status = "active"
|
|
94
|
+
WITH DISTINCT n
|
|
95
|
+
MATCH (n)-[e:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
|
|
96
|
+
WHERE e.branch IN [$default_branch, $global_branch]
|
|
97
|
+
AND e.to IS NULL
|
|
98
|
+
AND e.status = "active"
|
|
99
|
+
// ------------
|
|
100
|
+
// If the attribute has an existing value on the branch, then set the to time on it
|
|
101
|
+
// but only if the value is different from the new value
|
|
102
|
+
// ------------
|
|
103
|
+
WITH DISTINCT n, attr
|
|
104
|
+
CALL (attr) {
|
|
105
|
+
OPTIONAL MATCH (attr)-[e:HAS_VALUE]->(existing_av)
|
|
106
|
+
WHERE e.branch IN [$default_branch, $global_branch]
|
|
107
|
+
AND e.to IS NULL
|
|
108
|
+
AND e.status = "active"
|
|
109
|
+
RETURN existing_av, e AS existing_has_value
|
|
110
|
+
}
|
|
111
|
+
CALL (existing_has_value, existing_av, n) {
|
|
112
|
+
WITH existing_has_value, existing_av, n
|
|
113
|
+
WHERE existing_has_value IS NOT NULL
|
|
114
|
+
AND existing_av.value <> $values_by_id[n.uuid]
|
|
115
|
+
SET existing_has_value.to = $at
|
|
116
|
+
}
|
|
117
|
+
WITH n, attr, existing_av
|
|
118
|
+
"""
|
|
119
|
+
else:
|
|
120
|
+
update_value_query = """
|
|
121
|
+
// ------------
|
|
122
|
+
// Find the Nodes and Attributes we need to update
|
|
123
|
+
// ------------
|
|
124
|
+
MATCH (n:Node)
|
|
125
|
+
WHERE n.uuid IN $node_uuids
|
|
126
|
+
CALL (n) {
|
|
127
|
+
MATCH (n)-[r:IS_PART_OF]->(:Root)
|
|
128
|
+
WHERE %(branch_filter)s
|
|
129
|
+
RETURN r.status = "active" AS is_active
|
|
130
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
131
|
+
LIMIT 1
|
|
132
|
+
}
|
|
133
|
+
WITH n, is_active
|
|
134
|
+
WHERE is_active = TRUE
|
|
135
|
+
WITH DISTINCT n
|
|
136
|
+
CALL (n) {
|
|
137
|
+
MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
|
|
138
|
+
WHERE %(branch_filter)s
|
|
139
|
+
RETURN attr, r.status = "active" AS is_active
|
|
140
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
141
|
+
LIMIT 1
|
|
142
|
+
}
|
|
143
|
+
WITH DISTINCT n, attr, is_active
|
|
144
|
+
WHERE is_active = TRUE
|
|
145
|
+
// ------------
|
|
146
|
+
// If the attribute has an existing value on the branch, then set the to time on it
|
|
147
|
+
// but only if the value is different from the new value
|
|
148
|
+
// ------------
|
|
149
|
+
CALL (n, attr) {
|
|
150
|
+
OPTIONAL MATCH (attr)-[r:HAS_VALUE]->(existing_av)
|
|
151
|
+
WHERE %(branch_filter)s
|
|
152
|
+
WITH r, existing_av, n
|
|
153
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
154
|
+
LIMIT 1
|
|
155
|
+
WITH CASE
|
|
156
|
+
WHEN existing_av.value <> $values_by_id[n.uuid]
|
|
157
|
+
AND r.status = "active"
|
|
158
|
+
AND r.branch = $branch
|
|
159
|
+
THEN [r, existing_av]
|
|
160
|
+
ELSE [NULL, NULL]
|
|
161
|
+
END AS existing_details
|
|
162
|
+
RETURN existing_details[0] AS existing_has_value, existing_details[1] AS existing_av
|
|
163
|
+
}
|
|
164
|
+
CALL (existing_has_value) {
|
|
165
|
+
WITH existing_has_value
|
|
166
|
+
WHERE existing_has_value IS NOT NULL
|
|
167
|
+
SET existing_has_value.to = $at
|
|
168
|
+
}
|
|
169
|
+
WITH n, attr, existing_av
|
|
170
|
+
""" % {"branch_filter": branch_filter}
|
|
171
|
+
self.add_to_query(update_value_query)
|
|
172
|
+
|
|
173
|
+
set_value_query = """
|
|
174
|
+
// ------------
|
|
175
|
+
// only make updates if the existing value is not the same as the new value
|
|
176
|
+
// ------------
|
|
177
|
+
WITH n, attr, existing_av, $values_by_id[n.uuid] AS required_value
|
|
178
|
+
WHERE existing_av.value <> required_value
|
|
179
|
+
OR existing_av IS NULL
|
|
180
|
+
CALL (n, attr) {
|
|
181
|
+
MERGE (av:AttributeValue&AttributeValueIndexed {is_default: false, value: $values_by_id[n.uuid]} )
|
|
182
|
+
WITH av, attr
|
|
183
|
+
LIMIT 1
|
|
184
|
+
CREATE (attr)-[r:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
|
|
185
|
+
}
|
|
186
|
+
"""
|
|
187
|
+
self.add_to_query(set_value_query)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class GetNodesWithoutDisplayLabelQuery(Query):
|
|
191
|
+
"""Get all active nodes that do not have a display_label attribute on the default branch."""
|
|
192
|
+
|
|
193
|
+
name = "get_nodes_without_display_label"
|
|
194
|
+
type = QueryType.READ
|
|
195
|
+
|
|
196
|
+
def __init__(self, kinds_to_skip: list[str] | None = None, **kwargs: Any) -> None:
|
|
197
|
+
super().__init__(**kwargs)
|
|
198
|
+
self.kinds_to_skip = kinds_to_skip or []
|
|
199
|
+
|
|
200
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
201
|
+
self.params = {
|
|
202
|
+
"branch_names": [registry.default_branch, GLOBAL_BRANCH_NAME],
|
|
203
|
+
"kinds_to_skip": self.kinds_to_skip,
|
|
204
|
+
"attribute_name": "display_label",
|
|
205
|
+
}
|
|
206
|
+
query = """
|
|
207
|
+
// ------------
|
|
208
|
+
// Get all active nodes that don't have a display_label attribute
|
|
209
|
+
// ------------
|
|
210
|
+
MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
|
|
211
|
+
WHERE NOT n.kind IN $kinds_to_skip
|
|
212
|
+
AND e.branch IN $branch_names
|
|
213
|
+
AND e.status = "active"
|
|
214
|
+
AND e.to IS NULL
|
|
215
|
+
AND NOT exists((n)-[:IS_PART_OF {branch: e.branch, status: "deleted"}]->(:Root))
|
|
216
|
+
WITH DISTINCT n
|
|
217
|
+
CALL (n) {
|
|
218
|
+
OPTIONAL MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
|
|
219
|
+
WHERE r.branch IN $branch_names
|
|
220
|
+
RETURN r AS has_attr_e
|
|
221
|
+
ORDER BY r.from DESC, r.status ASC
|
|
222
|
+
LIMIT 1
|
|
223
|
+
}
|
|
224
|
+
WITH n, has_attr_e
|
|
225
|
+
WHERE (has_attr_e IS NULL OR has_attr_e.status = "deleted")
|
|
226
|
+
WITH n.uuid AS node_uuid
|
|
227
|
+
"""
|
|
228
|
+
self.add_to_query(query)
|
|
229
|
+
self.return_labels = ["node_uuid"]
|
|
230
|
+
|
|
231
|
+
def get_node_uuids(self) -> list[str]:
|
|
232
|
+
return [result.get_as_type(label="node_uuid", return_type=str) for result in self.get_results()]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class GetNodesWithoutDisplayLabelBranchQuery(Query):
|
|
236
|
+
"""Get all active nodes that do not have a display_label attribute on a non-default branch."""
|
|
237
|
+
|
|
238
|
+
name = "get_nodes_without_display_label_branch"
|
|
239
|
+
type = QueryType.READ
|
|
240
|
+
|
|
241
|
+
def __init__(self, kinds_to_skip: list[str] | None = None, **kwargs: Any) -> None:
|
|
242
|
+
super().__init__(**kwargs)
|
|
243
|
+
self.kinds_to_skip = kinds_to_skip or []
|
|
244
|
+
|
|
245
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
246
|
+
branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
|
|
247
|
+
self.params = {
|
|
248
|
+
"kinds_to_skip": self.kinds_to_skip,
|
|
249
|
+
"attribute_name": "display_label",
|
|
250
|
+
**branch_filter_params,
|
|
251
|
+
}
|
|
252
|
+
query = """
|
|
253
|
+
// ------------
|
|
254
|
+
// Get all active nodes that don't have a display_label attribute
|
|
255
|
+
// ------------
|
|
256
|
+
MATCH (n:Node)
|
|
257
|
+
WHERE NOT n.kind IN $kinds_to_skip
|
|
258
|
+
CALL (n) {
|
|
259
|
+
MATCH (n)-[r:IS_PART_OF]->(:Root)
|
|
260
|
+
WHERE %(branch_filter)s
|
|
261
|
+
RETURN r AS is_part_of_e
|
|
262
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
263
|
+
LIMIT 1
|
|
264
|
+
}
|
|
265
|
+
WITH n, is_part_of_e
|
|
266
|
+
WHERE is_part_of_e.status = "active"
|
|
267
|
+
CALL (n) {
|
|
268
|
+
OPTIONAL MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
|
|
269
|
+
WHERE %(branch_filter)s
|
|
270
|
+
RETURN r AS has_attr_e
|
|
271
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
272
|
+
LIMIT 1
|
|
273
|
+
}
|
|
274
|
+
WITH n, has_attr_e
|
|
275
|
+
WHERE (has_attr_e IS NULL OR has_attr_e.status = "deleted")
|
|
276
|
+
WITH n.uuid AS node_uuid
|
|
277
|
+
""" % {"branch_filter": branch_filter}
|
|
278
|
+
self.add_to_query(query)
|
|
279
|
+
self.return_labels = ["node_uuid"]
|
|
280
|
+
|
|
281
|
+
def get_node_uuids(self) -> list[str]:
|
|
282
|
+
return [result.get_as_type(label="node_uuid", return_type=str) for result in self.get_results()]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class CreateDisplayLabelNullQuery(Query):
|
|
286
|
+
"""Create display_label attribute with NULL value for the given nodes."""
|
|
287
|
+
|
|
288
|
+
name = "create_display_label_null"
|
|
289
|
+
type = QueryType.WRITE
|
|
290
|
+
insert_return = False
|
|
291
|
+
|
|
292
|
+
def __init__(self, node_uuids: list[str], **kwargs: Any) -> None:
|
|
293
|
+
super().__init__(**kwargs)
|
|
294
|
+
self.node_uuids = node_uuids
|
|
295
|
+
|
|
296
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
297
|
+
self.params = {
|
|
298
|
+
"node_uuids": self.node_uuids,
|
|
299
|
+
"attribute_name": "display_label",
|
|
300
|
+
"default_branch": registry.default_branch,
|
|
301
|
+
"global_branch": GLOBAL_BRANCH_NAME,
|
|
302
|
+
"branch": self.branch.name,
|
|
303
|
+
"branch_level": self.branch.hierarchy_level,
|
|
304
|
+
"at": self.at.to_string(),
|
|
305
|
+
"null_value": NULL_VALUE,
|
|
306
|
+
"branch_support": "aware",
|
|
307
|
+
"is_protected_default": False,
|
|
308
|
+
"is_visible_default": True,
|
|
309
|
+
}
|
|
310
|
+
branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
|
|
311
|
+
self.params.update(branch_filter_params)
|
|
312
|
+
|
|
313
|
+
# Create the NULL AttributeValue first
|
|
314
|
+
create_av_query = """
|
|
315
|
+
MERGE (av:AttributeValue&AttributeValueIndexed {is_default: true, value: $null_value})
|
|
316
|
+
WITH av
|
|
317
|
+
LIMIT 1
|
|
318
|
+
MERGE (is_protected_value:Boolean { value: $is_protected_default })
|
|
319
|
+
MERGE (is_visible_value:Boolean { value: $is_visible_default })
|
|
320
|
+
"""
|
|
321
|
+
self.add_to_query(create_av_query)
|
|
322
|
+
|
|
323
|
+
if self.branch.name in [registry.default_branch, GLOBAL_BRANCH_NAME]:
|
|
324
|
+
query = """
|
|
325
|
+
// ------------
|
|
326
|
+
// Create the display_label attribute with NULL value for nodes
|
|
327
|
+
// ------------
|
|
328
|
+
WITH av, is_protected_value, is_visible_value
|
|
329
|
+
MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
|
|
330
|
+
WHERE n.uuid IN $node_uuids
|
|
331
|
+
AND e.branch IN [$default_branch, $global_branch]
|
|
332
|
+
AND e.to IS NULL
|
|
333
|
+
AND e.status = "active"
|
|
334
|
+
CREATE (a:Attribute { uuid: randomUUID(), name: $attribute_name, branch_support: $branch_support })
|
|
335
|
+
CREATE (n)-[:HAS_ATTRIBUTE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(a)
|
|
336
|
+
CREATE (a)-[:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
|
|
337
|
+
CREATE (a)-[:IS_PROTECTED { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_protected_value)
|
|
338
|
+
CREATE (a)-[:IS_VISIBLE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_visible_value)
|
|
339
|
+
"""
|
|
340
|
+
else:
|
|
341
|
+
query = """
|
|
342
|
+
// ------------
|
|
343
|
+
// Create the display_label attribute with NULL value for nodes
|
|
344
|
+
// ------------
|
|
345
|
+
WITH av, is_protected_value, is_visible_value
|
|
346
|
+
MATCH (n:Node)
|
|
347
|
+
WHERE n.uuid IN $node_uuids
|
|
348
|
+
CALL (n) {
|
|
349
|
+
MATCH (n)-[r:IS_PART_OF]->(:Root)
|
|
350
|
+
WHERE %(branch_filter)s
|
|
351
|
+
RETURN r.status = "active" AS is_active
|
|
352
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
353
|
+
LIMIT 1
|
|
354
|
+
}
|
|
355
|
+
WITH n, is_active, av, is_protected_value, is_visible_value
|
|
356
|
+
WHERE is_active = TRUE
|
|
357
|
+
CREATE (a:Attribute { uuid: randomUUID(), name: $attribute_name, branch_support: $branch_support })
|
|
358
|
+
CREATE (n)-[:HAS_ATTRIBUTE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(a)
|
|
359
|
+
CREATE (a)-[:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
|
|
360
|
+
CREATE (a)-[:IS_PROTECTED { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_protected_value)
|
|
361
|
+
CREATE (a)-[:IS_VISIBLE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(is_visible_value)
|
|
362
|
+
""" % {"branch_filter": branch_filter}
|
|
363
|
+
|
|
364
|
+
self.add_to_query(query)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class Migration047(MigrationRequiringRebase):
|
|
368
|
+
"""
|
|
369
|
+
Backfill `display_label` attributes for all nodes:
|
|
370
|
+
- If schema does not define display_label OR attribute doesn't exist: insert NULL value
|
|
371
|
+
- If schema defines display_label: compute and store the value, invalidate NULL value if exists
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
name: str = "047_backfill_or_null_display_label"
|
|
375
|
+
minimum_version: int = 46
|
|
376
|
+
update_batch_size: int = 1000
|
|
377
|
+
# skip these b/c the attributes on these schema-related nodes are used to define the values included in
|
|
378
|
+
# the display_label attributes on instances of these schema, so should not be updated
|
|
379
|
+
kinds_to_skip: list[str] = ["SchemaNode", "SchemaAttribute", "SchemaRelationship", "SchemaGeneric"]
|
|
380
|
+
|
|
381
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
382
|
+
return MigrationResult()
|
|
383
|
+
|
|
384
|
+
def _extract_schema_paths_from_display_label(
|
|
385
|
+
self, schema: MainSchemaTypes, schema_branch: SchemaBranch
|
|
386
|
+
) -> list[SchemaAttributePath]:
|
|
387
|
+
"""Extract schema paths from display_label, handling both simple paths and Jinja2 templates.
|
|
388
|
+
|
|
389
|
+
This follows the same logic as _validate_display_label in schema_branch.py.
|
|
390
|
+
"""
|
|
391
|
+
if not schema.display_label:
|
|
392
|
+
return []
|
|
393
|
+
|
|
394
|
+
if not _is_jinja2_template(schema.display_label):
|
|
395
|
+
schema_path = schema.parse_schema_path(path=schema.display_label, schema=schema_branch)
|
|
396
|
+
return [schema_path]
|
|
397
|
+
|
|
398
|
+
schema_paths = []
|
|
399
|
+
for variable in _extract_jinja2_variables(schema.display_label):
|
|
400
|
+
schema_path = schema.parse_schema_path(path=variable, schema=schema_branch)
|
|
401
|
+
schema_paths.append(schema_path)
|
|
402
|
+
|
|
403
|
+
return schema_paths
|
|
404
|
+
|
|
405
|
+
async def _do_one_schema_all(
|
|
406
|
+
self,
|
|
407
|
+
db: InfrahubDatabase,
|
|
408
|
+
branch: Branch,
|
|
409
|
+
schema: MainSchemaTypes,
|
|
410
|
+
schema_branch: SchemaBranch,
|
|
411
|
+
attribute_schema: AttributeSchema,
|
|
412
|
+
progress: Progress | None = None,
|
|
413
|
+
update_task: TaskID | None = None,
|
|
414
|
+
) -> None:
|
|
415
|
+
if not schema.display_label:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
schema_paths = self._extract_schema_paths_from_display_label(schema=schema, schema_branch=schema_branch)
|
|
419
|
+
if not schema_paths:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
offset = 0
|
|
423
|
+
|
|
424
|
+
# loop until we get no results from the get_details_query
|
|
425
|
+
while True:
|
|
426
|
+
if branch.is_default:
|
|
427
|
+
get_details_query: GetResultMapQuery = await GetPathDetailsDefaultBranch.init(
|
|
428
|
+
db=db,
|
|
429
|
+
schema_kind=schema.kind,
|
|
430
|
+
schema_paths=schema_paths,
|
|
431
|
+
offset=offset,
|
|
432
|
+
limit=self.update_batch_size,
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
get_details_query = await GetPathDetailsBranchQuery.init(
|
|
436
|
+
db=db,
|
|
437
|
+
branch=branch,
|
|
438
|
+
schema_kind=schema.kind,
|
|
439
|
+
schema_paths=schema_paths,
|
|
440
|
+
updates_only=False,
|
|
441
|
+
offset=offset,
|
|
442
|
+
limit=self.update_batch_size,
|
|
443
|
+
)
|
|
444
|
+
await get_details_query.execute(db=db)
|
|
445
|
+
|
|
446
|
+
# Get the values for all schema paths
|
|
447
|
+
schema_path_values_map = get_details_query.get_result_map(schema_paths)
|
|
448
|
+
num_updates = len(schema_path_values_map)
|
|
449
|
+
|
|
450
|
+
formatted_schema_path_values_map: dict[str, str] = {}
|
|
451
|
+
for k, v in schema_path_values_map.items():
|
|
452
|
+
if not v:
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
rendered_value = await _render_display_label(
|
|
456
|
+
display_label=schema.display_label,
|
|
457
|
+
variable_names=[s.attribute_path_as_str for s in schema_paths],
|
|
458
|
+
values=v,
|
|
459
|
+
)
|
|
460
|
+
if rendered_value is not None:
|
|
461
|
+
formatted_schema_path_values_map[k] = rendered_value
|
|
462
|
+
|
|
463
|
+
if formatted_schema_path_values_map:
|
|
464
|
+
update_display_label_query = await UpdateAttributeValuesQuery.init(
|
|
465
|
+
db=db,
|
|
466
|
+
branch=branch,
|
|
467
|
+
attribute_schema=attribute_schema,
|
|
468
|
+
values_by_id_map=formatted_schema_path_values_map,
|
|
469
|
+
)
|
|
470
|
+
await update_display_label_query.execute(db=db)
|
|
471
|
+
|
|
472
|
+
if progress is not None and update_task is not None:
|
|
473
|
+
progress.update(update_task, advance=num_updates)
|
|
474
|
+
|
|
475
|
+
if num_updates == 0:
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
offset += self.update_batch_size
|
|
479
|
+
|
|
480
|
+
async def execute(self, db: InfrahubDatabase) -> MigrationResult:
|
|
481
|
+
root_node = await get_root_node(db=db, initialize=False)
|
|
482
|
+
default_branch_name = root_node.default_branch
|
|
483
|
+
default_branch = await Branch.get_by_name(db=db, name=default_branch_name)
|
|
484
|
+
|
|
485
|
+
main_schema_branch = await get_or_load_schema_branch(db=db, branch=default_branch)
|
|
486
|
+
|
|
487
|
+
base_node_schema = main_schema_branch.get("SchemaNode", duplicate=False)
|
|
488
|
+
display_label_attribute_schema = base_node_schema.get_attribute("display_label")
|
|
489
|
+
|
|
490
|
+
# Get nodes without display_label in the database
|
|
491
|
+
get_nodes_without_dl_query = await GetNodesWithoutDisplayLabelQuery.init(
|
|
492
|
+
db=db, kinds_to_skip=self.kinds_to_skip
|
|
493
|
+
)
|
|
494
|
+
await get_nodes_without_dl_query.execute(db=db)
|
|
495
|
+
nodes_without_display_label = get_nodes_without_dl_query.get_node_uuids()
|
|
496
|
+
|
|
497
|
+
# Count nodes that will get computed values
|
|
498
|
+
kinds_to_backfill: list[str] = []
|
|
499
|
+
for node_schema_name in (
|
|
500
|
+
main_schema_branch.node_names + main_schema_branch.profile_names + main_schema_branch.template_names
|
|
501
|
+
):
|
|
502
|
+
if node_schema_name in self.kinds_to_skip:
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
node_schema = main_schema_branch.get(name=node_schema_name, duplicate=False)
|
|
506
|
+
if node_schema.branch != BranchSupportType.AWARE or not node_schema.display_label:
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
kinds_to_backfill.append(node_schema.kind)
|
|
510
|
+
|
|
511
|
+
backfill_count = 0
|
|
512
|
+
if kinds_to_backfill:
|
|
513
|
+
count_query = await DefaultBranchNodeCount.init(
|
|
514
|
+
db=db, kinds_to_skip=self.kinds_to_skip, kinds_to_include=kinds_to_backfill
|
|
515
|
+
)
|
|
516
|
+
await count_query.execute(db=db)
|
|
517
|
+
backfill_count = count_query.get_num_nodes()
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
with Progress(console=console) as progress:
|
|
521
|
+
# Create NULL display_label
|
|
522
|
+
if nodes_without_display_label:
|
|
523
|
+
null_task = progress.add_task(
|
|
524
|
+
f"Creating NULL display_label for {len(nodes_without_display_label)} nodes",
|
|
525
|
+
total=len(nodes_without_display_label),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
for offset in range(0, len(nodes_without_display_label), self.update_batch_size):
|
|
529
|
+
batch_uuids = nodes_without_display_label[offset : offset + self.update_batch_size]
|
|
530
|
+
if not batch_uuids:
|
|
531
|
+
break
|
|
532
|
+
|
|
533
|
+
create_display_label_query = await CreateDisplayLabelNullQuery.init(
|
|
534
|
+
db=db, branch=default_branch, node_uuids=batch_uuids
|
|
535
|
+
)
|
|
536
|
+
await create_display_label_query.execute(db=db)
|
|
537
|
+
|
|
538
|
+
progress.update(null_task, advance=len(batch_uuids))
|
|
539
|
+
|
|
540
|
+
# Backfill computed display_label values
|
|
541
|
+
if backfill_count > 0:
|
|
542
|
+
backfill_task = progress.add_task(
|
|
543
|
+
f"Backfilling computed display_label for {backfill_count} nodes",
|
|
544
|
+
total=backfill_count,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
for node_schema_name in kinds_to_backfill:
|
|
548
|
+
await self._do_one_schema_all(
|
|
549
|
+
db=db,
|
|
550
|
+
branch=default_branch,
|
|
551
|
+
schema=main_schema_branch.get(name=node_schema_name, duplicate=False),
|
|
552
|
+
schema_branch=main_schema_branch,
|
|
553
|
+
attribute_schema=display_label_attribute_schema,
|
|
554
|
+
progress=progress,
|
|
555
|
+
update_task=backfill_task,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
except Exception as exc:
|
|
559
|
+
return MigrationResult(errors=[str(exc)])
|
|
560
|
+
return MigrationResult()
|
|
561
|
+
|
|
562
|
+
async def execute_against_branch(self, db: InfrahubDatabase, branch: Branch) -> MigrationResult:
|
|
563
|
+
schema_branch = await get_or_load_schema_branch(db=db, branch=branch)
|
|
564
|
+
|
|
565
|
+
base_node_schema = schema_branch.get("SchemaNode", duplicate=False)
|
|
566
|
+
display_label_attribute_schema = base_node_schema.get_attribute("display_label")
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
get_nodes_without_dl_query = await GetNodesWithoutDisplayLabelBranchQuery.init(
|
|
570
|
+
db=db, branch=branch, kinds_to_skip=self.kinds_to_skip
|
|
571
|
+
)
|
|
572
|
+
await get_nodes_without_dl_query.execute(db=db)
|
|
573
|
+
nodes_without_display_label = get_nodes_without_dl_query.get_node_uuids()
|
|
574
|
+
|
|
575
|
+
if nodes_without_display_label:
|
|
576
|
+
for offset in range(0, len(nodes_without_display_label), self.update_batch_size):
|
|
577
|
+
batch_uuids = nodes_without_display_label[offset : offset + self.update_batch_size]
|
|
578
|
+
if not batch_uuids:
|
|
579
|
+
break
|
|
580
|
+
|
|
581
|
+
create_display_label_query = await CreateDisplayLabelNullQuery.init(
|
|
582
|
+
db=db, branch=branch, node_uuids=batch_uuids
|
|
583
|
+
)
|
|
584
|
+
await create_display_label_query.execute(db=db)
|
|
585
|
+
|
|
586
|
+
for node_schema_name in (
|
|
587
|
+
schema_branch.node_names + schema_branch.profile_names + schema_branch.template_names
|
|
588
|
+
):
|
|
589
|
+
if node_schema_name in self.kinds_to_skip:
|
|
590
|
+
continue
|
|
591
|
+
|
|
592
|
+
node_schema = schema_branch.get(name=node_schema_name, duplicate=False)
|
|
593
|
+
if node_schema.branch != BranchSupportType.AWARE or not node_schema.display_label:
|
|
594
|
+
continue
|
|
595
|
+
|
|
596
|
+
await self._do_one_schema_all(
|
|
597
|
+
db=db,
|
|
598
|
+
branch=branch,
|
|
599
|
+
schema=node_schema,
|
|
600
|
+
schema_branch=schema_branch,
|
|
601
|
+
attribute_schema=display_label_attribute_schema,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
except Exception as exc:
|
|
605
|
+
return MigrationResult(errors=[str(exc)])
|
|
606
|
+
return MigrationResult()
|