-
Notifications
You must be signed in to change notification settings - Fork 6
Merge 'develop' into 'stable' for SDK 1.14 & Infrahub 1.4 #509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
91a828a
544b49a
4717ffc
8e357b9
f82744b
199532f
0cde3c6
3269a41
8273796
90802de
8f1ab44
7905287
a93d013
545956a
988b1d9
076d477
8310a3e
309e530
b24a04d
c1a1fb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added `infrahubctl repository init` command to allow the initialization of an Infrahub repository using [infrahub-template](https://github.com/opsmill/infrahub-template). |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,12 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import typer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import yaml | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from copier import run_copy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid hard dependency on optional package "copier" (move import inside the command).
Apply this diff: -from copier import run_copyAnd see the follow-up diff in the init() block that adds a lazy import.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import ValidationError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from rich.console import Console | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from rich.table import Table | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -165,3 +167,52 @@ async def list( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.print(table) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.command() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def init( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directory: Path = typer.Argument(help="Directory path for the new project."), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| template: str = typer.Option( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default="https://github.com/opsmill/infrahub-template.git", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| help="Template to use for the new repository. Can be a local path or a git repository URL.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: Optional[Path] = typer.Option(default=None, help="Path to YAML file containing answers to CLI prompt."), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vcs_ref: Optional[str] = typer.Option( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default="HEAD", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| help="VCS reference to use for the template. Defaults to HEAD.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trust: Optional[bool] = typer.Option( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default=False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| help="Trust the template repository. If set, the template will be cloned without verification.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _: str = CONFIG_PARAM, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Initialize a new Infrahub repository.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config_data = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if data: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with Path.open(data, encoding="utf-8") as file: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config_data = yaml.safe_load(file) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typer.echo(f"Loaded config: {config_data}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typer.echo(f"Error loading YAML file: {exc}", err=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise typer.Exit(code=1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+192
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YAML handling: avoid leaking secrets; validate structure; use idiomatic Path.open.
Apply this diff: - config_data = None
- if data:
- try:
- with Path.open(data, encoding="utf-8") as file:
- config_data = yaml.safe_load(file)
- typer.echo(f"Loaded config: {config_data}")
- except Exception as exc:
- typer.echo(f"Error loading YAML file: {exc}", err=True)
- raise typer.Exit(code=1)
+ config_data = None
+ if data:
+ try:
+ with data.open("r", encoding="utf-8") as file:
+ config_data = yaml.safe_load(file) or {}
+ if not isinstance(config_data, dict):
+ raise typer.BadParameter("Expected a mapping in the YAML answers file (key: value).")
+ except (FileNotFoundError, yaml.YAMLError, typer.BadParameter) as exc:
+ typer.echo(f"Invalid YAML file: {exc}", err=True)
+ raise typer.Exit(code=1)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Allow template to be a local path or a URL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| template_source = template or "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if template and Path(template).exists(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| template_source = str(Path(template).resolve()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await asyncio.to_thread( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_copy, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| template_source, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| str(directory), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data=config_data, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vcs_ref=vcs_ref, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unsafe=trust, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+208
to
+216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Lazy import copier and handle copier-specific errors; keep the event loop responsive. Perform a local import with a clear guidance if Apply this diff: - try:
- await asyncio.to_thread(
- run_copy,
- template_source,
- str(directory),
- data=config_data,
- vcs_ref=vcs_ref,
- unsafe=trust,
- )
- except Exception as e:
- typer.echo(f"Error running copier: {e}", err=True)
- raise typer.Exit(code=1)
+ # Lazy import to keep 'copier' optional for users not using 'repository init'
+ try:
+ from copier import run_copy # type: ignore
+ try:
+ from copier.errors import UserMessageError as CopierUserMessageError # type: ignore
+ except Exception: # older copier
+ CopierUserMessageError = Exception # type: ignore
+ except Exception:
+ typer.echo(
+ "The 'copier' package is required for 'infrahubctl repository init'. "
+ "Install with: pip install 'infrahub-sdk[ctl]' or pip install copier",
+ err=True,
+ )
+ raise typer.Exit(code=1)
+
+ try:
+ await asyncio.to_thread(
+ run_copy,
+ template_source,
+ str(directory),
+ data=config_data,
+ vcs_ref=vcs_ref,
+ unsafe=trust,
+ )
+ except CopierUserMessageError as exc:
+ typer.echo(f"Template error: {exc}", err=True)
+ raise typer.Exit(code=1)
+ except Exception as exc:
+ typer.echo(f"Error running copier: {exc}", err=True)
+ raise typer.Exit(code=1)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typer.echo(f"Error running copier: {e}", err=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise typer.Exit(code=1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,7 +36,7 @@ def validate_schema_content_and_exit(client: InfrahubClient, schemas: list[Schem | |||||||||||||||||||||||||
| has_error: bool = False | ||||||||||||||||||||||||||
| for schema_file in schemas: | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| client.schema.validate(data=schema_file.content) | ||||||||||||||||||||||||||
| client.schema.validate(data=schema_file.payload) | ||||||||||||||||||||||||||
| except ValidationError as exc: | ||||||||||||||||||||||||||
| console.print(f"[red]Schema not valid, found '{len(exc.errors())}' error(s) in {schema_file.location}") | ||||||||||||||||||||||||||
| has_error = True | ||||||||||||||||||||||||||
|
|
@@ -48,7 +48,7 @@ def validate_schema_content_and_exit(client: InfrahubClient, schemas: list[Schem | |||||||||||||||||||||||||
| raise typer.Exit(1) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def display_schema_load_errors(response: dict[str, Any], schemas_data: list[dict]) -> None: | ||||||||||||||||||||||||||
| def display_schema_load_errors(response: dict[str, Any], schemas_data: list[SchemaFile]) -> None: | ||||||||||||||||||||||||||
| console.print("[red]Unable to load the schema:") | ||||||||||||||||||||||||||
| if "detail" not in response: | ||||||||||||||||||||||||||
| handle_non_detail_errors(response=response) | ||||||||||||||||||||||||||
|
|
@@ -87,7 +87,7 @@ def handle_non_detail_errors(response: dict[str, Any]) -> None: | |||||||||||||||||||||||||
| if "error" in response: | ||||||||||||||||||||||||||
| console.print(f" {response.get('error')}") | ||||||||||||||||||||||||||
| elif "errors" in response: | ||||||||||||||||||||||||||
| for error in response.get("errors"): | ||||||||||||||||||||||||||
| for error in response["errors"]: | ||||||||||||||||||||||||||
| console.print(f" {error.get('message')}") | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| console.print(f" '{response}'") | ||||||||||||||||||||||||||
|
|
@@ -97,9 +97,9 @@ def valid_error_path(loc_path: list[Any]) -> bool: | |||||||||||||||||||||||||
| return len(loc_path) >= 6 and loc_path[0] == "body" and loc_path[1] == "schemas" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def get_node(schemas_data: list[dict], schema_index: int, node_index: int) -> dict | None: | ||||||||||||||||||||||||||
| if schema_index < len(schemas_data) and node_index < len(schemas_data[schema_index].content["nodes"]): | ||||||||||||||||||||||||||
| return schemas_data[schema_index].content["nodes"][node_index] | ||||||||||||||||||||||||||
| def get_node(schemas_data: list[SchemaFile], schema_index: int, node_index: int) -> dict | None: | ||||||||||||||||||||||||||
| if schema_index < len(schemas_data) and node_index < len(schemas_data[schema_index].payload["nodes"]): | ||||||||||||||||||||||||||
| return schemas_data[schema_index].payload["nodes"][node_index] | ||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||
|
Comment on lines
+100
to
103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard against missing nodes key and out-of-range indices. Using Apply this diff: -def get_node(schemas_data: list[SchemaFile], schema_index: int, node_index: int) -> dict | None:
- if schema_index < len(schemas_data) and node_index < len(schemas_data[schema_index].payload["nodes"]):
- return schemas_data[schema_index].payload["nodes"][node_index]
- return None
+def get_node(schemas_data: list[SchemaFile], schema_index: int, node_index: int) -> dict | None:
+ if schema_index >= len(schemas_data):
+ return None
+ payload = schemas_data[schema_index].payload or {}
+ nodes = payload.get("nodes", [])
+ if isinstance(nodes, list) and 0 <= node_index < len(nodes):
+ return nodes[node_index]
+ return None📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -122,7 +122,7 @@ async def load( | |||||||||||||||||||||||||
| validate_schema_content_and_exit(client=client, schemas=schemas_data) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| start_time = time.time() | ||||||||||||||||||||||||||
| response = await client.schema.load(schemas=[item.content for item in schemas_data], branch=branch) | ||||||||||||||||||||||||||
| response = await client.schema.load(schemas=[item.payload for item in schemas_data], branch=branch) | ||||||||||||||||||||||||||
| loading_time = time.time() - start_time | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if response.errors: | ||||||||||||||||||||||||||
|
|
@@ -170,10 +170,10 @@ async def check( | |||||||||||||||||||||||||
| client = initialize_client() | ||||||||||||||||||||||||||
| validate_schema_content_and_exit(client=client, schemas=schemas_data) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| success, response = await client.schema.check(schemas=[item.content for item in schemas_data], branch=branch) | ||||||||||||||||||||||||||
| success, response = await client.schema.check(schemas=[item.payload for item in schemas_data], branch=branch) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if not success: | ||||||||||||||||||||||||||
| display_schema_load_errors(response=response, schemas_data=schemas_data) | ||||||||||||||||||||||||||
| display_schema_load_errors(response=response or {}, schemas_data=schemas_data) | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| for schema_file in schemas_data: | ||||||||||||||||||||||||||
| console.print(f"[green] schema '{schema_file.location}' is Valid!") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix grammar/style in the new
repository initsection; slight wording polish.Several bullets read as fragments and trip style checks. Tightening phrasing improves clarity.
Apply the following diff within this block:
Arguments:
-*
DIRECTORY: Directory path for the new project. [required]+*
DIRECTORY: Directory path for the new project. [required]Options:
-*
--template TEXT: Template to use for the new repository. Can be a local path or a git repository URL. [default: https://github.com/opsmill/infrahub-template.git]-*
--data PATH: Path to YAML file containing answers to CLI prompt.+*
--template TEXT: Template to use for the new repository; can be a local path or a Git repository URL. [default: https://github.com/opsmill/infrahub-template.git]+*
--data PATH: Path to a YAML file containing answers to the CLI prompts.--vcs-ref TEXT: VCS reference to use for the template. Defaults to HEAD. [default: HEAD]-*
--trust / --no-trust: Trust the template repository. If set, the template will be cloned without verification. [default: no-trust]+*
--trust / --no-trust: Trust the template repository. If enabled, the template will be used without additional verification. [default: no-trust]--config-file TEXT: [env var: INFRAHUBCTL_CONFIG; default: infrahubctl.toml]--help: Show this message and exit.In docs/docs/infrahubctl/infrahubctl-repository.mdx around lines 51 to 73,
polish wording and fix grammar in the
infrahubctl repository initsection:change the
--templatedescription to "Template to use for the new repository;can be a local path or a Git repository URL.", change
--datato "Path to aYAML file containing answers to the CLI prompts.", and update
--trust / --no-trustto "Trust the template repository. If enabled, the template will beused without additional verification." Keep the rest of the block intact and
optionally add a short "Validation" subsection listing expected files like
.copier-answers.ymlandpyproject.tomlafter a successful run.