infrahub-server 1.2.5__py3-none-any.whl → 1.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/cli/db.py +2 -0
- infrahub/cli/patch.py +153 -0
- infrahub/computed_attribute/models.py +81 -1
- infrahub/computed_attribute/tasks.py +34 -53
- infrahub/core/manager.py +15 -2
- infrahub/core/node/__init__.py +4 -1
- infrahub/core/query/ipam.py +7 -5
- infrahub/core/registry.py +2 -3
- infrahub/core/schema/schema_branch.py +34 -37
- infrahub/database/__init__.py +2 -0
- infrahub/graphql/manager.py +10 -0
- infrahub/graphql/mutations/main.py +4 -5
- infrahub/graphql/mutations/resource_manager.py +3 -3
- infrahub/patch/__init__.py +0 -0
- infrahub/patch/constants.py +13 -0
- infrahub/patch/edge_adder.py +64 -0
- infrahub/patch/edge_deleter.py +33 -0
- infrahub/patch/edge_updater.py +28 -0
- infrahub/patch/models.py +98 -0
- infrahub/patch/plan_reader.py +107 -0
- infrahub/patch/plan_writer.py +92 -0
- infrahub/patch/queries/__init__.py +0 -0
- infrahub/patch/queries/base.py +17 -0
- infrahub/patch/runner.py +254 -0
- infrahub/patch/vertex_adder.py +61 -0
- infrahub/patch/vertex_deleter.py +33 -0
- infrahub/patch/vertex_updater.py +28 -0
- infrahub/tasks/registry.py +4 -1
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/ctl/cli_commands.py +2 -2
- infrahub_sdk/ctl/menu.py +56 -13
- infrahub_sdk/ctl/object.py +55 -5
- infrahub_sdk/ctl/utils.py +22 -1
- infrahub_sdk/exceptions.py +19 -1
- infrahub_sdk/node.py +42 -26
- infrahub_sdk/protocols_generator/__init__.py +0 -0
- infrahub_sdk/protocols_generator/constants.py +28 -0
- infrahub_sdk/{code_generator.py → protocols_generator/generator.py} +47 -34
- infrahub_sdk/protocols_generator/template.j2 +114 -0
- infrahub_sdk/schema/__init__.py +110 -74
- infrahub_sdk/schema/main.py +36 -2
- infrahub_sdk/schema/repository.py +2 -0
- infrahub_sdk/spec/menu.py +3 -3
- infrahub_sdk/spec/object.py +522 -41
- infrahub_sdk/testing/docker.py +4 -5
- infrahub_sdk/testing/schemas/animal.py +7 -0
- infrahub_sdk/yaml.py +63 -7
- {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/METADATA +1 -1
- {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/RECORD +56 -39
- infrahub_testcontainers/container.py +52 -2
- infrahub_testcontainers/docker-compose.test.yml +27 -0
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub_testcontainers/plugin.py +1 -1
- infrahub_sdk/ctl/constants.py +0 -115
- {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/WHEEL +0 -0
- {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/entry_points.txt +0 -0
infrahub_sdk/spec/object.py
CHANGED
|
@@ -1,20 +1,363 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
8
|
+
from ..exceptions import ObjectValidationError, ValidationError
|
|
9
|
+
from ..schema import GenericSchemaAPI, RelationshipKind, RelationshipSchema
|
|
7
10
|
from ..yaml import InfrahubFile, InfrahubFileKind
|
|
8
11
|
|
|
9
12
|
if TYPE_CHECKING:
|
|
10
13
|
from ..client import InfrahubClient
|
|
11
|
-
from ..
|
|
14
|
+
from ..node import InfrahubNode
|
|
15
|
+
from ..schema import MainSchemaTypesAPI, RelationshipSchema
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_list_of_scalars(value: list[Any]) -> bool:
|
|
19
|
+
return all(isinstance(item, (str, int, float, bool)) for item in value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_list_of_hfids(value: list[Any]) -> bool:
|
|
23
|
+
return all(isinstance(item, (str, list)) for item in value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_list_of_data_dicts(value: list[Any]) -> bool:
|
|
27
|
+
return all(isinstance(item, dict) and "data" in item for item in value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_list_of_objects(value: list[Any]) -> bool:
|
|
31
|
+
return all(isinstance(item, dict) for item in value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RelationshipDataFormat(str, Enum):
|
|
35
|
+
UNKNOWN = "unknown"
|
|
36
|
+
|
|
37
|
+
ONE_REF = "one_ref"
|
|
38
|
+
ONE_OBJ = "one_obj"
|
|
39
|
+
|
|
40
|
+
MANY_OBJ_DICT_LIST = "many_obj_dict_list"
|
|
41
|
+
MANY_OBJ_LIST_DICT = "many_obj_list_dict"
|
|
42
|
+
MANY_REF = "many_ref_list"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RelationshipInfo(BaseModel):
|
|
46
|
+
name: str
|
|
47
|
+
rel_schema: RelationshipSchema
|
|
48
|
+
peer_kind: str
|
|
49
|
+
peer_rel: RelationshipSchema | None = None
|
|
50
|
+
reason_relationship_not_valid: str | None = None
|
|
51
|
+
format: RelationshipDataFormat = RelationshipDataFormat.UNKNOWN
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_bidirectional(self) -> bool:
|
|
55
|
+
"""Indicate if a relationship with the same identifier exists on the other side"""
|
|
56
|
+
return bool(self.peer_rel)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_mandatory(self) -> bool:
|
|
60
|
+
if not self.peer_rel:
|
|
61
|
+
return False
|
|
62
|
+
# For hierarchical node, currently the relationship to the parent is always optional in the schema even if it's mandatory
|
|
63
|
+
# In order to build the tree from top to bottom, we need to consider it as mandatory
|
|
64
|
+
# While it should technically work bottom-up, it created some unexpected behavior while loading the menu
|
|
65
|
+
if self.peer_rel.cardinality == "one" and self.peer_rel.kind == RelationshipKind.HIERARCHY:
|
|
66
|
+
return True
|
|
67
|
+
return not self.peer_rel.optional
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_valid(self) -> bool:
|
|
71
|
+
return not self.reason_relationship_not_valid
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def is_reference(self) -> bool:
|
|
75
|
+
return self.format in [RelationshipDataFormat.ONE_REF, RelationshipDataFormat.MANY_REF]
|
|
76
|
+
|
|
77
|
+
def get_context(self, value: Any) -> dict:
|
|
78
|
+
"""Return a dict to insert to the context if the relationship is mandatory"""
|
|
79
|
+
if self.peer_rel and self.is_mandatory and self.peer_rel.cardinality == "one":
|
|
80
|
+
return {self.peer_rel.name: value}
|
|
81
|
+
if self.peer_rel and self.is_mandatory and self.peer_rel.cardinality == "many":
|
|
82
|
+
return {self.peer_rel.name: [value]}
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
def find_matching_relationship(
|
|
86
|
+
self, peer_schema: MainSchemaTypesAPI, force: bool = False
|
|
87
|
+
) -> RelationshipSchema | None:
|
|
88
|
+
"""Find the matching relationship on the other side of the relationship"""
|
|
89
|
+
if self.peer_rel and not force:
|
|
90
|
+
return self.peer_rel
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
self.peer_rel = peer_schema.get_matching_relationship(
|
|
94
|
+
id=self.rel_schema.identifier or "", direction=self.rel_schema.direction
|
|
95
|
+
)
|
|
96
|
+
except ValueError:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
return self.peer_rel
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def get_relationship_info(
|
|
103
|
+
client: InfrahubClient, schema: MainSchemaTypesAPI, name: str, value: Any, branch: str | None = None
|
|
104
|
+
) -> RelationshipInfo:
|
|
105
|
+
"""
|
|
106
|
+
Get the relationship info for a given relationship name.
|
|
107
|
+
"""
|
|
108
|
+
rel_schema = schema.get_relationship(name=name)
|
|
109
|
+
|
|
110
|
+
info = RelationshipInfo(name=name, peer_kind=rel_schema.peer, rel_schema=rel_schema)
|
|
111
|
+
|
|
112
|
+
if isinstance(value, dict) and "data" not in value:
|
|
113
|
+
info.reason_relationship_not_valid = f"Relationship {name} must be a dict with 'data'"
|
|
114
|
+
return info
|
|
115
|
+
|
|
116
|
+
if isinstance(value, dict) and "kind" in value:
|
|
117
|
+
info.peer_kind = value["kind"]
|
|
118
|
+
|
|
119
|
+
peer_schema = await client.schema.get(kind=info.peer_kind, branch=branch)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
info.peer_rel = peer_schema.get_matching_relationship(
|
|
123
|
+
id=rel_schema.identifier or "", direction=rel_schema.direction
|
|
124
|
+
)
|
|
125
|
+
except ValueError:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
if rel_schema.cardinality == "one" and isinstance(value, list):
|
|
129
|
+
# validate the list is composed of string
|
|
130
|
+
if validate_list_of_scalars(value):
|
|
131
|
+
info.format = RelationshipDataFormat.ONE_REF
|
|
132
|
+
else:
|
|
133
|
+
info.reason_relationship_not_valid = "Too many objects provided for a relationship of cardinality one"
|
|
134
|
+
|
|
135
|
+
elif rel_schema.cardinality == "one" and isinstance(value, str):
|
|
136
|
+
info.format = RelationshipDataFormat.ONE_REF
|
|
137
|
+
|
|
138
|
+
elif rel_schema.cardinality == "one" and isinstance(value, dict) and "data" in value:
|
|
139
|
+
info.format = RelationshipDataFormat.ONE_OBJ
|
|
140
|
+
|
|
141
|
+
elif (
|
|
142
|
+
rel_schema.cardinality == "many"
|
|
143
|
+
and isinstance(value, dict)
|
|
144
|
+
and "data" in value
|
|
145
|
+
and validate_list_of_objects(value["data"])
|
|
146
|
+
):
|
|
147
|
+
# Initial format, we need to support it for backward compatibility for menu
|
|
148
|
+
# it's helpful if there is only one type of object to manage
|
|
149
|
+
info.format = RelationshipDataFormat.MANY_OBJ_DICT_LIST
|
|
150
|
+
|
|
151
|
+
elif rel_schema.cardinality == "many" and isinstance(value, dict) and "data" not in value:
|
|
152
|
+
info.reason_relationship_not_valid = "Invalid structure for a relationship of cardinality many,"
|
|
153
|
+
" either provide a dict with data as a list or a list of objects"
|
|
154
|
+
|
|
155
|
+
elif rel_schema.cardinality == "many" and isinstance(value, list):
|
|
156
|
+
if validate_list_of_data_dicts(value):
|
|
157
|
+
info.format = RelationshipDataFormat.MANY_OBJ_LIST_DICT
|
|
158
|
+
elif validate_list_of_hfids(value):
|
|
159
|
+
info.format = RelationshipDataFormat.MANY_REF
|
|
160
|
+
else:
|
|
161
|
+
info.reason_relationship_not_valid = "Invalid structure for a relationship of cardinality many,"
|
|
162
|
+
" either provide a list of dict with data or a list of hfids"
|
|
163
|
+
|
|
164
|
+
return info
|
|
12
165
|
|
|
13
166
|
|
|
14
167
|
class InfrahubObjectFileData(BaseModel):
|
|
15
168
|
kind: str
|
|
16
169
|
data: list[dict[str, Any]] = Field(default_factory=list)
|
|
17
170
|
|
|
171
|
+
async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ObjectValidationError]:
|
|
172
|
+
errors: list[ObjectValidationError] = []
|
|
173
|
+
schema = await client.schema.get(kind=self.kind, branch=branch)
|
|
174
|
+
for idx, item in enumerate(self.data):
|
|
175
|
+
errors.extend(
|
|
176
|
+
await self.validate_object(
|
|
177
|
+
client=client,
|
|
178
|
+
position=[idx + 1],
|
|
179
|
+
schema=schema,
|
|
180
|
+
data=item,
|
|
181
|
+
branch=branch,
|
|
182
|
+
default_schema_kind=self.kind,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
return errors
|
|
186
|
+
|
|
187
|
+
async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
|
|
188
|
+
schema = await client.schema.get(kind=self.kind, branch=branch)
|
|
189
|
+
for idx, item in enumerate(self.data):
|
|
190
|
+
await self.create_node(
|
|
191
|
+
client=client,
|
|
192
|
+
schema=schema,
|
|
193
|
+
data=item,
|
|
194
|
+
position=[idx + 1],
|
|
195
|
+
branch=branch,
|
|
196
|
+
default_schema_kind=self.kind,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
async def validate_object(
|
|
201
|
+
cls,
|
|
202
|
+
client: InfrahubClient,
|
|
203
|
+
schema: MainSchemaTypesAPI,
|
|
204
|
+
data: dict,
|
|
205
|
+
position: list[int | str],
|
|
206
|
+
context: dict | None = None,
|
|
207
|
+
branch: str | None = None,
|
|
208
|
+
default_schema_kind: str | None = None,
|
|
209
|
+
) -> list[ObjectValidationError]:
|
|
210
|
+
errors: list[ObjectValidationError] = []
|
|
211
|
+
context = context.copy() if context else {}
|
|
212
|
+
|
|
213
|
+
# First validate if all mandatory fields are present
|
|
214
|
+
for element in schema.mandatory_input_names:
|
|
215
|
+
if not any([element in data.keys(), element in context.keys()]):
|
|
216
|
+
errors.append(ObjectValidationError(position=position + [element], message=f"{element} is mandatory"))
|
|
217
|
+
|
|
218
|
+
# Validate if all attributes are valid
|
|
219
|
+
for key, value in data.items():
|
|
220
|
+
if key not in schema.attribute_names and key not in schema.relationship_names:
|
|
221
|
+
errors.append(
|
|
222
|
+
ObjectValidationError(
|
|
223
|
+
position=position + [key],
|
|
224
|
+
message=f"{key} is not a valid attribute or relationship for {schema.kind}",
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if key in schema.attribute_names:
|
|
229
|
+
if not isinstance(value, (str, int, float, bool, list, dict)):
|
|
230
|
+
errors.append(
|
|
231
|
+
ObjectValidationError(
|
|
232
|
+
position=position + [key],
|
|
233
|
+
message=f"{key} must be a string, int, float, bool, list, or dict",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if key in schema.relationship_names:
|
|
238
|
+
rel_info = await get_relationship_info(
|
|
239
|
+
client=client, schema=schema, name=key, value=value, branch=branch
|
|
240
|
+
)
|
|
241
|
+
if not rel_info.is_valid:
|
|
242
|
+
errors.append(
|
|
243
|
+
ObjectValidationError(
|
|
244
|
+
position=position + [key],
|
|
245
|
+
message=rel_info.reason_relationship_not_valid or "Invalid relationship",
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
errors.extend(
|
|
250
|
+
await cls.validate_related_nodes(
|
|
251
|
+
client=client,
|
|
252
|
+
position=position + [key],
|
|
253
|
+
rel_info=rel_info,
|
|
254
|
+
data=value,
|
|
255
|
+
context=context,
|
|
256
|
+
branch=branch,
|
|
257
|
+
default_schema_kind=default_schema_kind,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return errors
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
async def validate_related_nodes(
|
|
265
|
+
cls,
|
|
266
|
+
client: InfrahubClient,
|
|
267
|
+
position: list[int | str],
|
|
268
|
+
rel_info: RelationshipInfo,
|
|
269
|
+
data: dict | list[dict],
|
|
270
|
+
context: dict | None = None,
|
|
271
|
+
branch: str | None = None,
|
|
272
|
+
default_schema_kind: str | None = None,
|
|
273
|
+
) -> list[ObjectValidationError]:
|
|
274
|
+
context = context.copy() if context else {}
|
|
275
|
+
errors: list[ObjectValidationError] = []
|
|
276
|
+
|
|
277
|
+
if isinstance(data, (list, str)) and rel_info.format == RelationshipDataFormat.ONE_REF:
|
|
278
|
+
return errors
|
|
279
|
+
|
|
280
|
+
if isinstance(data, list) and rel_info.format == RelationshipDataFormat.MANY_REF:
|
|
281
|
+
return errors
|
|
282
|
+
|
|
283
|
+
if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.ONE_OBJ:
|
|
284
|
+
peer_kind = data.get("kind") or rel_info.peer_kind
|
|
285
|
+
peer_schema = await cls.get_peer_schema(
|
|
286
|
+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
rel_info.find_matching_relationship(peer_schema=peer_schema)
|
|
290
|
+
context.update(rel_info.get_context(value="placeholder"))
|
|
291
|
+
|
|
292
|
+
errors.extend(
|
|
293
|
+
await cls.validate_object(
|
|
294
|
+
client=client,
|
|
295
|
+
position=position,
|
|
296
|
+
schema=peer_schema,
|
|
297
|
+
data=data["data"],
|
|
298
|
+
context=context,
|
|
299
|
+
branch=branch,
|
|
300
|
+
default_schema_kind=default_schema_kind,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
return errors
|
|
304
|
+
|
|
305
|
+
if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.MANY_OBJ_DICT_LIST:
|
|
306
|
+
peer_kind = data.get("kind") or rel_info.peer_kind
|
|
307
|
+
peer_schema = await cls.get_peer_schema(
|
|
308
|
+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
rel_info.find_matching_relationship(peer_schema=peer_schema)
|
|
312
|
+
context.update(rel_info.get_context(value="placeholder"))
|
|
313
|
+
|
|
314
|
+
for idx, peer_data in enumerate(data["data"]):
|
|
315
|
+
context["list_index"] = idx
|
|
316
|
+
errors.extend(
|
|
317
|
+
await cls.validate_object(
|
|
318
|
+
client=client,
|
|
319
|
+
position=position + [idx + 1],
|
|
320
|
+
schema=peer_schema,
|
|
321
|
+
data=peer_data,
|
|
322
|
+
context=context,
|
|
323
|
+
branch=branch,
|
|
324
|
+
default_schema_kind=default_schema_kind,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
return errors
|
|
328
|
+
|
|
329
|
+
if isinstance(data, list) and rel_info.format == RelationshipDataFormat.MANY_OBJ_LIST_DICT:
|
|
330
|
+
for idx, item in enumerate(data):
|
|
331
|
+
context["list_index"] = idx
|
|
332
|
+
peer_kind = item.get("kind") or rel_info.peer_kind
|
|
333
|
+
peer_schema = await cls.get_peer_schema(
|
|
334
|
+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
rel_info.find_matching_relationship(peer_schema=peer_schema)
|
|
338
|
+
context.update(rel_info.get_context(value="placeholder"))
|
|
339
|
+
|
|
340
|
+
errors.extend(
|
|
341
|
+
await cls.validate_object(
|
|
342
|
+
client=client,
|
|
343
|
+
position=position + [idx + 1],
|
|
344
|
+
schema=peer_schema,
|
|
345
|
+
data=item["data"],
|
|
346
|
+
context=context,
|
|
347
|
+
branch=branch,
|
|
348
|
+
default_schema_kind=default_schema_kind,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
return errors
|
|
352
|
+
|
|
353
|
+
errors.append(
|
|
354
|
+
ObjectValidationError(
|
|
355
|
+
position=position,
|
|
356
|
+
message=f"Relationship {rel_info.rel_schema.name} doesn't have the right format {rel_info.rel_schema.cardinality} / {type(data)}",
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
return errors
|
|
360
|
+
|
|
18
361
|
@classmethod
|
|
19
362
|
def enrich_node(cls, data: dict, context: dict) -> dict: # noqa: ARG003
|
|
20
363
|
return data
|
|
@@ -25,35 +368,82 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
25
368
|
client: InfrahubClient,
|
|
26
369
|
schema: MainSchemaTypesAPI,
|
|
27
370
|
data: dict,
|
|
371
|
+
position: list[int | str],
|
|
28
372
|
context: dict | None = None,
|
|
29
373
|
branch: str | None = None,
|
|
30
374
|
default_schema_kind: str | None = None,
|
|
31
|
-
) ->
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
375
|
+
) -> InfrahubNode:
|
|
376
|
+
context = context.copy() if context else {}
|
|
377
|
+
|
|
378
|
+
errors = await cls.validate_object(
|
|
379
|
+
client=client,
|
|
380
|
+
position=position,
|
|
381
|
+
schema=schema,
|
|
382
|
+
data=data,
|
|
383
|
+
context=context,
|
|
384
|
+
branch=branch,
|
|
385
|
+
default_schema_kind=default_schema_kind,
|
|
386
|
+
)
|
|
387
|
+
if errors:
|
|
388
|
+
messages = [str(error) for error in errors]
|
|
389
|
+
raise ObjectValidationError(position=position, message="Object is not valid - " + ", ".join(messages))
|
|
36
390
|
|
|
37
391
|
clean_data: dict[str, Any] = {}
|
|
38
392
|
|
|
393
|
+
# List of relationships that need to be processed after the current object has been created
|
|
39
394
|
remaining_rels = []
|
|
395
|
+
rels_info: dict[str, RelationshipInfo] = {}
|
|
396
|
+
|
|
40
397
|
for key, value in data.items():
|
|
41
398
|
if key in schema.attribute_names:
|
|
42
399
|
clean_data[key] = value
|
|
400
|
+
continue
|
|
43
401
|
|
|
44
402
|
if key in schema.relationship_names:
|
|
45
|
-
|
|
403
|
+
rel_info = await get_relationship_info(
|
|
404
|
+
client=client, schema=schema, name=key, value=value, branch=branch
|
|
405
|
+
)
|
|
406
|
+
rels_info[key] = rel_info
|
|
46
407
|
|
|
47
|
-
if
|
|
48
|
-
|
|
408
|
+
if not rel_info.is_valid:
|
|
409
|
+
client.log.info(rel_info.reason_relationship_not_valid)
|
|
410
|
+
continue
|
|
49
411
|
|
|
50
|
-
#
|
|
51
|
-
if
|
|
412
|
+
# We need to determine if the related object depend on this object or if this is the other way around.
|
|
413
|
+
# - if the relationship is bidirectional and is mandatory on the other side, then we need to create this object First
|
|
414
|
+
# - if the relationship is bidirectional and is not mandatory on the other side, then we need should create the related object First
|
|
415
|
+
# - if the relationship is not bidirectional, then we need to create the related object First
|
|
416
|
+
if rel_info.is_reference and isinstance(value, list):
|
|
52
417
|
clean_data[key] = value
|
|
53
|
-
elif
|
|
418
|
+
elif rel_info.format == RelationshipDataFormat.ONE_REF and isinstance(value, str):
|
|
54
419
|
clean_data[key] = [value]
|
|
55
|
-
|
|
420
|
+
elif not rel_info.is_reference and rel_info.is_bidirectional and rel_info.is_mandatory:
|
|
56
421
|
remaining_rels.append(key)
|
|
422
|
+
elif not rel_info.is_reference and not rel_info.is_mandatory:
|
|
423
|
+
if rel_info.format == RelationshipDataFormat.ONE_OBJ:
|
|
424
|
+
nodes = await cls.create_related_nodes(
|
|
425
|
+
client=client,
|
|
426
|
+
position=position,
|
|
427
|
+
rel_info=rel_info,
|
|
428
|
+
data=value,
|
|
429
|
+
branch=branch,
|
|
430
|
+
default_schema_kind=default_schema_kind,
|
|
431
|
+
)
|
|
432
|
+
clean_data[key] = nodes[0]
|
|
433
|
+
|
|
434
|
+
else:
|
|
435
|
+
nodes = await cls.create_related_nodes(
|
|
436
|
+
client=client,
|
|
437
|
+
position=position,
|
|
438
|
+
rel_info=rel_info,
|
|
439
|
+
data=value,
|
|
440
|
+
branch=branch,
|
|
441
|
+
default_schema_kind=default_schema_kind,
|
|
442
|
+
)
|
|
443
|
+
clean_data[key] = nodes
|
|
444
|
+
|
|
445
|
+
else:
|
|
446
|
+
raise ValueError(f"Situation unaccounted for: {rel_info}")
|
|
57
447
|
|
|
58
448
|
if context:
|
|
59
449
|
clean_context = {
|
|
@@ -67,54 +457,136 @@ class InfrahubObjectFileData(BaseModel):
|
|
|
67
457
|
|
|
68
458
|
node = await client.create(kind=schema.kind, branch=branch, data=clean_data)
|
|
69
459
|
await node.save(allow_upsert=True)
|
|
460
|
+
|
|
70
461
|
display_label = node.get_human_friendly_id_as_string() or f"{node.get_kind()} : {node.id}"
|
|
71
462
|
client.log.info(f"Node: {display_label}")
|
|
72
463
|
|
|
73
464
|
for rel in remaining_rels:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
465
|
+
context = {}
|
|
466
|
+
|
|
467
|
+
# If there is a peer relationship, we add the node id to the context
|
|
468
|
+
rel_info = rels_info[rel]
|
|
469
|
+
context.update(rel_info.get_context(value=node.id))
|
|
470
|
+
|
|
471
|
+
await cls.create_related_nodes(
|
|
472
|
+
client=client,
|
|
473
|
+
parent_node=node,
|
|
474
|
+
rel_info=rel_info,
|
|
475
|
+
position=position,
|
|
476
|
+
data=data[rel],
|
|
477
|
+
context=context,
|
|
478
|
+
branch=branch,
|
|
479
|
+
default_schema_kind=default_schema_kind,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return node
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
async def create_related_nodes(
|
|
486
|
+
cls,
|
|
487
|
+
client: InfrahubClient,
|
|
488
|
+
rel_info: RelationshipInfo,
|
|
489
|
+
position: list[int | str],
|
|
490
|
+
data: dict | list[dict],
|
|
491
|
+
parent_node: InfrahubNode | None = None,
|
|
492
|
+
context: dict | None = None,
|
|
493
|
+
branch: str | None = None,
|
|
494
|
+
default_schema_kind: str | None = None,
|
|
495
|
+
) -> list[InfrahubNode]:
|
|
496
|
+
nodes: list[InfrahubNode] = []
|
|
497
|
+
context = context.copy() if context else {}
|
|
77
498
|
|
|
78
|
-
|
|
79
|
-
peer_kind = data
|
|
499
|
+
if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.ONE_OBJ:
|
|
500
|
+
peer_kind = data.get("kind") or rel_info.peer_kind
|
|
80
501
|
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
|
|
81
502
|
|
|
82
|
-
if
|
|
83
|
-
|
|
503
|
+
if parent_node:
|
|
504
|
+
rel_info.find_matching_relationship(peer_schema=peer_schema)
|
|
505
|
+
context.update(rel_info.get_context(value=parent_node.id))
|
|
84
506
|
|
|
85
|
-
|
|
507
|
+
new_node = await cls.create_node(
|
|
508
|
+
client=client,
|
|
509
|
+
schema=peer_schema,
|
|
510
|
+
position=position,
|
|
511
|
+
data=data["data"],
|
|
512
|
+
context=context,
|
|
513
|
+
branch=branch,
|
|
514
|
+
default_schema_kind=default_schema_kind,
|
|
515
|
+
)
|
|
516
|
+
return [new_node]
|
|
86
517
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
518
|
+
if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.MANY_OBJ_DICT_LIST:
|
|
519
|
+
peer_kind = data.get("kind") or rel_info.peer_kind
|
|
520
|
+
peer_schema = await cls.get_peer_schema(
|
|
521
|
+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
|
|
522
|
+
)
|
|
91
523
|
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
schema=peer_schema,
|
|
96
|
-
data=rel_data,
|
|
97
|
-
context=context,
|
|
98
|
-
branch=branch,
|
|
99
|
-
default_schema_kind=default_schema_kind,
|
|
100
|
-
)
|
|
524
|
+
if parent_node:
|
|
525
|
+
rel_info.find_matching_relationship(peer_schema=peer_schema)
|
|
526
|
+
context.update(rel_info.get_context(value=parent_node.id))
|
|
101
527
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
await cls.create_node(
|
|
528
|
+
for idx, peer_data in enumerate(data["data"]):
|
|
529
|
+
context["list_index"] = idx
|
|
530
|
+
if isinstance(peer_data, dict):
|
|
531
|
+
node = await cls.create_node(
|
|
106
532
|
client=client,
|
|
107
533
|
schema=peer_schema,
|
|
534
|
+
position=position + [rel_info.name, idx + 1],
|
|
108
535
|
data=peer_data,
|
|
109
536
|
context=context,
|
|
110
537
|
branch=branch,
|
|
111
538
|
default_schema_kind=default_schema_kind,
|
|
112
539
|
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
540
|
+
nodes.append(node)
|
|
541
|
+
return nodes
|
|
542
|
+
|
|
543
|
+
if isinstance(data, list) and rel_info.format == RelationshipDataFormat.MANY_OBJ_LIST_DICT:
|
|
544
|
+
for idx, item in enumerate(data):
|
|
545
|
+
context["list_index"] = idx
|
|
546
|
+
|
|
547
|
+
peer_kind = item.get("kind") or rel_info.peer_kind
|
|
548
|
+
peer_schema = await cls.get_peer_schema(
|
|
549
|
+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
|
|
116
550
|
)
|
|
117
551
|
|
|
552
|
+
if parent_node:
|
|
553
|
+
rel_info.find_matching_relationship(peer_schema=peer_schema)
|
|
554
|
+
context.update(rel_info.get_context(value=parent_node.id))
|
|
555
|
+
|
|
556
|
+
node = await cls.create_node(
|
|
557
|
+
client=client,
|
|
558
|
+
schema=peer_schema,
|
|
559
|
+
position=position + [rel_info.name, idx + 1],
|
|
560
|
+
data=item["data"],
|
|
561
|
+
context=context,
|
|
562
|
+
branch=branch,
|
|
563
|
+
default_schema_kind=default_schema_kind,
|
|
564
|
+
)
|
|
565
|
+
nodes.append(node)
|
|
566
|
+
|
|
567
|
+
return nodes
|
|
568
|
+
|
|
569
|
+
raise ValueError(
|
|
570
|
+
f"Relationship {rel_info.rel_schema.name} doesn't have the right format {rel_info.rel_schema.cardinality} / {type(data)}"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
async def get_peer_schema(
|
|
575
|
+
cls, client: InfrahubClient, peer_kind: str, branch: str | None = None, default_schema_kind: str | None = None
|
|
576
|
+
) -> MainSchemaTypesAPI:
|
|
577
|
+
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
|
|
578
|
+
if not isinstance(peer_schema, GenericSchemaAPI):
|
|
579
|
+
return peer_schema
|
|
580
|
+
|
|
581
|
+
if not default_schema_kind:
|
|
582
|
+
raise ValueError(f"Found a peer schema as a generic {peer_kind} but no default value was provided")
|
|
583
|
+
|
|
584
|
+
# if the initial peer_kind was a generic, we try the default_schema_kind
|
|
585
|
+
peer_schema = await client.schema.get(kind=default_schema_kind, branch=branch)
|
|
586
|
+
if isinstance(peer_schema, GenericSchemaAPI):
|
|
587
|
+
raise ValueError(f"Default schema kind {default_schema_kind} can't be a generic")
|
|
588
|
+
return peer_schema
|
|
589
|
+
|
|
118
590
|
|
|
119
591
|
class ObjectFile(InfrahubFile):
|
|
120
592
|
_spec: InfrahubObjectFileData | None = None
|
|
@@ -130,3 +602,12 @@ class ObjectFile(InfrahubFile):
|
|
|
130
602
|
if self.kind != InfrahubFileKind.OBJECT:
|
|
131
603
|
raise ValueError("File is not an Infrahub Object file")
|
|
132
604
|
self._spec = InfrahubObjectFileData(**self.data.spec)
|
|
605
|
+
|
|
606
|
+
async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> None:
|
|
607
|
+
self.validate_content()
|
|
608
|
+
errors = await self.spec.validate_format(client=client, branch=branch)
|
|
609
|
+
if errors:
|
|
610
|
+
raise ValidationError(identifier=str(self.location), messages=[str(error) for error in errors])
|
|
611
|
+
|
|
612
|
+
async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
|
|
613
|
+
await self.spec.process(client=client, branch=branch)
|
infrahub_sdk/testing/docker.py
CHANGED
|
@@ -8,13 +8,16 @@ from packaging.version import InvalidVersion, Version
|
|
|
8
8
|
|
|
9
9
|
from .. import Config, InfrahubClient, InfrahubClientSync
|
|
10
10
|
|
|
11
|
-
INFRAHUB_VERSION = os.getenv("INFRAHUB_TESTING_IMAGE_VER"
|
|
11
|
+
INFRAHUB_VERSION = os.getenv("INFRAHUB_TESTING_IMAGE_VER")
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def skip_version(min_infrahub_version: str | None = None, max_infrahub_version: str | None = None) -> bool:
|
|
15
15
|
"""
|
|
16
16
|
Check if a test should be skipped depending on infrahub version.
|
|
17
17
|
"""
|
|
18
|
+
if INFRAHUB_VERSION is None:
|
|
19
|
+
return True
|
|
20
|
+
|
|
18
21
|
try:
|
|
19
22
|
version = Version(INFRAHUB_VERSION)
|
|
20
23
|
except InvalidVersion:
|
|
@@ -31,10 +34,6 @@ def skip_version(min_infrahub_version: str | None = None, max_infrahub_version:
|
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
class TestInfrahubDockerClient(TestInfrahubDocker):
|
|
34
|
-
@pytest.fixture(scope="class")
|
|
35
|
-
def infrahub_version(self) -> str:
|
|
36
|
-
return INFRAHUB_VERSION
|
|
37
|
-
|
|
38
37
|
@pytest.fixture(scope="class")
|
|
39
38
|
def client(self, infrahub_port: int) -> InfrahubClient:
|
|
40
39
|
return InfrahubClient(
|
|
@@ -20,6 +20,7 @@ TESTING_ANIMAL = f"{NAMESPACE}Animal"
|
|
|
20
20
|
TESTING_CAT = f"{NAMESPACE}Cat"
|
|
21
21
|
TESTING_DOG = f"{NAMESPACE}Dog"
|
|
22
22
|
TESTING_PERSON = f"{NAMESPACE}Person"
|
|
23
|
+
BUILTIN_TAG = "BuiltinTag"
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class SchemaAnimal:
|
|
@@ -125,6 +126,12 @@ class SchemaAnimal:
|
|
|
125
126
|
cardinality="many",
|
|
126
127
|
direction=RelationshipDirection.INBOUND,
|
|
127
128
|
),
|
|
129
|
+
Rel(
|
|
130
|
+
name="tags",
|
|
131
|
+
optional=True,
|
|
132
|
+
peer=BUILTIN_TAG,
|
|
133
|
+
cardinality="many",
|
|
134
|
+
),
|
|
128
135
|
],
|
|
129
136
|
)
|
|
130
137
|
|