Skip to content

Commit e5d56fe

Browse files
committed
Ensure default kind is properly propagated and that peer schema can't be a generic
Add `menu validate` command to validate the format of menu files.
1 parent 7b264c2 commit e5d56fe

File tree

5 files changed

+138
-31
lines changed

5 files changed

+138
-31
lines changed

changelog/+menu-validate.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `menu validate` command to validate the format of menu files.

docs/docs/infrahubctl/infrahubctl-menu.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ $ infrahubctl menu [OPTIONS] COMMAND [ARGS]...
1717
**Commands**:
1818

1919
* `load`: Load one or multiple menu files into...
20+
* `validate`: Validate one or multiple menu files.
2021

2122
## `infrahubctl menu load`
2223

@@ -38,3 +39,24 @@ $ infrahubctl menu load [OPTIONS] MENUS...
3839
* `--branch TEXT`: Branch on which to load the menu.
3940
* `--config-file TEXT`: [env var: INFRAHUBCTL_CONFIG; default: infrahubctl.toml]
4041
* `--help`: Show this message and exit.
42+
43+
## `infrahubctl menu validate`
44+
45+
Validate one or multiple menu files.
46+
47+
**Usage**:
48+
49+
```console
50+
$ infrahubctl menu validate [OPTIONS] PATHS...
51+
```
52+
53+
**Arguments**:
54+
55+
* `PATHS...`: [required]
56+
57+
**Options**:
58+
59+
* `--debug / --no-debug`: [default: no-debug]
60+
* `--branch TEXT`: Branch on which to validate the objects.
61+
* `--config-file TEXT`: [env var: INFRAHUBCTL_CONFIG; default: infrahubctl.toml]
62+
* `--help`: Show this message and exit.

infrahub_sdk/ctl/menu.py

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
from ..async_typer import AsyncTyper
88
from ..ctl.client import initialize_client
99
from ..ctl.utils import catch_exception, init_logging
10-
from ..exceptions import ObjectValidationError
10+
from ..exceptions import ObjectValidationError, ValidationError
1111
from ..spec.menu import MenuFile
1212
from .parameters import CONFIG_PARAM
13-
from .utils import load_yamlfile_from_disk_and_exit
13+
from .utils import (
14+
display_object_validate_format_error,
15+
display_object_validate_format_success,
16+
load_yamlfile_from_disk_and_exit,
17+
)
1418

