Skip to content

Commit 15912bf

Browse files
committed
fix issue 264
1 parent b6028a0 commit 15912bf

13 files changed

Lines changed: 985 additions & 64 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
Export/Import Guide
2+
3+
## Export/Import Link Resolution
4+
5+
The element engine resolves import links to exported resources via
6+
`ElementEngine.get_resource_by_export_link()`. This function matches an import
7+
link against the element's registered exports and returns the corresponding
8+
resource.
9+
10+
### Link Format
11+
12+
**Export full link** is constructed by the `Export.full_link` property:
13+
14+
```text
15+
${element.name}.${link}
16+
```
17+
18+
**Import link** is constructed at the call site:
19+
20+
```text
21+
${element_prefix}.${import_link}
22+
```
23+
24+
Both resolve to the same dotted path structure:
25+
26+
```text
27+
$prefix.namespace.type.$resource_name
28+
```
29+
30+
### Matching Algorithm
31+
32+
The function tries two strategies in order:
33+
34+
1. **Exact match** — the full export link string equals the import link string.
35+
2. **Suffix match** — the export link without its first segment
36+
(`export_parts[1:]`) equals all of the import link parts. This handles the
37+
case where `${element.name}` duplicates the first `$`-prefixed segment of
38+
`link`.
39+
40+
### Examples
41+
42+
#### Simple direct lookup
43+
44+
A manifest exports a resource and the same manifest imports it. The element
45+
prefixes and paths match exactly.
46+
47+
| Side | Link |
48+
|------|------|
49+
| Export full link | `$dbaas.$dbaas.types.postgres.instances.$cluster_pg` |
50+
| Import link | `$dbaas.types.postgres.instances.$cluster_pg` |
51+
52+
- **Exact match**: no (`$dbaas.$dbaas.` != `$dbaas.`)
53+
- **Suffix match**: `export_parts[1:]` = `$dbaas.types.postgres.instances.$cluster_pg` == import link → **match**
54+
55+
#### Cross-element import
56+
57+
Element `baz` imports from element `dbaas`. The element prefix differs, but the
58+
path structure is identical.
59+
60+
| Side | Link |
61+
|------|------|
62+
| Export full link | `$dbaas.$dbaas.types.postgres.instances.$cluster_pg` |
63+
| Import link | `$dbaas.types.postgres.instances.$cluster_pg` |
64+
65+
Same as above — suffix match resolves correctly.
66+
67+
#### Same element, same resource name, different paths
68+
69+
An element exports two resources with the same name in different namespaces.
70+
The import must resolve to the correct one.
71+
72+
| Side | Link |
73+
|------|------|
74+
| Export 1 full link | `$dbaas.$dbaas.types.postgres.instances.$cluster_pg` |
75+
| Export 2 full link | `$dbaas.$dbaas.types.volumes.instances.$cluster_pg` |
76+
| Import link | `$dbaas.types.volumes.instances.$cluster_pg` |
77+
78+
- Export 1 suffix: `$dbaas.types.postgres.instances.$cluster_pg` — no match
79+
- Export 2 suffix: `$dbaas.types.volumes.instances.$cluster_pg`**match**
80+
81+
#### Cross-element with same path structure
82+
83+
Two different elements (`foo` and `bar`) export a resource with the same path
84+
structure and resource name. Element `baz` imports from `bar`.
85+
86+
| Side | Link |
87+
|------|------|
88+
| Export foo full link | `$foo.$foo.types.postgres.versions.$cluster` |
89+
| Export bar full link | `$bar.$bar.types.postgres.versions.$cluster` |
90+
| Import link | `$bar.types.postgres.versions.$cluster` |
91+
92+
- Export foo suffix: `$foo.types.postgres.versions.$cluster` — no match
93+
(first segment `$foo` != `$bar`)
94+
- Export bar suffix: `$bar.types.postgres.versions.$cluster`**match**
95+
96+
#### Export link without element prefix duplication
97+
98+
When the export link does **not** start with the element prefix, `full_link`
99+
has no duplication and the exact match succeeds.
100+
101+
| Side | Link |
102+
|------|------|
103+
| Export full link | `$test_export_node_2.$core.compute.nodes.$test_node` |
104+
| Import link | `$test_export_node_2.$core.compute.nodes.$test_node` |
105+
106+
- **Exact match**: `$test_export_node_2.$core.compute.nodes.$test_node` ==
107+
`$test_export_node_2.$core.compute.nodes.$test_node`**match**
108+
109+
#### Mismatched path structure
110+
111+
The import link has a different intermediate path than any export.
112+
113+
| Side | Link |
114+
|------|------|
115+
| Export full link | `$dbaas.$dbaas.types.postgres.instances.$cluster_pg` |
116+
| Import link | `$stand.types.postgres.versions.$cluster_pg` |
117+
118+
- Exact match: no
119+
- Suffix: `$dbaas.types.postgres.instances.$cluster_pg` !=
120+
`$stand.types.postgres.versions.$cluster_pg`**raises `ValidateException`**
121+
122+
#### Nonexistent resource name
123+
124+
No export has a matching resource name.
125+
126+
| Side | Link |
127+
|------|------|
128+
| Export full link | `$dbaas.$dbaas.types.postgres.instances.$cluster_pg` |
129+
| Import link | `$stand.types.postgres.instances.$nonexistent` |
130+
131+
- Exact match: no
132+
- Suffix: `$dbaas.types.postgres.instances.$cluster_pg` !=
133+
`$stand.types.postgres.instances.$nonexistent`**raises `ValidateException`**

