infrahub-server 1.4.0b1__py3-none-any.whl → 1.4.1__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/schema.py +3 -7
- infrahub/cli/db.py +25 -0
- infrahub/cli/db_commands/__init__.py +0 -0
- infrahub/cli/db_commands/check_inheritance.py +284 -0
- infrahub/cli/upgrade.py +3 -0
- infrahub/config.py +4 -4
- infrahub/core/attribute.py +6 -0
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/diff/model/path.py +0 -39
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +26 -21
- infrahub/core/manager.py +2 -2
- infrahub/core/migrations/__init__.py +2 -0
- infrahub/core/migrations/graph/__init__.py +4 -2
- infrahub/core/migrations/graph/m033_deduplicate_relationship_vertices.py +1 -1
- infrahub/core/migrations/graph/m035_orphan_relationships.py +43 -0
- infrahub/core/migrations/graph/{m035_drop_attr_value_index.py → m036_drop_attr_value_index.py} +3 -3
- infrahub/core/migrations/graph/{m036_index_attr_vals.py → m037_index_attr_vals.py} +3 -3
- infrahub/core/migrations/query/node_duplicate.py +26 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +156 -0
- infrahub/core/models.py +5 -1
- infrahub/core/node/resource_manager/ip_address_pool.py +50 -48
- infrahub/core/node/resource_manager/ip_prefix_pool.py +55 -53
- infrahub/core/node/resource_manager/number_pool.py +20 -18
- infrahub/core/query/branch.py +37 -20
- infrahub/core/query/node.py +15 -0
- infrahub/core/relationship/model.py +13 -13
- infrahub/core/schema/definitions/internal.py +1 -1
- infrahub/core/schema/generated/attribute_schema.py +1 -1
- infrahub/core/schema/node_schema.py +0 -5
- infrahub/core/schema/schema_branch.py +2 -2
- infrahub/core/validators/attribute/choices.py +28 -3
- infrahub/core/validators/attribute/kind.py +5 -1
- infrahub/core/validators/determiner.py +22 -2
- infrahub/events/__init__.py +2 -0
- infrahub/events/proposed_change_action.py +22 -0
- infrahub/graphql/app.py +2 -1
- infrahub/graphql/context.py +1 -1
- infrahub/graphql/mutations/proposed_change.py +5 -0
- infrahub/graphql/mutations/relationship.py +1 -1
- infrahub/graphql/mutations/schema.py +14 -1
- infrahub/graphql/queries/diff/tree.py +53 -2
- infrahub/graphql/schema.py +3 -14
- infrahub/graphql/types/event.py +8 -0
- infrahub/permissions/__init__.py +3 -0
- infrahub/permissions/constants.py +13 -0
- infrahub/permissions/globals.py +32 -0
- infrahub/task_manager/event.py +5 -1
- infrahub_sdk/client.py +8 -8
- infrahub_sdk/node/node.py +2 -2
- infrahub_sdk/protocols.py +6 -40
- infrahub_sdk/pytest_plugin/items/graphql_query.py +1 -1
- infrahub_sdk/schema/repository.py +1 -1
- infrahub_sdk/testing/docker.py +1 -1
- infrahub_sdk/utils.py +11 -7
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/METADATA +4 -4
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/RECORD +60 -56
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/entry_points.txt +0 -0
infrahub/api/schema.py
CHANGED
|
@@ -36,6 +36,7 @@ from infrahub.events import EventMeta
|
|
|
36
36
|
from infrahub.events.schema_action import SchemaUpdatedEvent
|
|
37
37
|
from infrahub.exceptions import MigrationError
|
|
38
38
|
from infrahub.log import get_log_data, get_logger
|
|
39
|
+
from infrahub.permissions import define_global_permission_from_branch
|
|
39
40
|
from infrahub.types import ATTRIBUTE_PYTHON_TYPES
|
|
40
41
|
from infrahub.worker import WORKER_IDENTITY
|
|
41
42
|
from infrahub.workflows.catalogue import SCHEMA_APPLY_MIGRATION, SCHEMA_VALIDATE_MIGRATION
|
|
@@ -287,13 +288,8 @@ async def load_schema(
|
|
|
287
288
|
context: InfrahubContext = Depends(get_context),
|
|
288
289
|
) -> SchemaUpdate:
|
|
289
290
|
permission_manager.raise_for_permission(
|
|
290
|
-
permission=
|
|
291
|
-
|
|
292
|
-
decision=(
|
|
293
|
-
PermissionDecision.ALLOW_DEFAULT
|
|
294
|
-
if branch.name in (GLOBAL_BRANCH_NAME, registry.default_branch)
|
|
295
|
-
else PermissionDecision.ALLOW_OTHER
|
|
296
|
-
).value,
|
|
291
|
+
permission=define_global_permission_from_branch(
|
|
292
|
+
permission=GlobalPermissions.MANAGE_SCHEMA, branch_name=branch.name
|
|
297
293
|
)
|
|
298
294
|
)
|
|
299
295
|
|
infrahub/cli/db.py
CHANGED
|
@@ -53,6 +53,7 @@ from infrahub.database.neo4j import IndexManagerNeo4j
|
|
|
53
53
|
from infrahub.log import get_logger
|
|
54
54
|
|
|
55
55
|
from .constants import ERROR_BADGE, FAILED_BADGE, SUCCESS_BADGE
|
|
56
|
+
from .db_commands.check_inheritance import check_inheritance
|
|
56
57
|
from .patch import patch_app
|
|
57
58
|
|
|
58
59
|
|
|
@@ -175,6 +176,30 @@ async def migrate_cmd(
|
|
|
175
176
|
await dbdriver.close()
|
|
176
177
|
|
|
177
178
|
|
|
179
|
+
@app.command(name="check-inheritance")
|
|
180
|
+
async def check_inheritance_cmd(
|
|
181
|
+
ctx: typer.Context,
|
|
182
|
+
fix: bool = typer.Option(False, help="Fix the inheritance of any invalid nodes."),
|
|
183
|
+
config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Check the database for any vertices with incorrect inheritance"""
|
|
186
|
+
logging.getLogger("infrahub").setLevel(logging.WARNING)
|
|
187
|
+
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
|
188
|
+
logging.getLogger("prefect").setLevel(logging.ERROR)
|
|
189
|
+
|
|
190
|
+
config.load_and_exit(config_file_name=config_file)
|
|
191
|
+
|
|
192
|
+
context: CliContext = ctx.obj
|
|
193
|
+
dbdriver = await context.init_db(retry=1)
|
|
194
|
+
await initialize_registry(db=dbdriver)
|
|
195
|
+
|
|
196
|
+
success = await check_inheritance(db=dbdriver, fix=fix)
|
|
197
|
+
if not success:
|
|
198
|
+
raise typer.Exit(code=1)
|
|
199
|
+
|
|
200
|
+
await dbdriver.close()
|
|
201
|
+
|
|
202
|
+
|
|
178
203
|
@app.command(name="update-core-schema")
|
|
179
204
|
async def update_core_schema_cmd(
|
|
180
205
|
ctx: typer.Context,
|
|
File without changes
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from rich import print as rprint
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from infrahub.core import registry
|
|
12
|
+
from infrahub.core.branch.models import Branch
|
|
13
|
+
from infrahub.core.constants import InfrahubKind
|
|
14
|
+
from infrahub.core.migrations.query.node_duplicate import NodeDuplicateQuery, SchemaNodeInfo
|
|
15
|
+
from infrahub.core.query import Query, QueryType
|
|
16
|
+
from infrahub.core.schema import SchemaRoot, internal_schema
|
|
17
|
+
from infrahub.core.schema.manager import SchemaManager
|
|
18
|
+
from infrahub.log import get_logger
|
|
19
|
+
|
|
20
|
+
from ..constants import FAILED_BADGE, SUCCESS_BADGE
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from infrahub.core.schema.node_schema import NodeSchema
|
|
24
|
+
from infrahub.database import InfrahubDatabase
|
|
25
|
+
|
|
26
|
+
log = get_logger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GetSchemaWithUpdatedInheritance(Query):
|
|
30
|
+
"""
|
|
31
|
+
Get the name, namespace, and branch of any SchemaNodes with _updated_ inheritance
|
|
32
|
+
This query will only return schemas that have had `inherit_from` updated after they were created
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name = "get_schema_with_updated_inheritance"
|
|
36
|
+
type = QueryType.READ
|
|
37
|
+
insert_return = False
|
|
38
|
+
|
|
39
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
40
|
+
query = """
|
|
41
|
+
// find inherit_from attributes that have been updated
|
|
42
|
+
MATCH p = (schema_node:SchemaNode)-[has_attr_e:HAS_ATTRIBUTE {status: "active"}]->(a:Attribute {name: "inherit_from"})
|
|
43
|
+
WHERE has_attr_e.to IS NULL
|
|
44
|
+
CALL (a) {
|
|
45
|
+
// only get branches on which the value was updated, we can ignore the initial create
|
|
46
|
+
MATCH (a)-[e:HAS_VALUE]->(:AttributeValue)
|
|
47
|
+
ORDER BY e.from ASC
|
|
48
|
+
// tail leaves out the earliest one, which is the initial create
|
|
49
|
+
RETURN tail(collect(e.branch)) AS branches
|
|
50
|
+
}
|
|
51
|
+
WITH schema_node, a, branches
|
|
52
|
+
WHERE size(branches) > 0
|
|
53
|
+
UNWIND branches AS branch
|
|
54
|
+
WITH DISTINCT schema_node, a, branch
|
|
55
|
+
|
|
56
|
+
//get branch details
|
|
57
|
+
CALL (branch) {
|
|
58
|
+
MATCH (b:Branch {name: branch})
|
|
59
|
+
RETURN b.branched_from AS branched_from, b.hierarchy_level AS branch_level
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// get the namespace for the schema
|
|
63
|
+
CALL (schema_node, a, branch, branched_from, branch_level) {
|
|
64
|
+
MATCH (schema_node)-[e1:HAS_ATTRIBUTE]-(:Attribute {name: "namespace"})-[e2:HAS_VALUE]->(av)
|
|
65
|
+
WHERE (
|
|
66
|
+
e1.branch = branch OR
|
|
67
|
+
(e1.branch_level < branch_level AND e1.from <= branched_from)
|
|
68
|
+
) AND e1.to IS NULL
|
|
69
|
+
AND e1.status = "active"
|
|
70
|
+
AND (
|
|
71
|
+
e2.branch = branch OR
|
|
72
|
+
(e2.branch_level < branch_level AND e2.from <= branched_from)
|
|
73
|
+
) AND e2.to IS NULL
|
|
74
|
+
AND e2.status = "active"
|
|
75
|
+
ORDER BY e2.branch_level DESC, e1.branch_level DESC, e2.from DESC, e1.from DESC
|
|
76
|
+
RETURN av.value AS namespace
|
|
77
|
+
LIMIT 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// get the name for the schema
|
|
81
|
+
CALL (schema_node, a, branch, branched_from, branch_level) {
|
|
82
|
+
MATCH (schema_node)-[e1:HAS_ATTRIBUTE]-(:Attribute {name: "name"})-[e2:HAS_VALUE]->(av)
|
|
83
|
+
WHERE (
|
|
84
|
+
e1.branch = branch OR
|
|
85
|
+
(e1.branch_level < branch_level AND e1.from <= branched_from)
|
|
86
|
+
) AND e1.to IS NULL
|
|
87
|
+
AND e1.status = "active"
|
|
88
|
+
AND (
|
|
89
|
+
e2.branch = branch OR
|
|
90
|
+
(e2.branch_level < branch_level AND e2.from <= branched_from)
|
|
91
|
+
) AND e2.to IS NULL
|
|
92
|
+
AND e2.status = "active"
|
|
93
|
+
ORDER BY e2.branch_level DESC, e1.branch_level DESC, e2.from DESC, e1.from DESC
|
|
94
|
+
RETURN av.value AS name
|
|
95
|
+
LIMIT 1
|
|
96
|
+
}
|
|
97
|
+
RETURN name, namespace, branch
|
|
98
|
+
"""
|
|
99
|
+
self.return_labels = ["name", "namespace", "branch"]
|
|
100
|
+
self.add_to_query(query)
|
|
101
|
+
|
|
102
|
+
def get_updated_inheritance_kinds_by_branch(self) -> dict[str, list[str]]:
|
|
103
|
+
kinds_by_branch: dict[str, list[str]] = defaultdict(list)
|
|
104
|
+
for result in self.results:
|
|
105
|
+
name = result.get_as_type(label="name", return_type=str)
|
|
106
|
+
namespace = result.get_as_type(label="namespace", return_type=str)
|
|
107
|
+
branch = result.get_as_type(label="branch", return_type=str)
|
|
108
|
+
kinds_by_branch[branch].append(f"{namespace}{name}")
|
|
109
|
+
return kinds_by_branch
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class KindLabelCount:
|
|
114
|
+
kind: str
|
|
115
|
+
labels: frozenset[str]
|
|
116
|
+
num_nodes: int
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class KindLabelCountCorrected(KindLabelCount):
|
|
121
|
+
node_schema: NodeSchema
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class GetAllKindsAndLabels(Query):
|
|
125
|
+
"""
|
|
126
|
+
Get the kind, labels, and number of nodes for the given kinds and branch
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
name = "get_all_kinds_and_labels"
|
|
130
|
+
type = QueryType.READ
|
|
131
|
+
insert_return = False
|
|
132
|
+
|
|
133
|
+
def __init__(self, kinds: list[str] | None = None, **kwargs: Any) -> None:
|
|
134
|
+
super().__init__(**kwargs)
|
|
135
|
+
self.kinds = kinds
|
|
136
|
+
|
|
137
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
138
|
+
self.params["branch_name"] = self.branch.name
|
|
139
|
+
self.params["branched_from"] = self.branch.get_branched_from()
|
|
140
|
+
self.params["branch_level"] = self.branch.hierarchy_level
|
|
141
|
+
kinds_str = "Node"
|
|
142
|
+
if self.kinds:
|
|
143
|
+
kinds_str = "|".join(self.kinds)
|
|
144
|
+
query = """
|
|
145
|
+
MATCH (n:%(kinds_str)s)-[r:IS_PART_OF]->(:Root)
|
|
146
|
+
WHERE (
|
|
147
|
+
r.branch = $branch_name OR
|
|
148
|
+
(r.branch_level < $branch_level AND r.from <= $branched_from)
|
|
149
|
+
)
|
|
150
|
+
AND r.to IS NULL
|
|
151
|
+
AND r.status = "active"
|
|
152
|
+
RETURN DISTINCT n.kind AS kind, labels(n) AS labels, count(*) AS num_nodes
|
|
153
|
+
ORDER BY kind ASC
|
|
154
|
+
""" % {"kinds_str": kinds_str}
|
|
155
|
+
self.return_labels = ["kind", "labels", "num_nodes"]
|
|
156
|
+
self.add_to_query(query)
|
|
157
|
+
|
|
158
|
+
def get_kind_label_counts(self) -> list[KindLabelCount]:
|
|
159
|
+
kind_label_counts: list[KindLabelCount] = []
|
|
160
|
+
for result in self.results:
|
|
161
|
+
kind = result.get_as_type(label="kind", return_type=str)
|
|
162
|
+
num_nodes = result.get_as_type(label="num_nodes", return_type=int)
|
|
163
|
+
labels: list[str] = result.get_as_type(label="labels", return_type=list)
|
|
164
|
+
# we can ignore the Node label and the label that matches the kind
|
|
165
|
+
cleaned_labels = frozenset(str(lbl) for lbl in labels if lbl not in ["Node", "CoreNode", kind])
|
|
166
|
+
kind_label_counts.append(KindLabelCount(kind=kind, labels=cleaned_labels, num_nodes=num_nodes))
|
|
167
|
+
return kind_label_counts
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def display_kind_label_counts(kind_label_counts_by_branch: dict[str, list[KindLabelCountCorrected]]) -> None:
|
|
171
|
+
console = Console()
|
|
172
|
+
|
|
173
|
+
table = Table(title="Incorrect Inheritance Nodes")
|
|
174
|
+
|
|
175
|
+
table.add_column("Branch")
|
|
176
|
+
table.add_column("Kind")
|
|
177
|
+
table.add_column("Incorrect Labels")
|
|
178
|
+
table.add_column("Num Nodes")
|
|
179
|
+
|
|
180
|
+
for branch_name, kind_label_counts in kind_label_counts_by_branch.items():
|
|
181
|
+
for kind_label_count in kind_label_counts:
|
|
182
|
+
table.add_row(
|
|
183
|
+
branch_name, kind_label_count.kind, str(list(kind_label_count.labels)), str(kind_label_count.num_nodes)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
console.print(table)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def check_inheritance(db: InfrahubDatabase, fix: bool = False) -> bool:
|
|
190
|
+
"""
|
|
191
|
+
Run migrations to update the inheritance of any nodes with incorrect inheritance from a failed migration
|
|
192
|
+
1. Identifies node schemas that have had their inheritance updated after they were created
|
|
193
|
+
a. includes the kind and branch of the inheritance update
|
|
194
|
+
2. Checks nodes of the given kinds on the given branch to verify their inheritance is correct
|
|
195
|
+
3. Displays counts of any kinds with incorrect inheritance on the given branch
|
|
196
|
+
4. If fix is True, runs migrations to update the inheritance of any nodes with incorrect inheritance
|
|
197
|
+
on the correct branch
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
updated_inheritance_query = await GetSchemaWithUpdatedInheritance.init(db=db)
|
|
201
|
+
await updated_inheritance_query.execute(db=db)
|
|
202
|
+
updated_inheritance_kinds_by_branch = updated_inheritance_query.get_updated_inheritance_kinds_by_branch()
|
|
203
|
+
|
|
204
|
+
if not updated_inheritance_kinds_by_branch:
|
|
205
|
+
rprint(f"{SUCCESS_BADGE} No schemas have had their inheritance updated")
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
schema_manager = SchemaManager()
|
|
209
|
+
registry.schema = schema_manager
|
|
210
|
+
schema = SchemaRoot(**internal_schema)
|
|
211
|
+
schema_manager.register_schema(schema=schema)
|
|
212
|
+
branches_by_name = {b.name: b for b in await Branch.get_list(db=db)}
|
|
213
|
+
|
|
214
|
+
kind_label_counts_by_branch: dict[str, list[KindLabelCountCorrected]] = defaultdict(list)
|
|
215
|
+
for branch_name, kinds in updated_inheritance_kinds_by_branch.items():
|
|
216
|
+
rprint(f"Checking branch: {branch_name}", end="...")
|
|
217
|
+
branch = branches_by_name[branch_name]
|
|
218
|
+
schema_branch = await schema_manager.load_schema_from_db(db=db, branch=branch)
|
|
219
|
+
kind_label_query = await GetAllKindsAndLabels.init(db=db, branch=branch, kinds=kinds)
|
|
220
|
+
await kind_label_query.execute(db=db)
|
|
221
|
+
kind_label_counts = kind_label_query.get_kind_label_counts()
|
|
222
|
+
|
|
223
|
+
for kind_label_count in kind_label_counts:
|
|
224
|
+
node_schema = schema_branch.get_node(name=kind_label_count.kind, duplicate=False)
|
|
225
|
+
correct_labels = frozenset(node_schema.inherit_from)
|
|
226
|
+
if kind_label_count.labels == correct_labels:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
kind_label_counts_by_branch[branch_name].append(
|
|
230
|
+
KindLabelCountCorrected(
|
|
231
|
+
kind=kind_label_count.kind,
|
|
232
|
+
labels=kind_label_count.labels,
|
|
233
|
+
num_nodes=kind_label_count.num_nodes,
|
|
234
|
+
node_schema=node_schema,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
rprint("done")
|
|
238
|
+
|
|
239
|
+
if not kind_label_counts_by_branch:
|
|
240
|
+
rprint(f"{SUCCESS_BADGE} All nodes have the correct inheritance")
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
display_kind_label_counts(kind_label_counts_by_branch)
|
|
244
|
+
|
|
245
|
+
if not fix:
|
|
246
|
+
rprint(f"{FAILED_BADGE} Use the --fix flag to fix the inheritance of any invalid nodes")
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
for branch_name, kind_label_counts_corrected in kind_label_counts_by_branch.items():
|
|
250
|
+
for kind_label_count in kind_label_counts_corrected:
|
|
251
|
+
rprint(f"Fixing kind {kind_label_count.kind} on branch {branch_name}", end="...")
|
|
252
|
+
node_schema = kind_label_count.node_schema
|
|
253
|
+
migration_query = await NodeDuplicateQuery.init(
|
|
254
|
+
db=db,
|
|
255
|
+
branch=branches_by_name[branch_name],
|
|
256
|
+
previous_node=SchemaNodeInfo(
|
|
257
|
+
name=node_schema.name,
|
|
258
|
+
namespace=node_schema.namespace,
|
|
259
|
+
branch_support=node_schema.branch.value,
|
|
260
|
+
labels=list(kind_label_count.labels) + [kind_label_count.kind, InfrahubKind.NODE],
|
|
261
|
+
kind=kind_label_count.kind,
|
|
262
|
+
),
|
|
263
|
+
new_node=SchemaNodeInfo(
|
|
264
|
+
name=node_schema.name,
|
|
265
|
+
namespace=node_schema.namespace,
|
|
266
|
+
branch_support=node_schema.branch.value,
|
|
267
|
+
labels=list(node_schema.inherit_from) + [kind_label_count.kind, InfrahubKind.NODE],
|
|
268
|
+
kind=kind_label_count.kind,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
await migration_query.execute(db=db)
|
|
272
|
+
rprint("done")
|
|
273
|
+
|
|
274
|
+
rprint(f"{SUCCESS_BADGE} All nodes have the correct inheritance")
|
|
275
|
+
|
|
276
|
+
if registry.default_branch in kind_label_counts_by_branch:
|
|
277
|
+
kinds = [kind_label_count.kind for kind_label_count in kind_label_counts_by_branch[registry.default_branch]]
|
|
278
|
+
rprint(
|
|
279
|
+
"[bold cyan]Note that migrations were run on the default branch for the following schema kinds: "
|
|
280
|
+
f"{', '.join(kinds)}. You should rebase any branches that include/will include changes using "
|
|
281
|
+
"the migrated schemas[/bold cyan]"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return True
|
infrahub/cli/upgrade.py
CHANGED
|
@@ -14,6 +14,7 @@ from infrahub import config
|
|
|
14
14
|
from infrahub.core.initialization import create_anonymous_role, create_default_account_groups, initialize_registry
|
|
15
15
|
from infrahub.core.manager import NodeManager
|
|
16
16
|
from infrahub.core.protocols import CoreAccount, CoreObjectPermission
|
|
17
|
+
from infrahub.dependencies.registry import build_component_registry
|
|
17
18
|
from infrahub.menu.menu import default_menu
|
|
18
19
|
from infrahub.menu.models import MenuDict
|
|
19
20
|
from infrahub.menu.repository import MenuRepository
|
|
@@ -54,6 +55,8 @@ async def upgrade_cmd(
|
|
|
54
55
|
|
|
55
56
|
await initialize_registry(db=dbdriver)
|
|
56
57
|
|
|
58
|
+
build_component_registry()
|
|
59
|
+
|
|
57
60
|
# NOTE add step to validate if the database and the task manager are reachable
|
|
58
61
|
|
|
59
62
|
# -------------------------------------------
|
infrahub/config.py
CHANGED
|
@@ -328,6 +328,8 @@ class DevelopmentSettings(BaseSettings):
|
|
|
328
328
|
|
|
329
329
|
|
|
330
330
|
class BrokerSettings(BaseSettings):
|
|
331
|
+
"""Configuration settings for the message bus."""
|
|
332
|
+
|
|
331
333
|
model_config = SettingsConfigDict(env_prefix="INFRAHUB_BROKER_")
|
|
332
334
|
enable: bool = True
|
|
333
335
|
tls_enabled: bool = Field(default=False, description="Indicates if TLS is enabled for the connection")
|
|
@@ -441,9 +443,7 @@ class GitSettings(BaseSettings):
|
|
|
441
443
|
|
|
442
444
|
|
|
443
445
|
class HTTPSettings(BaseSettings):
|
|
444
|
-
"""The HTTP settings control how Infrahub interacts with external HTTP servers
|
|
445
|
-
|
|
446
|
-
This can be things like webhooks and OAuth2 providers"""
|
|
446
|
+
"""The HTTP settings control how Infrahub interacts with external HTTP servers. This can be things like webhooks and OAuth2 providers."""
|
|
447
447
|
|
|
448
448
|
model_config = SettingsConfigDict(env_prefix="INFRAHUB_HTTP_")
|
|
449
449
|
timeout: int = Field(default=10, description="Default connection timeout in seconds")
|
|
@@ -667,7 +667,7 @@ class SecuritySettings(BaseSettings):
|
|
|
667
667
|
oidc_providers: list[OIDCProvider] = Field(default_factory=list, description="The selected OIDC providers")
|
|
668
668
|
oidc_provider_settings: SecurityOIDCProviderSettings = Field(default_factory=SecurityOIDCProviderSettings)
|
|
669
669
|
restrict_untrusted_jinja2_filters: bool = Field(
|
|
670
|
-
default=True, description="Indicates if untrusted Jinja2 filters should be
|
|
670
|
+
default=True, description="Indicates if untrusted Jinja2 filters should be disallowed for computed attributes"
|
|
671
671
|
)
|
|
672
672
|
_oauth2_settings: dict[str, SecurityOAuth2Settings] = PrivateAttr(default_factory=dict)
|
|
673
673
|
_oidc_settings: dict[str, SecurityOIDCSettings] = PrivateAttr(default_factory=dict)
|
infrahub/core/attribute.py
CHANGED
|
@@ -680,6 +680,12 @@ class String(BaseAttribute):
|
|
|
680
680
|
type = str
|
|
681
681
|
value: str
|
|
682
682
|
|
|
683
|
+
@classmethod
|
|
684
|
+
def validate_content(cls, value: Any, name: str, schema: AttributeSchema) -> None:
|
|
685
|
+
if value is not None and not is_large_attribute_type(schema.kind):
|
|
686
|
+
validate_string_length(value=str(value))
|
|
687
|
+
super().validate_content(value=value, name=name, schema=schema)
|
|
688
|
+
|
|
683
689
|
|
|
684
690
|
class StringOptional(String):
|
|
685
691
|
value: str | None
|
|
@@ -66,6 +66,7 @@ class EventType(InfrahubStringEnum):
|
|
|
66
66
|
PROPOSED_CHANGE_APPROVED = f"{EVENT_NAMESPACE}.proposed_change.approved"
|
|
67
67
|
PROPOSED_CHANGE_REJECTED = f"{EVENT_NAMESPACE}.proposed_change.rejected"
|
|
68
68
|
PROPOSED_CHANGE_APPROVAL_REVOKED = f"{EVENT_NAMESPACE}.proposed_change.approval_revoked"
|
|
69
|
+
PROPOSED_CHANGE_APPROVALS_REVOKED = f"{EVENT_NAMESPACE}.proposed_change.approvals_revoked"
|
|
69
70
|
PROPOSED_CHANGE_REJECTION_REVOKED = f"{EVENT_NAMESPACE}.proposed_change.rejection_revoked"
|
|
70
71
|
PROPOSED_CHANGE_THREAD_CREATED = f"{EVENT_NAMESPACE}.proposed_change_thread.created"
|
|
71
72
|
PROPOSED_CHANGE_THREAD_UPDATED = f"{EVENT_NAMESPACE}.proposed_change_thread.updated"
|
infrahub/core/diff/model/path.py
CHANGED
|
@@ -22,8 +22,6 @@ if TYPE_CHECKING:
|
|
|
22
22
|
from neo4j.graph import Relationship as Neo4jRelationship
|
|
23
23
|
from whenever import TimeDelta
|
|
24
24
|
|
|
25
|
-
from infrahub.graphql.initialization import GraphqlContext
|
|
26
|
-
|
|
27
25
|
|
|
28
26
|
@dataclass
|
|
29
27
|
class TimeRange:
|
|
@@ -314,12 +312,6 @@ class EnrichedDiffRelationship(BaseSummary):
|
|
|
314
312
|
)
|
|
315
313
|
|
|
316
314
|
|
|
317
|
-
@dataclass
|
|
318
|
-
class ParentNodeInfo:
|
|
319
|
-
node: EnrichedDiffNode
|
|
320
|
-
relationship_name: str = "undefined"
|
|
321
|
-
|
|
322
|
-
|
|
323
315
|
@dataclass
|
|
324
316
|
class EnrichedDiffNode(BaseSummary):
|
|
325
317
|
identifier: NodeIdentifier
|
|
@@ -364,37 +356,6 @@ class EnrichedDiffNode(BaseSummary):
|
|
|
364
356
|
rel.clear_conflicts()
|
|
365
357
|
self.conflict = None
|
|
366
358
|
|
|
367
|
-
def get_parent_info(self, graphql_context: GraphqlContext | None = None) -> ParentNodeInfo | None:
|
|
368
|
-
for r in self.relationships:
|
|
369
|
-
for n in r.nodes:
|
|
370
|
-
relationship_name: str = "undefined"
|
|
371
|
-
|
|
372
|
-
if not graphql_context:
|
|
373
|
-
return ParentNodeInfo(node=n, relationship_name=relationship_name)
|
|
374
|
-
|
|
375
|
-
node_schema = graphql_context.db.schema.get(name=self.kind)
|
|
376
|
-
rel_schema = node_schema.get_relationship(name=r.name)
|
|
377
|
-
|
|
378
|
-
parent_schema = graphql_context.db.schema.get(name=n.kind)
|
|
379
|
-
rels_parent = parent_schema.get_relationships_by_identifier(id=rel_schema.get_identifier())
|
|
380
|
-
|
|
381
|
-
if rels_parent and len(rels_parent) == 1:
|
|
382
|
-
relationship_name = rels_parent[0].name
|
|
383
|
-
elif rels_parent and len(rels_parent) > 1:
|
|
384
|
-
for rel_parent in rels_parent:
|
|
385
|
-
if (
|
|
386
|
-
rel_schema.direction == RelationshipDirection.INBOUND
|
|
387
|
-
and rel_parent.direction == RelationshipDirection.OUTBOUND
|
|
388
|
-
) or (
|
|
389
|
-
rel_schema.direction == RelationshipDirection.OUTBOUND
|
|
390
|
-
and rel_parent.direction == RelationshipDirection.INBOUND
|
|
391
|
-
):
|
|
392
|
-
relationship_name = rel_parent.name
|
|
393
|
-
break
|
|
394
|
-
|
|
395
|
-
return ParentNodeInfo(node=n, relationship_name=relationship_name)
|
|
396
|
-
return None
|
|
397
|
-
|
|
398
359
|
def get_all_child_nodes(self) -> set[EnrichedDiffNode]:
|
|
399
360
|
all_children = set()
|
|
400
361
|
for r in self.relationships:
|
infrahub/core/graph/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
GRAPH_VERSION =
|
|
1
|
+
GRAPH_VERSION = 37
|
infrahub/core/initialization.py
CHANGED
|
@@ -37,7 +37,7 @@ from infrahub.exceptions import DatabaseError
|
|
|
37
37
|
from infrahub.graphql.manager import GraphQLSchemaManager
|
|
38
38
|
from infrahub.log import get_logger
|
|
39
39
|
from infrahub.menu.utils import create_default_menu
|
|
40
|
-
from infrahub.permissions import PermissionBackend
|
|
40
|
+
from infrahub.permissions import PermissionBackend, get_or_create_global_permission
|
|
41
41
|
from infrahub.storage import InfrahubObjectStorage
|
|
42
42
|
|
|
43
43
|
if TYPE_CHECKING:
|
|
@@ -371,20 +371,13 @@ async def create_default_role(db: InfrahubDatabase) -> CoreAccountRole:
|
|
|
371
371
|
await proposed_change_permission.save(db=db)
|
|
372
372
|
|
|
373
373
|
# Other permissions, created to keep references of them from the start
|
|
374
|
-
for permission_action
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
374
|
+
for permission_action in (
|
|
375
|
+
GlobalPermissions.EDIT_DEFAULT_BRANCH,
|
|
376
|
+
GlobalPermissions.MANAGE_ACCOUNTS,
|
|
377
|
+
GlobalPermissions.MANAGE_PERMISSIONS,
|
|
378
|
+
GlobalPermissions.MERGE_BRANCH,
|
|
379
379
|
):
|
|
380
|
-
|
|
381
|
-
await permission.new(
|
|
382
|
-
db=db,
|
|
383
|
-
action=permission_action.value,
|
|
384
|
-
decision=PermissionDecision.ALLOW_ALL.value,
|
|
385
|
-
description=permission_description,
|
|
386
|
-
)
|
|
387
|
-
await permission.save(db=db)
|
|
380
|
+
await get_or_create_global_permission(db=db, permission=permission_action)
|
|
388
381
|
|
|
389
382
|
view_permission = await Node.init(db=db, schema=InfrahubKind.OBJECTPERMISSION)
|
|
390
383
|
await view_permission.new(
|
|
@@ -428,18 +421,31 @@ async def create_default_role(db: InfrahubDatabase) -> CoreAccountRole:
|
|
|
428
421
|
|
|
429
422
|
|
|
430
423
|
async def create_proposed_change_reviewer_role(db: InfrahubDatabase) -> CoreAccountRole:
|
|
431
|
-
|
|
432
|
-
|
|
424
|
+
edit_default_branch_permission = await get_or_create_global_permission(
|
|
425
|
+
db=db, permission=GlobalPermissions.EDIT_DEFAULT_BRANCH
|
|
426
|
+
)
|
|
427
|
+
reviewer_permission = await get_or_create_global_permission(
|
|
428
|
+
db=db, permission=GlobalPermissions.REVIEW_PROPOSED_CHANGE
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
proposed_change_update_permission = await Node.init(db=db, schema=InfrahubKind.OBJECTPERMISSION)
|
|
432
|
+
await proposed_change_update_permission.new(
|
|
433
433
|
db=db,
|
|
434
|
-
|
|
434
|
+
name="ProposedChange",
|
|
435
|
+
namespace="Core",
|
|
436
|
+
action=PermissionAction.UPDATE.value,
|
|
435
437
|
decision=PermissionDecision.ALLOW_ALL.value,
|
|
436
|
-
description="Allow a user to
|
|
438
|
+
description="Allow a user to update proposed changes",
|
|
437
439
|
)
|
|
438
|
-
await
|
|
440
|
+
await proposed_change_update_permission.save(db=db)
|
|
439
441
|
|
|
440
442
|
role_name = "Proposed Change Reviewer"
|
|
441
443
|
role = await Node.init(db=db, schema=CoreAccountRole)
|
|
442
|
-
await role.new(
|
|
444
|
+
await role.new(
|
|
445
|
+
db=db,
|
|
446
|
+
name=role_name,
|
|
447
|
+
permissions=[edit_default_branch_permission, reviewer_permission, proposed_change_update_permission],
|
|
448
|
+
)
|
|
443
449
|
await role.save(db=db)
|
|
444
450
|
log.info(f"Created account role: {role_name}")
|
|
445
451
|
|
|
@@ -497,7 +503,6 @@ async def create_default_account_groups(
|
|
|
497
503
|
|
|
498
504
|
default_role = await create_default_role(db=db)
|
|
499
505
|
proposed_change_reviewer_role = await create_proposed_change_reviewer_role(db=db)
|
|
500
|
-
|
|
501
506
|
await create_accounts_group(
|
|
502
507
|
db=db, name="Infrahub Users", roles=[default_role, proposed_change_reviewer_role], accounts=accounts or []
|
|
503
508
|
)
|
infrahub/core/manager.py
CHANGED
|
@@ -400,7 +400,7 @@ class NodeManager:
|
|
|
400
400
|
|
|
401
401
|
results = []
|
|
402
402
|
for peer in peers_info:
|
|
403
|
-
result =
|
|
403
|
+
result = Relationship(schema=schema, branch=branch, at=at, node_id=peer.source_id).load(
|
|
404
404
|
db=db,
|
|
405
405
|
id=peer.rel_node_id,
|
|
406
406
|
db_id=peer.rel_node_db_id,
|
|
@@ -408,7 +408,7 @@ class NodeManager:
|
|
|
408
408
|
data=peer,
|
|
409
409
|
)
|
|
410
410
|
if fetch_peers:
|
|
411
|
-
|
|
411
|
+
result.set_peer(value=peer_nodes[peer.peer_id])
|
|
412
412
|
results.append(result)
|
|
413
413
|
|
|
414
414
|
return results
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .schema.attribute_kind_update import AttributeKindUpdateMigration
|
|
1
2
|
from .schema.attribute_name_update import AttributeNameUpdateMigration
|
|
2
3
|
from .schema.node_attribute_add import NodeAttributeAddMigration
|
|
3
4
|
from .schema.node_attribute_remove import NodeAttributeRemoveMigration
|
|
@@ -17,6 +18,7 @@ MIGRATION_MAP: dict[str, type[SchemaMigration] | None] = {
|
|
|
17
18
|
"node.relationship.remove": PlaceholderDummyMigration,
|
|
18
19
|
"attribute.name.update": AttributeNameUpdateMigration,
|
|
19
20
|
"attribute.branch.update": None,
|
|
21
|
+
"attribute.kind.update": AttributeKindUpdateMigration,
|
|
20
22
|
"relationship.branch.update": None,
|
|
21
23
|
"relationship.direction.update": None,
|
|
22
24
|
"relationship.identifier.update": PlaceholderDummyMigration,
|
|
@@ -36,8 +36,9 @@ from .m031_check_number_attributes import Migration031
|
|
|
36
36
|
from .m032_cleanup_orphaned_branch_relationships import Migration032
|
|
37
37
|
from .m033_deduplicate_relationship_vertices import Migration033
|
|
38
38
|
from .m034_find_orphaned_schema_fields import Migration034
|
|
39
|
-
from .
|
|
40
|
-
from .
|
|
39
|
+
from .m035_orphan_relationships import Migration035
|
|
40
|
+
from .m036_drop_attr_value_index import Migration036
|
|
41
|
+
from .m037_index_attr_vals import Migration037
|
|
41
42
|
|
|
42
43
|
if TYPE_CHECKING:
|
|
43
44
|
from infrahub.core.root import Root
|
|
@@ -81,6 +82,7 @@ MIGRATIONS: list[type[GraphMigration | InternalSchemaMigration | ArbitraryMigrat
|
|
|
81
82
|
Migration034,
|
|
82
83
|
Migration035,
|
|
83
84
|
Migration036,
|
|
85
|
+
Migration037,
|
|
84
86
|
]
|
|
85
87
|
|
|
86
88
|
|
|
@@ -89,7 +89,7 @@ class Migration033(GraphMigration):
|
|
|
89
89
|
"""
|
|
90
90
|
|
|
91
91
|
name: str = "033_deduplicate_relationship_vertices"
|
|
92
|
-
minimum_version: int =
|
|
92
|
+
minimum_version: int = 32
|
|
93
93
|
queries: Sequence[type[Query]] = [DeduplicateRelationshipVerticesQuery]
|
|
94
94
|
|
|
95
95
|
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Sequence
|
|
4
|
+
|
|
5
|
+
from infrahub.core.migrations.shared import GraphMigration, MigrationResult
|
|
6
|
+
|
|
7
|
+
from ...query import Query, QueryType
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from infrahub.database import InfrahubDatabase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CleanupOrphanedRelationshipsQuery(Query):
|
|
14
|
+
name = "cleanup_orphaned_relationships"
|
|
15
|
+
type = QueryType.WRITE
|
|
16
|
+
insert_return = False
|
|
17
|
+
|
|
18
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
19
|
+
query = """
|
|
20
|
+
MATCH (rel:Relationship)-[:IS_RELATED]-(peer:Node)
|
|
21
|
+
WITH DISTINCT rel, peer.uuid AS p_uuid
|
|
22
|
+
WITH rel, count(*) AS num_peers
|
|
23
|
+
WHERE num_peers < 2
|
|
24
|
+
DETACH DELETE rel
|
|
25
|
+
"""
|
|
26
|
+
self.add_to_query(query)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Migration035(GraphMigration):
|
|
30
|
+
"""
|
|
31
|
+
Remove Relationship vertices that only have a single peer
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name: str = "035_clean_up_orphaned_relationships"
|
|
35
|
+
minimum_version: int = 34
|
|
36
|
+
queries: Sequence[type[Query]] = [CleanupOrphanedRelationshipsQuery]
|
|
37
|
+
|
|
38
|
+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
|
|
39
|
+
return MigrationResult()
|
|
40
|
+
|
|
41
|
+
async def execute(self, db: InfrahubDatabase) -> MigrationResult:
|
|
42
|
+
# overrides parent class to skip transaction in case there are a lot of relationships to delete
|
|
43
|
+
return await self.do_execute(db=db)
|