44
55from pydantic import BaseModel , Field
66
7+ from ..schema import RelationshipSchema
78from ..yaml import InfrahubFile , InfrahubFileKind
89
910if 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
1471class 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
119229class ObjectFile (InfrahubFile ):
0 commit comments