Skip to content

Commit a91d1db

Browse files
committed
Add integration tests for infrahubctl object command
1 parent 8794721 commit a91d1db

File tree

13 files changed

+694
-173
lines changed

13 files changed

+694
-173
lines changed

infrahub_sdk/ctl/cli_commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
app.add_typer(validate_app, name="validate")
6363
app.add_typer(repository_app, name="repository")
6464
app.add_typer(menu_app, name="menu")
65-
app.add_typer(object_app, name="object", hidden=True)
65+
app.add_typer(object_app, name="object")
6666

6767
app.command(name="dump")(dump)
6868
app.command(name="load")(load)

infrahub_sdk/schema/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class RelationshipSchema(BaseModel):
143143
class RelationshipSchemaAPI(RelationshipSchema):
144144
model_config = ConfigDict(use_enum_values=True)
145145

146+
identifier: str
146147
inherited: bool = False
147148
read_only: bool = False
148149
hierarchical: str | None = None

infrahub_sdk/spec/object.py

Lines changed: 148 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,68 @@
44

55
from pydantic import BaseModel, Field
66

7+
from ..schema import RelationshipSchema
78
from ..yaml import InfrahubFile, InfrahubFileKind
89

910
if TYPE_CHECKING:
1011
from ..client import InfrahubClient
11-
from ..schema import MainSchemaTypesAPI
12+
from ..node import InfrahubNode
13+
from ..schema import MainSchemaTypesAPI, RelationshipSchema
14+
15+
16+
class RelationshipInfo(BaseModel):
17+
name: str
18+
rel_schema: RelationshipSchema
19+
peer_kind: str
20+
peer_rel: RelationshipSchema | None = None
21+
is_reference: bool = True
22+
reason_relationship_not_valid: str | None = None
23+
24+
@property
25+
def is_bidirectional(self) -> bool:
26+
return bool(self.peer_rel)
27+
28+
@property
29+
def is_mandatory(self) -> bool:
30+
if not self.peer_rel:
31+
return False
32+
return not self.peer_rel.optional
33+
34+
@property
35+
def is_valid(self) -> bool:
36+
return not self.reason_relationship_not_valid
37+
38+
39+
async def get_relationship_info(
40+
client: InfrahubClient, schema: MainSchemaTypesAPI, key: str, value: Any, branch: str | None = None
41+
) -> RelationshipInfo:
42+
"""
43+
Get the relationship info for a given relationship name.
44+
"""
45+
rel_schema = schema.get_relationship(name=key)
46+
47+
info = RelationshipInfo(name=key, peer_kind=rel_schema.peer, rel_schema=rel_schema)
48+
49+
if isinstance(value, dict) and "data" not in value:
50+
info.reason_relationship_not_valid = f"Relationship {key} must be a dict with 'data'"
51+
return info
52+
53+
if isinstance(value, dict) and "kind" in value:
54+
info.peer_kind = value["kind"]
55+
56+
peer_schema = await client.schema.get(kind=info.peer_kind, branch=branch)
57+
58+
try:
59+
info.peer_rel = peer_schema.get_matching_relationship(id=rel_schema.identifier, direction=rel_schema.direction)
60+
except ValueError:
61+
pass
62+
63+
# Check if the content of the relationship is a reference to existing objects
64+
# or if it contains the data to create/update related objects
65+
if isinstance(value, dict) and "data" in value:
66+
info.is_reference = False
67+
68+
return info
1269

1370

1471
class InfrahubObjectFileData(BaseModel):
@@ -28,32 +85,60 @@ async def create_node(
2885
context: dict | None = None,
2986
branch: str | None = None,
3087
default_schema_kind: str | None = None,
31-
) -> None:
32-
# First validate of all mandatory fields are present
88+
) -> InfrahubNode:
89+
context = context or {}
90+
91+
# First validate if all mandatory fields are present
3392
for element in schema.mandatory_attribute_names + schema.mandatory_relationship_names:
34-
if element not in data.keys():
93+
if not any([element in data.keys(), element in context.keys()]):
3594
raise ValueError(f"{element} is mandatory")
3695

3796
clean_data: dict[str, Any] = {}
3897

98+
# List of relationships that need to be processed after the current object has been created
3999
remaining_rels = []
100+
rels_info: dict[str, RelationshipInfo] = {}
101+
40102
for key, value in data.items():
41103
if key in schema.attribute_names:
42104
clean_data[key] = value
43105

44106
if key in schema.relationship_names:
45107
rel_schema = schema.get_relationship(name=key)
46108

47-
if isinstance(value, dict) and "data" not in value:
48-
raise ValueError(f"Relationship {key} must be a dict with 'data'")
109+
rel_info = await get_relationship_info(
110+
client=client, schema=schema, key=key, value=value, branch=branch
111+
)
112+
rels_info[key] = rel_info
49113

50-
# This is a simple implementation for now, need to revisit once we have the integration tests
51-
if isinstance(value, (list)):
114+
if not rel_info.is_valid:
115+
client.log.info(rel_info.reason_relationship_not_valid)
116+
continue
117+
118+
# We need to determine if the related object depend on this object or if this is the other way around.
119+
# - if the relationship is bidirectional and is mandatory on the other side, then we need to create this object First
120+
# - if the relationship is bidirectional and is not mandatory on the other side, then we need should create the related object First
121+
# - if the relationship is not bidirectional, then we need to create the related object First
122+
if rel_info.is_reference and isinstance(value, list):
52123
clean_data[key] = value
53-
elif rel_schema.cardinality == "one" and isinstance(value, str):
124+
elif rel_info.is_reference and rel_schema.cardinality == "one" and isinstance(value, str):
54125
clean_data[key] = [value]
55-
else:
126+
elif not rel_info.is_reference and rel_info.is_bidirectional and rel_info.is_mandatory:
56127
remaining_rels.append(key)
128+
elif not rel_info.is_reference and not rel_info.is_mandatory:
129+
nodes = await cls.create_related_nodes(
130+
client=client,
131+
rel_info=rel_info,
132+
data=value["data"],
133+
branch=branch,
134+
default_schema_kind=default_schema_kind,
135+
)
136+
if rel_info.rel_schema.cardinality == "one":
137+
clean_data[key] = nodes[0]
138+
else:
139+
clean_data[key] = nodes
140+
else:
141+
raise ValueError(f"Situation unaccounted for: {rel_info}")
57142

