Skip to content

Commit 8222579

Browse files
authored
Merge pull request #73 from opsmill/dga-20241012-menu
Add `infrahubctl menu load` command to load menu items from a file
2 parents d03e036 + cd83cde commit 8222579

File tree

10 files changed

+385
-50
lines changed

10 files changed

+385
-50
lines changed

infrahub_sdk/ctl/cli_commands.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@
2222
from infrahub_sdk.ctl.client import initialize_client, initialize_client_sync
2323
from infrahub_sdk.ctl.exceptions import QueryNotFoundError
2424
from infrahub_sdk.ctl.generator import run as run_generator
25+
from infrahub_sdk.ctl.menu import app as menu_app
26+
from infrahub_sdk.ctl.object import app as object_app
2527
from infrahub_sdk.ctl.render import list_jinja2_transforms
2628
from infrahub_sdk.ctl.repository import app as repository_app
2729
from infrahub_sdk.ctl.repository import get_repository_config
2830
from infrahub_sdk.ctl.schema import app as schema_app
29-
from infrahub_sdk.ctl.schema import load_schemas_from_disk_and_exit
3031
from infrahub_sdk.ctl.transform import list_transforms
31-
from infrahub_sdk.ctl.utils import catch_exception, execute_graphql_query, parse_cli_vars
32+
from infrahub_sdk.ctl.utils import (
33+
catch_exception,
34+
execute_graphql_query,
35+
load_yamlfile_from_disk_and_exit,
36+
parse_cli_vars,
37+
)
3238
from infrahub_sdk.ctl.validate import app as validate_app
3339
from infrahub_sdk.exceptions import GraphQLError, InfrahubTransformNotFoundError
3440
from infrahub_sdk.jinja2 import identify_faulty_jinja_code
@@ -39,6 +45,7 @@
3945
)
4046
from infrahub_sdk.transforms import get_transform_class_instance
4147
from infrahub_sdk.utils import get_branch, write_to_file
48+
from infrahub_sdk.yaml import SchemaFile
4249

4350
from .exporter import dump
4451
from .importer import load
@@ -50,6 +57,9 @@
5057
app.add_typer(schema_app, name="schema")
5158
app.add_typer(validate_app, name="validate")
5259
app.add_typer(repository_app, name="repository")
60+
app.add_typer(menu_app, name="menu")
61+
app.add_typer(object_app, name="object", hidden=True)
62+
5363
app.command(name="dump")(dump)
5464
app.command(name="load")(load)
5565