1519
app = AsyncTyper()
1620
console = Console()
@@ -40,21 +44,55 @@ async def load(
4044
files = load_yamlfile_from_disk_and_exit(paths=menus, file_type=MenuFile, console=console)
4145
client = initialize_client()
4246

47+
has_errors = False
48+
49+
for file in files:
50+
try:
51+
await file.validate_format(client=client, branch=branch)
52+
except ValidationError as exc:
53+
has_errors = True
54+
display_object_validate_format_error(file=file, error=exc, console=console)
55+
56+
if has_errors:
57+
raise typer.Exit(1)
58+
59+
for file in files:
60+
try:
61+
await file.process(client=client, branch=branch)
62+
except ObjectValidationError as exc:
63+
has_errors = True
64+
console.print(f"[red] {exc!s}")
65+
66+
if has_errors:
67+
raise typer.Exit(1)
68+
69+
70+
71+
@app.command()
72+
@catch_exception(console=console)
73+
async def validate(
74+
paths: list[Path],
75+
debug: bool = False,
76+
branch: str = typer.Option(None, help="Branch on which to validate the objects."),
77+
_: str = CONFIG_PARAM,
78+
) -> None:
79+
"""Validate one or multiple menu files."""
80+
81+
init_logging(debug=debug)
82+
83+
logging.getLogger("infrahub_sdk").setLevel(logging.INFO)
84+
85+
files = load_yamlfile_from_disk_and_exit(paths=paths, file_type=MenuFile, console=console)
86+
client = initialize_client()
87+
88+
has_errors = False
4389
for file in files:
44-
file.validate_content()
45-
schema = await client.schema.get(kind=file.spec.kind, branch=branch)
46-
47-
for idx, item in enumerate(file.spec.data):
48-
try:
49-
await file.spec.create_node(
50-
client=client,
51-
schema=schema,
52-
position=[idx + 1],
53-
data=item,
54-
branch=branch,
55-
default_schema_kind=file.spec.kind,
56-
context={"list_index": idx},
57-
)
58-
except ObjectValidationError as exc:
59-
console.print(f"[red] {exc!s}")
60-
raise typer.Exit(1)
90+
try:
91+
await file.validate_format(client=client, branch=branch)
92+
display_object_validate_format_success(file=file, console=console)
93+
except ValidationError as exc:
94+
has_errors = True
95+
display_object_validate_format_error(file=file, error=exc, console=console)
96+
97+
if has_errors:
98+
raise typer.Exit(1)

infrahub_sdk/spec/menu.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from ..yaml import InfrahubFile, InfrahubFileKind
4-
from .object import InfrahubObjectFileData
4+
from .object import InfrahubObjectFileData, ObjectFile
55

66

77
class InfrahubMenuFileData(InfrahubObjectFileData):
@@ -18,7 +18,7 @@ def enrich_node(cls, data: dict, context: dict) -> dict:
1818
return data
1919

2020

21-
class MenuFile(InfrahubFile):
21+
class MenuFile(ObjectFile):
2222
_spec: InfrahubMenuFileData | None = None
2323

2424
@property
@@ -28,7 +28,7 @@ def spec(self) -> InfrahubMenuFileData:
2828
return self._spec
2929

3030
def validate_content(self) -> None:
31-
super().validate_content()
31+
InfrahubFile.validate_content(self)
3232
if self.kind != InfrahubFileKind.MENU:
3333
raise ValueError("File is not an Infrahub Menu file")
3434
self._spec = InfrahubMenuFileData(**self.data.spec)

infrahub_sdk/spec/object.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import BaseModel, Field
77

88
from ..exceptions import ObjectValidationError, ValidationError
9-
from ..schema import RelationshipSchema
9+
from ..schema import GenericSchemaAPI, RelationshipSchema
1010
from ..yaml import InfrahubFile, InfrahubFileKind
1111

1212
if TYPE_CHECKING:
@@ -168,14 +168,21 @@ async def validate_format(self, client: InfrahubClient, branch: str | None = Non
168168
schema = await client.schema.get(kind=self.kind, branch=branch)
169169
for idx, item in enumerate(self.data):
170170
errors.extend(
171-
await self.validate_object(client=client, position=[idx + 1], schema=schema, data=item, branch=branch)
171+
await self.validate_object(
172+
client=client,
173+
position=[idx + 1],
174+
schema=schema,
175+
data=item,
176+
branch=branch,
177+
default_schema_kind=self.kind,
178+
)
172179
)
173180
return errors
174181

175182
async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
176183
schema = await client.schema.get(kind=self.kind, branch=branch)
177184
for idx, item in enumerate(self.data):
178-
await self.create_node(client=client, schema=schema, data=item, position=[idx + 1], branch=branch)
185+
await self.create_node(client=client, schema=schema, data=item, position=[idx + 1], branch=branch, default_schema_kind=self.kind)
179186

180187
@classmethod
181188
async def validate_object(
@@ -186,6 +193,7 @@ async def validate_object(
186193
position: list[int | str],
187194
context: dict | None = None,
188195
branch: str | None = None,
196+
default_schema_kind: str | None = None,
189197
) -> list[ObjectValidationError]:
190198
errors: list[ObjectValidationError] = []
191199
context = context.copy() if context else {}
@@ -234,6 +242,7 @@ async def validate_object(
234242
data=value,
235243
context=context,
236244
branch=branch,
245+
default_schema_kind=default_schema_kind,
237246
)
238247
)
239248

@@ -248,6 +257,7 @@ async def validate_related_nodes(
248257
data: dict | list[dict],
249258
context: dict | None = None,
250259
branch: str | None = None,
260+
default_schema_kind: str | None = None,
251261
) -> list[ObjectValidationError]:
252262
context = context.copy() if context else {}
253263
errors: list[ObjectValidationError] = []
@@ -260,7 +270,9 @@ async def validate_related_nodes(
260270

261271
if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.ONE_OBJ:
262272
peer_kind = data.get("kind") or rel_info.peer_kind
263-
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
273+
peer_schema = await cls.get_peer_schema(
274+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
275+
)
264276

265277
rel_info.find_matching_relationship(peer_schema=peer_schema)
266278
context.update(rel_info.get_context(value="placeholder"))
@@ -273,13 +285,16 @@ async def validate_related_nodes(
273285
data=data["data"],
274286
context=context,
275287
branch=branch,
288+
default_schema_kind=default_schema_kind,
276289
)
277290
)
278291
return errors
279292

280293
if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.MANY_OBJ_DICT_LIST:
281294
peer_kind = data.get("kind") or rel_info.peer_kind
282-
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
295+
peer_schema = await cls.get_peer_schema(
296+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
297+
)
283298

284299
rel_info.find_matching_relationship(peer_schema=peer_schema)
285300
context.update(rel_info.get_context(value="placeholder"))
@@ -294,6 +309,7 @@ async def validate_related_nodes(
294309
data=peer_data,
295310
context=context,
296311
branch=branch,
312+
default_schema_kind=default_schema_kind,
297313
)
298314
)
299315
return errors
@@ -302,7 +318,9 @@ async def validate_related_nodes(
302318
for idx, item in enumerate(data):
303319
context["list_index"] = idx
304320
peer_kind = item.get("kind") or rel_info.peer_kind
305-
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
321+
peer_schema = await cls.get_peer_schema(
322+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
323+
)
306324

307325
rel_info.find_matching_relationship(peer_schema=peer_schema)
308326
context.update(rel_info.get_context(value="placeholder"))
@@ -315,6 +333,7 @@ async def validate_related_nodes(
315333
data=item["data"],
316334
context=context,
317335
branch=branch,
336+
default_schema_kind=default_schema_kind,
318337
)
319338
)
320339
return errors
@@ -345,7 +364,13 @@ async def create_node(
345364
context = context.copy() if context else {}
346365

347366
errors = await cls.validate_object(
348-
client=client, position=position, schema=schema, data=data, context=context, branch=branch
367+
client=client,
368+
position=position,
369+
schema=schema,
370+
data=data,
371+
context=context,
372+
branch=branch,
373+
default_schema_kind=default_schema_kind,
349374
)
350375
if errors:
351376
messages = [str(error) for error in errors]
@@ -480,7 +505,9 @@ async def create_related_nodes(
480505

481506
if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.MANY_OBJ_DICT_LIST:
482507
peer_kind = data.get("kind") or rel_info.peer_kind
483-
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
508+
peer_schema = await cls.get_peer_schema(
509+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
510+
)
484511

485512
if parent_node:
486513
rel_info.find_matching_relationship(peer_schema=peer_schema)
@@ -506,7 +533,9 @@ async def create_related_nodes(
506533
context["list_index"] = idx
507534

508535
peer_kind = item.get("kind") or rel_info.peer_kind
509-
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
536+
peer_schema = await cls.get_peer_schema(
537+
client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
538+
)
510539

511540
if parent_node:
512541
rel_info.find_matching_relationship(peer_schema=peer_schema)
@@ -529,6 +558,23 @@ async def create_related_nodes(
529558
f"Relationship {rel_info.rel_schema.name} doesn't have the right format {rel_info.rel_schema.cardinality} / {type(data)}"
530559
)
531560

561+
@classmethod
562+
async def get_peer_schema(
563+
cls, client: InfrahubClient, peer_kind: str, branch: str | None = None, default_schema_kind: str | None = None
564+
) -> MainSchemaTypesAPI:
565+
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
566+
if not isinstance(peer_schema, GenericSchemaAPI):
567+
return peer_schema
568+
569+
if not default_schema_kind:
570+
raise ValueError(f"Found a peer schema as a generic {peer_kind} but no default value was provided")
571+
572+
# if the initial peer_kind was a generic, we try the default_schema_kind
573+
peer_schema = await client.schema.get(kind=default_schema_kind, branch=branch)
574+
if isinstance(peer_schema, GenericSchemaAPI):
575+
raise ValueError(f"Default schema kind {default_schema_kind} can't be a generic")
576+
return peer_schema
577+
532578

533579
class ObjectFile(InfrahubFile):
534580
_spec: InfrahubObjectFileData | None = None

0 commit comments

Comments
 (0)