docs/core-developer-guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,5 @@ and so on). See [Troubleshooting](../usage/troubleshooting.md) for the symptoms
7878

7979
- [Manifest reference](../em/manifest.md)
8080
- [Service as a Service API](../em/service.md)
81+
- [Exports reference](./exports.md)
8182
- [Troubleshooting](../usage/troubleshooting.md)

exordos_core/elements/dm/models.py

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ def install(self) -> "Manifest":
173173
174174
"""
175175
if Element.objects.get_one_or_none(filters={"name": ra_filters.EQ(self.name)}):
176-
raise ValueError(f"Element '{self.name}' already exists.")
176+
raise exceptions.ValidateException(
177+
err=f"Element '{self.name}' already exists."
178+
)
177179

178180
element_engine.load_from_database()
179181

@@ -196,8 +198,8 @@ def upgrade(self) -> "Manifest":
196198
filters={"name": ra_filters.EQ(self.name)}
197199
)
198200
if not element:
199-
raise ValueError(
200-
f"Element '{self.name}' does not exist, please install it first."
201+
raise exceptions.ValidateException(
202+
err=f"Element '{self.name}' does not exist, please install it first."
201203
)
202204

203205
element_engine.load_from_database()
@@ -228,7 +230,7 @@ def apply_imports(self, element: "Element"):
228230
from_resource = element_engine.get_resource_by_export_link(
229231
manifest=self,
230232
from_element=import_from_element,
231-
link=import_data["link"],
233+
link=f"{import_data['element']}.{import_data['link']}",
232234
)
233235
import_kwargs = dict(
234236
name=import_name,
@@ -390,8 +392,8 @@ def _check_no_dependents(element: "Element") -> None:
390392
version_matches = False
391393

392394
if version_matches:
393-
raise ValueError(
394-
f"Cannot uninstall element '{element.name}' version '{element.version}' "
395+
raise exceptions.ValidateException(
396+
err=f"Cannot uninstall element '{element.name}' version '{element.version}' "
395397
f"because it is required by element '{other_element.name}' version "
396398
f"'{other_element.version}'."
397399
)
@@ -400,10 +402,10 @@ def uninstall(self) -> "Manifest":
400402
element_engine.load_from_database()
401403
filters = ra_filters.AND(
402404
ra_filters.OR(
403-
{
404-
"name": ra_filters.EQ(self.name),
405-
"version": ra_filters.EQ(self.version),
406-
},
405+
ra_filters.AND(
406+
{"name": ra_filters.EQ(self.name)},
407+
{"version": ra_filters.EQ(self.version)},
408+
),
407409
{"manifest": ra_filters.EQ(self.uuid)},
408410
)
409411
)
@@ -668,8 +670,8 @@ def get_parameter_value(self, parameter: str):
668670
parts = parameter.split(":")
669671
resource_name = parts[0][1:]
670672
if resource_name != self.name:
671-
raise ValueError(
672-
f"Resource name `{resource_name}` does not match the"
673+
raise exceptions.ValidateException(
674+
err=f"Resource name `{resource_name}` does not match the"
673675
f" current resource name `{self.name}`"
674676
)
675677
resource_parameter_path = parts[1:]
@@ -715,8 +717,8 @@ def _fstring_replacement_callback(self, match, engine):
715717
)
716718
return str(value)
717719
except ValueError as e:
718-
raise ValueError(
719-
f"Can't render value `{var}` for resource `{repr(self)}` by reason: {e}"
720+
raise exceptions.ValidateException(
721+
err=f"Can't render value `{var}` for resource `{repr(self)}` by reason: {e}"
720722
)
721723

722724
def _render_value(self, value, engine):
@@ -731,8 +733,8 @@ def _render_value(self, value, engine):
731733
parameter=link.parameter,
732734
)
733735
except ValueError as e:
734-
raise ValueError(
735-
f"Can't render value `{value}` for resource"
736+
raise exceptions.ValidateException(
737+
err=f"Can't render value `{value}` for resource"
736738
f" `{repr(self)}` by reason: {e}"
737739
)
738740
elif value.startswith('f"'):
@@ -944,6 +946,10 @@ class Export(
944946
required=True,
945947
)
946948

949+
@property
950+
def full_link(self) -> str:
951+
return f"${self.element.name}.{self.link}"
952+
947953

948954
class ImportEnum(str, enum.Enum):
949955
RESOURCE = "resource"
@@ -1066,8 +1072,8 @@ def element(self):
10661072

10671073
def add_resource(self, resource: Resource | ImportedResource) -> None:
10681074
if resource.link in self._namespace_resources:
1069-
raise ValueError(
1070-
f"Resource with link string '{resource.link}' in element '{resource.element.name}' already exists."
1075+
raise exceptions.ValidateException(
1076+
err=f"Resource with link string '{resource.link}' in element '{resource.element.name}' already exists."
10711077
)
10721078
self._namespace_resources[resource.link] = resource
10731079

@@ -1076,16 +1082,16 @@ def get_resources(self):
10761082

10771083
def delete_resource(self, resource: Resource | ImportedResource) -> None:
10781084
if resource.link not in self._namespace_resources:
1079-
raise ValueError(
1080-
f"Resource with link string '{resource.link}' in element '{resource.element.name}' does not exist."
1085+
raise exceptions.ValidateException(
1086+
err=f"Resource with link string '{resource.link}' in element '{resource.element.name}' does not exist."
10811087
)
10821088
del self._namespace_resources[resource.link]
10831089

10841090
def get_resource_by_link(self, link):
10851091
clear_link = utils.clear_parameters(link)
10861092
if clear_link not in self._namespace_resources:
1087-
raise ValueError(
1088-
f"Resource with link string '{clear_link}' does not exist."
1093+
raise exceptions.ValidateException(
1094+
err=f"Resource with link string '{clear_link}' does not exist. _namespace_resources: {self._namespace_resources}"
10891095
)
10901096
return self._namespace_resources[clear_link]
10911097

@@ -1128,8 +1134,8 @@ def load_from_database(self) -> None:
11281134
)
11291135
self.add_resource(resource)
11301136
else:
1131-
raise ValueError(
1132-
f"Unsupported import type '{import_.kind}' for import "
1137+
raise exceptions.ValidateException(
1138+
err=f"Unsupported import type '{import_.kind}' for import "
11331139
f"'{import_.name}'. Only '{ImportEnum.RESOURCE.value}' "
11341140
f"imports are currently supported."
11351141
)
@@ -1141,17 +1147,17 @@ def load_from_database(self) -> None:
11411147
if export.kind == ExportEnum.RESOURCE.value:
11421148
self.add_resource_by_export(export)
11431149
else:
1144-
raise ValueError(
1145-
f"Unsupported export type '{export.kind}' for export "
1150+
raise exceptions.ValidateException(
1151+
err=f"Unsupported export type '{export.kind}' for export "
11461152
f"'{export.name}'. Only '{ExportEnum.RESOURCE.value}' "
11471153
f"exports are currently supported."
11481154
)
11491155

11501156
def add_resource(self, resource: Resource | ImportedResource) -> None:
11511157
element = resource.element
11521158
if element.link not in self._namespaces:
1153-
ValueError(
1154-
f"The element '{element}' is unknown. Please add the element"
1159+
exceptions.ValidateException(
1160+
err=f"The element '{element}' is unknown. Please add the element"
11551161
" before adding resources to it."
11561162
)
11571163
namespace = self._namespaces[resource.element.link]
@@ -1169,17 +1175,17 @@ def get_resources(self) -> tp.List["Resource"]:
11691175

11701176
def get_resource_by_link(self, element: "Element", link: str) -> "Resource":
11711177
if element.link not in self._namespaces:
1172-
raise ValueError(
1173-
f"Can't load element {element}. Element"
1178+
raise exceptions.ValidateException(
1179+
err=f"Can't load element {element}. Element"
11741180
f" {self._namespaces[element.link].element} is not found."
11751181
)
11761182
namespace = self._namespaces[element.link]
11771183
return namespace.get_resource_by_link(link)
11781184

11791185
def add_element(self, element: "Element") -> None:
11801186
if element.link in self._namespaces:
1181-
raise ValueError(
1182-
f"Can't load element {element}. Element"
1187+
raise exceptions.ValidateException(
1188+
err=f"Can't load element {element}. Element"
11831189
f" {self._namespaces[element.link].element} already exists"
11841190
" with the same UUID."
11851191
)
@@ -1190,7 +1196,9 @@ def get_element(self, link: str) -> "Element":
11901196

11911197
def remove_element(self, element: "Element") -> None:
11921198
if element.link not in self._namespaces:
1193-
raise ValueError(f"Can't remove element {element}. Element does not exist.")
1199+
raise exceptions.ValidateException(
1200+
err=f"Can't remove element {element}. Element does not exist."
1201+
)
11941202

11951203
del self._namespaces[element.link]
11961204

@@ -1202,24 +1210,28 @@ def add_resource_by_export(self, export: "Export") -> None:
12021210
)
12031211

12041212
if export.link in self._resource_exports:
1205-
raise ValueError(
1206-
f"Resource export with link '{export.link}' already exists."
1213+
raise exceptions.ValidateException(
1214+
err=f"Resource export with link '{export.link}' already exists."
12071215
)
1208-
self._resource_exports[export.link] = resource
1216+
self._resource_exports[export.full_link] = resource
12091217

12101218
def delete_resource_by_export(self, export: "Export") -> None:
1211-
del self._resource_exports[export.link]
1219+
del self._resource_exports[export.full_link]
12121220

12131221
def get_resource_by_export_link(
12141222
self, manifest: "Manifest", from_element: "Element", link: str
12151223
) -> "Resource":
1216-
# Implement check element here for export resources
1217-
if link not in self._resource_exports:
1218-
raise ValueError(
1219-
f"Resource {link} in manifest {manifest.name} ({manifest.version}) is not in export list "
1220-
f"in element {from_element.name}"
1221-
)
1222-
return self._resource_exports[link]
1224+
import_parts = link.split(".")
1225+
for export_link, resource in self._resource_exports.items():
1226+
if export_link == link:
1227+
return resource
1228+
export_parts = export_link.split(".")
1229+
if len(export_parts) > 1 and export_parts[1:] == import_parts:
1230+
return resource
1231+
raise exceptions.ValidateException(
1232+
err=f"Resource {link} in manifest {manifest.name} ({manifest.version}) is not in export list "
1233+
f"in element {from_element.name}"
1234+
)
12231235

12241236

12251237
element_engine = ElementEngine()
@@ -1232,8 +1244,8 @@ def __init__(self, **kwargs):
12321244
if not Service.objects.get_one_or_none(
12331245
filters={"uuid": ra_filters.EQ(self.service)}
12341246
):
1235-
raise ValueError(
1236-
"Service %s does not exist. Please create it first." % self.service
1247+
raise exceptions.ValidateException(
1248+
err="Service %s does not exist. Please create it first." % self.service
12371249
)
12381250

12391251
@classmethod

0 commit comments

Comments
 (0)