@@ -338,7 +348,7 @@ def protocols( # noqa: PLR0915
338348
schema: dict[str, MainSchemaTypes] = {}
339349

340350
if schemas:
341-
schemas_data = load_schemas_from_disk_and_exit(schemas=schemas)
351+
schemas_data = load_yamlfile_from_disk_and_exit(paths=schemas, file_type=SchemaFile, console=console)
342352

343353
for data in schemas_data:
344354
data.load_content()

infrahub_sdk/ctl/menu.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import logging
2+
from pathlib import Path
3+
4+
import typer
5+
from rich.console import Console
6+
7+
from infrahub_sdk.async_typer import AsyncTyper
8+
from infrahub_sdk.ctl.client import initialize_client
9+
from infrahub_sdk.ctl.utils import catch_exception, init_logging
10+
from infrahub_sdk.spec.menu import MenuFile
11+
12+
from .parameters import CONFIG_PARAM
13+
from .utils import load_yamlfile_from_disk_and_exit
14+
15+
app = AsyncTyper()
16+
console = Console()
17+
18+
19+
@app.callback()
20+
def callback() -> None:
21+
"""
22+
Manage the menu in a remote Infrahub instance.
23+
"""
24+
25+
26+
@app.command()
27+
@catch_exception(console=console)
28+
async def load(
29+
menus: list[Path],
30+
debug: bool = False,
31+
branch: str = typer.Option("main", help="Branch on which to load the menu."),
32+
_: str = CONFIG_PARAM,
33+
) -> None:
34+
"""Load one or multiple menu files into Infrahub."""
35+
36+
init_logging(debug=debug)
37+
38+
logging.getLogger("infrahub_sdk").setLevel(logging.INFO)
39+
40+
files = load_yamlfile_from_disk_and_exit(paths=menus, file_type=MenuFile, console=console)
41+
client = await initialize_client()
42+
43+
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+
await file.spec.create_node(
49+
client=client,
50+
schema=schema,
51+
data=item,
52+
branch=branch,
53+
default_schema_kind=file.spec.kind,
54+
context={"list_index": idx},
55+
)

infrahub_sdk/ctl/object.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
from pathlib import Path
3+
4+
import typer
5+
from rich.console import Console
6+
7+
from infrahub_sdk.async_typer import AsyncTyper
8+
from infrahub_sdk.ctl.client import initialize_client
9+
from infrahub_sdk.ctl.utils import catch_exception, init_logging
10+
from infrahub_sdk.spec.object import ObjectFile
11+
12+
from .parameters import CONFIG_PARAM
13+
from .utils import load_yamlfile_from_disk_and_exit
14+
15+
app = AsyncTyper()
16+
console = Console()
17+
18+
19+
@app.callback()
20+
def callback() -> None:
21+
"""
22+
Manage objects in a remote Infrahub instance.
23+
"""
24+
25+
26+
@app.command()
27+
@catch_exception(console=console)
28+
async def load(
29+
paths: list[Path],
30+
debug: bool = False,
31+
branch: str = typer.Option("main", help="Branch on which to load the objects."),
32+
_: str = CONFIG_PARAM,
33+
) -> None:
34+
"""Load one or multiple objects files into Infrahub."""
35+
36+
init_logging(debug=debug)
37+
38+
logging.getLogger("infrahub_sdk").setLevel(logging.INFO)
39+
40+
files = load_yamlfile_from_disk_and_exit(paths=paths, file_type=ObjectFile, console=console)
41+
client = await initialize_client()
42+
43+
for file in files:
44+
file.validate_content()
45+
schema = await client.schema.get(kind=file.spec.kind, branch=branch)
46+
47+
for item in file.spec.data:
48+
await file.spec.create_node(client=client, schema=schema, data=item, branch=branch)

infrahub_sdk/ctl/schema.py

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
from infrahub_sdk import InfrahubClient
1212
from infrahub_sdk.async_typer import AsyncTyper
1313
from infrahub_sdk.ctl.client import initialize_client
14-
from infrahub_sdk.ctl.exceptions import FileNotValidError
1514
from infrahub_sdk.ctl.utils import catch_exception, init_logging
1615
from infrahub_sdk.queries import SCHEMA_HASH_SYNC_STATUS
17-
from infrahub_sdk.utils import find_files
1816
from infrahub_sdk.yaml import SchemaFile
1917

2018
from .parameters import CONFIG_PARAM
19+
from .utils import load_yamlfile_from_disk_and_exit
2120

2221
app = AsyncTyper()
2322
console = Console()
@@ -30,45 +29,6 @@ def callback() -> None:
3029
"""
3130

3231

33-
def load_schemas_from_disk(schemas: list[Path]) -> list[SchemaFile]:
34-
schemas_data: list[SchemaFile] = []
35-
for schema in schemas:
36-
if schema.is_file():
37-
schema_file = SchemaFile(location=schema)
38-
schema_file.load_content()
39-
schemas_data.append(schema_file)
40-
elif schema.is_dir():
41-
files = find_files(extension=["yaml", "yml", "json"], directory=schema)
42-
for item in files:
43-
schema_file = SchemaFile(location=item)
44-
schema_file.load_content()
45-
schemas_data.append(schema_file)
46-
else:
47-
raise FileNotValidError(name=schema, message=f"Schema path: {schema} does not exist!")
48-
49-
return schemas_data
50-
51-
52-
def load_schemas_from_disk_and_exit(schemas: list[Path]) -> list[SchemaFile]:
53-
has_error = False
54-
try:
55-
schemas_data = load_schemas_from_disk(schemas=schemas)
56-
except FileNotValidError as exc:
57-
console.print(f"[red]{exc.message}")
58-
raise typer.Exit(1) from exc
59-
60-
for schema_file in schemas_data:
61-
if schema_file.valid and schema_file.content:
62-
continue
63-
console.print(f"[red]{schema_file.error_message} ({schema_file.location})")
64-
has_error = True
65-
66-
if has_error:
67-
raise typer.Exit(1)
68-
69-
return schemas_data
70-
71-
7232
def validate_schema_content_and_exit(client: InfrahubClient, schemas: list[SchemaFile]) -> None:
7333
has_error: bool = False
7434
for schema_file in schemas:
@@ -153,7 +113,7 @@ async def load(
153113

154114
init_logging(debug=debug)
155115

156-
schemas_data = load_schemas_from_disk_and_exit(schemas=schemas)
116+
schemas_data = load_yamlfile_from_disk_and_exit(paths=schemas, file_type=SchemaFile, console=console)
157117
schema_definition = "schema" if len(schemas_data) == 1 else "schemas"
158118
client = await initialize_client()
159119
validate_schema_content_and_exit(client=client, schemas=schemas_data)
@@ -203,7 +163,7 @@ async def check(
203163

204164
init_logging(debug=debug)
205165

206-
schemas_data = load_schemas_from_disk_and_exit(schemas=schemas)
166+
schemas_data = load_yamlfile_from_disk_and_exit(paths=schemas, file_type=SchemaFile, console=console)
207167
client = await initialize_client()
208168
validate_schema_content_and_exit(client=client, schemas=schemas_data)
209169

infrahub_sdk/ctl/utils.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import traceback
44
from functools import wraps
55
from pathlib import Path
6-
from typing import Any, Callable, Optional, Union
6+
from typing import Any, Callable, Optional, TypeVar, Union
77

88
import pendulum
99
import typer
@@ -14,7 +14,7 @@
1414
from rich.logging import RichHandler
1515
from rich.markup import escape
1616

17-
from infrahub_sdk.ctl.exceptions import QueryNotFoundError
17+
from infrahub_sdk.ctl.exceptions import FileNotValidError, QueryNotFoundError
1818
from infrahub_sdk.exceptions import (
1919
AuthenticationError,
2020
Error,
@@ -25,9 +25,12 @@
2525
ServerNotResponsiveError,
2626
)
2727
from infrahub_sdk.schema import InfrahubRepositoryConfig
28+
from infrahub_sdk.yaml import YamlFile
2829

2930
from .client import initialize_client_sync
3031

32+
YamlFileVar = TypeVar("YamlFileVar", bound=YamlFile)
33+
3134

3235
def init_logging(debug: bool = False) -> None:
3336
logging.getLogger("infrahub_sdk").setLevel(logging.CRITICAL)
@@ -179,3 +182,25 @@ def get_fixtures_dir() -> Path:
179182
"""Get the directory which stores fixtures that are common to multiple unit/integration tests."""
180183
here = Path(__file__).resolve().parent
181184
return here.parent.parent / "tests" / "fixtures"
185+
186+
187+
def load_yamlfile_from_disk_and_exit(
188+
paths: list[Path], file_type: type[YamlFileVar], console: Console
189+
) -> list[YamlFileVar]:
190+
has_error = False
191+
try:
192+
data_files = file_type.load_from_disk(paths=paths)
193+
except FileNotValidError as exc:
194+
console.print(f"[red]{exc.message}")
195+
raise typer.Exit(1) from exc
196+
197+
for data_file in data_files:
198+
if data_file.valid and data_file.content:
199+
continue
200+
console.print(f"[red]{data_file.error_message} ({data_file.location})")
201+
has_error = True
202+
203+
if has_error:
204+
raise typer.Exit(1)
205+
206+
return data_files

infrahub_sdk/protocols.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class CoreMenu(CoreNode):
143143
namespace: String
144144
name: String
145145
label: StringOptional
146+
kind: StringOptional
146147
path: StringOptional
147148
description: StringOptional
148149
icon: StringOptional
@@ -607,6 +608,7 @@ class CoreMenuSync(CoreNodeSync):
607608
namespace: String
608609
name: String
609610
label: StringOptional
611+
kind: StringOptional
610612
path: StringOptional
611613
description: StringOptional
612614
icon: StringOptional

infrahub_sdk/spec/__init__.py

Whitespace-only changes.

infrahub_sdk/spec/menu.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Optional
2+
3+
from infrahub_sdk.yaml import InfrahubFile, InfrahubFileKind
4+
5+
from .object import InfrahubObjectFileData
6+
7+
8+
class InfrahubMenuFileData(InfrahubObjectFileData):
9+
kind: str = "CoreMenuItem"
10+
11+
@classmethod
12+
def enrich_node(cls, data: dict, context: dict) -> dict:
13+
if "kind" in data and "path" not in data:
14+
data["path"] = "/objects/" + data["kind"]
15+
16+
if "list_index" in context and "order_weight" not in data:
17+
data["order_weight"] = (context["list_index"] + 1) * 1000
18+
19+
return data
20+
21+
22+
class MenuFile(InfrahubFile):
23+
_spec: Optional[InfrahubMenuFileData] = None
24+
25+
@property
26+
def spec(self) -> InfrahubMenuFileData:
27+
if not self._spec:
28+
self._spec = InfrahubMenuFileData(**self.data.spec)
29+
return self._spec
30+
31+
def validate_content(self) -> None:
32+
super().validate_content()
33+
if self.kind != InfrahubFileKind.MENU:
34+
raise ValueError("File is not an Infrahub Menu file")
35+
self._spec = InfrahubMenuFileData(**self.data.spec)

0 commit comments

Comments
 (0)