58143
if context:
59144
clean_context = {
@@ -67,53 +152,78 @@ async def create_node(
67152

68153
node = await client.create(kind=schema.kind, branch=branch, data=clean_data)
69154
await node.save(allow_upsert=True)
155+
70156
display_label = node.get_human_friendly_id_as_string() or f"{node.get_kind()} : {node.id}"
71157
client.log.info(f"Node: {display_label}")
72158

73159
for rel in remaining_rels:
74160
# identify what is the name of the relationship on the other side
75-
if not isinstance(data[rel], dict) and "data" in data[rel]:
76-
raise ValueError(f"relationship {rel} must be a dict with 'data'")
77-
78-
rel_schema = schema.get_relationship(name=rel)
79-
peer_kind = data[rel].get("kind", default_schema_kind) or rel_schema.peer
80-
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
161+
rel_info = rels_info[rel]
81162

82163
if rel_schema.identifier is None:
83164
raise ValueError("identifier must be defined")
84165

85-
peer_rel = peer_schema.get_matching_relationship(id=rel_schema.identifier, direction=rel_schema.direction)
86-
87166
rel_data = data[rel]["data"]
88167
context = {}
89-
if peer_rel:
90-
context[peer_rel.name] = node.id
91-
92-
if rel_schema.cardinality == "one" and isinstance(rel_data, dict):
93-
await cls.create_node(
94-
client=client,
95-
schema=peer_schema,
96-
data=rel_data,
97-
context=context,
98-
branch=branch,
99-
default_schema_kind=default_schema_kind,
100-
)
101168

102-
elif rel_schema.cardinality == "many" and isinstance(rel_data, list):
103-
for idx, peer_data in enumerate(rel_data):
104-
context["list_index"] = idx
105-
await cls.create_node(
169+
if rel_info.peer_rel:
170+
context[rel_info.peer_rel.name] = node.id
171+
172+
await cls.create_related_nodes(
173+
client=client,
174+
rel_info=rel_info,
175+
data=rel_data,
176+
context=context,
177+
branch=branch,
178+
default_schema_kind=default_schema_kind,
179+
)
180+
181+
return node
182+
183+
@classmethod
184+
async def create_related_nodes(
185+
cls,
186+
client: InfrahubClient,
187+
rel_info: RelationshipInfo,
188+
data: dict,
189+
context: dict | None = None,
190+
branch: str | None = None,
191+
default_schema_kind: str | None = None,
192+
) -> list[InfrahubNode]:
193+
peer_schema = await client.schema.get(kind=rel_info.peer_kind, branch=branch)
194+
195+
nodes: list[InfrahubNode] = []
196+
197+
if rel_info.rel_schema.cardinality == "one" and isinstance(data, dict):
198+
node = await cls.create_node(
199+
client=client,
200+
schema=peer_schema,
201+
data=data,
202+
context=context,
203+
branch=branch,
204+
default_schema_kind=default_schema_kind,
205+
)
206+
return [node]
207+
208+
if rel_info.rel_schema.cardinality == "many" and isinstance(data, list):
209+
context = context or {}
210+
for idx, peer_data in enumerate(data):
211+
context["list_index"] = idx
212+
if isinstance(peer_data, dict):
213+
node = await cls.create_node(
106214
client=client,
107215
schema=peer_schema,
108216
data=peer_data,
109217
context=context,
110218
branch=branch,
111219
default_schema_kind=default_schema_kind,
112220
)
113-
else:
114-
raise ValueError(
115-
f"Relationship {rel_schema.name} doesn't have the right format {rel_schema.cardinality} / {type(rel_data)}"
116-
)
221+
nodes.append(node)
222+
return nodes
223+
224+
raise ValueError(
225+
f"Relationship {rel_info.rel_schema.name} doesn't have the right format {rel_info.rel_schema.cardinality} / {type(data)}"
226+
)
117227

118228

119229
class ObjectFile(InfrahubFile):

infrahub_sdk/testing/schemas/animal.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
TESTING_CAT = f"{NAMESPACE}Cat"
2121
TESTING_DOG = f"{NAMESPACE}Dog"
2222
TESTING_PERSON = f"{NAMESPACE}Person"
23+
BUILTIN_TAG = "BuiltinTag"
2324

2425

2526
class SchemaAnimal:
@@ -125,6 +126,12 @@ def schema_person(self) -> NodeSchema:
125126
cardinality="many",
126127
direction=RelationshipDirection.INBOUND,
127128
),
129+
Rel(
130+
name="tags",
131+
optional=True,
132+
peer=BUILTIN_TAG,
133+
cardinality="many",
134+
),
128135
],
129136
)
130137

infrahub_sdk/yaml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class InfrahubFileData(BaseModel):
2525
api_version: InfrahubFileApiVersion = Field(InfrahubFileApiVersion.V1, alias="apiVersion")
2626
kind: InfrahubFileKind
2727
spec: dict
28-
metadata: dict | None = Field(default_factory=dict)
28+
metadata: dict | None = Field(default_factory=dict) # type: ignore[arg-type]
2929

3030

3131
class LocalFile(BaseModel):

0 commit comments

Comments
 (0)