Skip to content

bug: infrahubctl schema load - display_schema_load_errors crashes with ValueError when error path traverses an extensions: block #1007

Description

@iddocohen

Component

Python SDK, infrahubctl

Infrahub SDK version

1.20.0

Current Behavior

infrahubctl schema load calls infrahub_sdk.ctl.schema.display_schema_load_errors to render server-rejected schema validation errors. The function indexes loc_path[2] and loc_path[4] and casts both to int, assuming the loc tuple shape is ("body", "schemas", <schema_index>, "nodes"|"generics", <node_index>, ...).

When the rejected error originates inside an extensions: block (per docs.infrahub.app/topics/schema-extensions), the loc tuple has an extra segment:

("body", "schemas", <schema_index>, "extensions", "nodes"|"generics"|"relationships", <node_index>, <field>)

So loc_path[4] is a string like "generics" instead of the integer node index. int("generics") raises:

ValueError: invalid literal for int() with base 10: 'generics'

The traceback replaces the intended human-readable error message — the user never sees what the server actually rejected.

Expected Behavior

The server's rejection should be rendered in the same human-readable format as non-extensions errors, e.g.:

Unable to load the schema:
  Node: DcimGenericDevice (extensions/generics) | Extra inputs are not permitted (extra_forbidden) — field 'include_in_menu' is not part of the extension schema

Steps to Reproduce

Environment

  • Infrahub server: 1.9.2 (any 1.5+ exhibits the rejection that triggers the formatter)
  • infrahub-sdk[all] 1.20.0
  • Python 3.12

Steps

1. Have a running Infrahub instance with a schema that includes a generic that has include_in_menu: true (e.g., the DcimGenericDevice from opsmill/schema-library's base/dcim.yml). Any kind whose existing schema metadata you want to override works.

2. Author a schema-extension YAML that attempts to override a metadata field on an existing kind:

# repro_extension.yml
---
version: "1.0"
extensions:
  generics:
    - kind: DcimGenericDevice
      include_in_menu: false

3. Run infrahubctl schema load:

infrahubctl schema load repro_extension.yml

Actual output (1.20.0)

Schema loading.....
Unable to load the schema:
Traceback (most recent call last):
  File ".../site-packages/infrahub_sdk/ctl/schema.py", line 87, in load
    display_schema_load_errors(response=response.errors, schemas_data=schemas_data)
  File ".../site-packages/infrahub_sdk/ctl/schema.py", line 68, in display_schema_load_errors
    node_index = int(loc_path[4])
                 ^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'generics'

The user sees no human-readable explanation of why the schema was rejected.

Additional Information

Affected code

infrahub_sdk/ctl/schema.py lines 53–103 (in 1.20.0), function display_schema_load_errors:

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)
        return

    for error in response["detail"]:
        loc_path = error.get("loc", [])
        if not valid_error_path(loc_path=loc_path):
            continue

        # if the len of the path is equal to 6, the error is at the root of the object
        # if the len of the path is higher than 6, the error is in an attribute or a relationships
        schema_index = int(loc_path[2])
        node_index = int(loc_path[4])           # <-- crashes here for extensions paths
        ...

And the path validator at line 116, which lets the malformed path through:

def valid_error_path(loc_path: list[Any]) -> bool:
    return len(loc_path) >= 6 and loc_path[0] == "body" and loc_path[1] == "schemas"

valid_error_path does not check that loc_path[3] is "nodes" or "generics", so loc_path[3] == "extensions" slips through and the indexing assumption breaks.

Suggested fix

Branch on whether loc_path[3] == "extensions" and re-anchor the indexing:

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)
        return

    for error in response["detail"]:
        loc_path = error.get("loc", [])
        if not valid_error_path(loc_path=loc_path):
            continue

        schema_index = int(loc_path[2])

        # Distinguish top-level vs extensions-block error paths.
        # Top-level:  body/schemas/<si>/nodes|generics/<ni>/<field>
        # Extensions: body/schemas/<si>/extensions/nodes|generics|relationships/<ni>/<field>
        if loc_path[3] == "extensions":
            container = loc_path[4]                 # 'nodes' / 'generics' / 'relationships'
            node_index = int(loc_path[5])
            tail = loc_path[6:]
        else:
            container = loc_path[3]                 # 'nodes' / 'generics'
            node_index = int(loc_path[4])
            tail = loc_path[5:]
        ...

get_node and the downstream len(loc_path) == 6 / > 6 branches need corresponding adjustments (the absolute lengths shift by one for the extensions path).

valid_error_path should additionally accept loc_path[3] in ("nodes", "generics", "extensions") so unrelated server errors don't fall through this codepath.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions