Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions infrahub_sdk/schema/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,9 @@ class GenericSchemaAPI(BaseSchema, BaseSchemaAttrRelAPI):
"""A Generic can be either an Interface or a Union depending if there are some Attributes or Relationships defined."""

hash: str | None = None
hierarchical: bool | None = None
used_by: list[str] = Field(default_factory=list)
restricted_namespaces: list[str] | None = None
Comment on lines +292 to +294
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure around lines 283-294 in infrahub_sdk/schema/main.py
head -300 infrahub_sdk/schema/main.py | tail -50

Repository: opsmill/infrahub-sdk-python

Length of output: 1709


🏁 Script executed:

# Also check the entire GenericSchema and GenericSchemaAPI definitions with more context
sed -n '280,310p' infrahub_sdk/schema/main.py

Repository: opsmill/infrahub-sdk-python

Length of output: 1092


🏁 Script executed:

# Find the SchemaRoot definition
rg -n "class SchemaRoot" --type=py -A 20

Repository: opsmill/infrahub-sdk-python

Length of output: 2325


🏁 Script executed:

# Search for valid_generic_schema.json and how it's used
rg -n "valid_generic_schema" --type=py -B 2 -A 2

Repository: opsmill/infrahub-sdk-python

Length of output: 715


🏁 Script executed:

# Check the fixture file to see if it contains the fields in question
cat tests/unit/fixtures/models/valid_generic_schema.json

Repository: opsmill/infrahub-sdk-python

Length of output: 155


🏁 Script executed:

# Check the BaseModel/ConfigDict configuration for GenericSchema
sed -n '250,290p' infrahub_sdk/schema/main.py | grep -A 10 "class BaseSchema"

Repository: opsmill/infrahub-sdk-python

Length of output: 398


🏁 Script executed:

# Also check BaseSchemaAttrRel to see if it has any of these fields
rg -n "class BaseSchemaAttrRel" --type=py -A 30

Repository: opsmill/infrahub-sdk-python

Length of output: 2611


🏁 Script executed:

# Find the fixture file
find . -name "valid_generic_schema.json" -type f

Repository: opsmill/infrahub-sdk-python

Length of output: 120


🏁 Script executed:

# Check if there are any fixture files in the test directory
find tests -name "*generic_schema*" -type f

Repository: opsmill/infrahub-sdk-python

Length of output: 118


🏁 Script executed:

# Search for how ConfigDict is configured in the schema classes, especially looking for extra field behavior
rg -n "ConfigDict" --type=py infrahub_sdk/schema/main.py

Repository: opsmill/infrahub-sdk-python

Length of output: 625


🏁 Script executed:

# Check if convert_api() is called anywhere and how the returned GenericSchemaAPI is used
rg -n "convert_api" --type=py -B 2 -A 2

Repository: opsmill/infrahub-sdk-python

Length of output: 4473


🏁 Script executed:

# Read the fixture file to see if it contains restricted_namespaces
cat tests/fixtures/models/valid_generic_schema.json

Repository: opsmill/infrahub-sdk-python

Length of output: 737


🏁 Script executed:

# Check if there's a comment or test related to the fields not being in GenericSchema
rg -n "hierarchy|hierarchical|restricted_namespaces" --type=py tests/unit/sdk/test_hierarchical_nodes.py -B 2 -A 2

Repository: opsmill/infrahub-sdk-python

Length of output: 15108


🏁 Script executed:

# Check the full ConfigDict settings for GenericSchema and GenericSchemaAPI to see if there's any special config
sed -n '283,300p' infrahub_sdk/schema/main.py

Repository: opsmill/infrahub-sdk-python

Length of output: 722


🏁 Script executed:

# Check how the fixture is actually used in tests
rg -n "valid_generic_schema" --type=py -B 5 -A 10

Repository: opsmill/infrahub-sdk-python

Length of output: 1540


🏁 Script executed:

# Check if SchemaRoot parsing with the fixture would raise a validation error or silently drop fields
# Look at the test to see what happens
sed -n '133,160p' tests/unit/ctl/test_schema_app.py

Repository: opsmill/infrahub-sdk-python

Length of output: 1047


🏁 Script executed:

# Check if there are any schema validation tests that parse the fixture through SchemaRoot
rg -n "SchemaRoot.*valid_generic_schema" --type=py -B 5 -A 5

Repository: opsmill/infrahub-sdk-python

Length of output: 53


🏁 Script executed:

# Search for how restricted_namespaces is used in the codebase
rg -n "restricted_namespaces" --type=py -B 2 -A 2

Repository: opsmill/infrahub-sdk-python

Length of output: 2111


🏁 Script executed:

# Check Pydantic configuration in BaseModel and BaseSchema to see if extra fields raise errors
rg -n "model_config|extra|forbid" --type=py infrahub_sdk/schema/main.py | head -20

Repository: opsmill/infrahub-sdk-python

Length of output: 573


🏁 Script executed:

# Check if the CLI actually parses the JSON through SchemaRoot or sends it directly
rg -n "load.*schema" --type=py infrahub_sdk/cli -B 5 -A 10 | head -100

Repository: opsmill/infrahub-sdk-python

Length of output: 127


🏁 Script executed:

# Look at the complete test to see what happens with the loaded schema
sed -n '133,180p' tests/unit/ctl/test_schema_app.py

Repository: opsmill/infrahub-sdk-python

Length of output: 1713


🏁 Script executed:

# Check if there's any code that creates GenericSchema from JSON data that includes restricted_namespaces
rg -n "GenericSchema\(" --type=py -B 3 -A 3

Repository: opsmill/infrahub-sdk-python

Length of output: 1013


🏁 Script executed:

# Check if there's a test that validates round-trip conversion of GenericSchema with restricted_namespaces
rg -n "test.*generic.*schema|test.*restricted_namespaces" --type=py -i

Repository: opsmill/infrahub-sdk-python

Length of output: 565


🏁 Script executed:

# Look at the schema initialization/parsing logic to understand the flow better
rg -n "SchemaRoot\(" --type=py -B 3 -A 3

Repository: opsmill/infrahub-sdk-python

Length of output: 2766


🏁 Script executed:

# Check if hierarchical is ever used as an input field or only as output
rg -n "hierarchical" --type=py -B 2 -A 2

Repository: opsmill/infrahub-sdk-python

Length of output: 48353


🏁 Script executed:

# Check the Pydantic configuration more thoroughly - does it forbid extra fields?
sed -n '96,105p' infrahub_sdk/schema/main.py

Repository: opsmill/infrahub-sdk-python

Length of output: 358


🏁 Script executed:

# Let's trace through what happens when SchemaRoot parses the fixture with restricted_namespaces
python3 <<'EOF'
import sys
sys.path.insert(0, '.')

from infrahub_sdk.schema.main import SchemaRoot, GenericSchema
import json

# Load the fixture
with open('tests/fixtures/models/valid_generic_schema.json') as f:
    fixture_data = json.load(f)

print("Fixture data:")
print(json.dumps(fixture_data, indent=2))

# Try to parse it through SchemaRoot
try:
    schema_root = SchemaRoot(**fixture_data)
    print("\nSchemaRoot parsing succeeded")
    print(f"Number of generics: {len(schema_root.generics)}")
    if schema_root.generics:
        generic = schema_root.generics[0]
        print(f"Generic name: {generic.name}")
        print(f"Generic model dump: {generic.model_dump()}")
        print(f"Does generic have restricted_namespaces? {hasattr(generic, 'restricted_namespaces')}")
except Exception as e:
    print(f"Error parsing: {e}")
EOF

Repository: opsmill/infrahub-sdk-python

Length of output: 411


🏁 Script executed:

pip install ujson pydantic -q && python3 <<'EOF'
import sys
sys.path.insert(0, '.')

from infrahub_sdk.schema.main import SchemaRoot, GenericSchema
import json

# Load the fixture
with open('tests/fixtures/models/valid_generic_schema.json') as f:
    fixture_data = json.load(f)

print("Fixture data:")
print(json.dumps(fixture_data, indent=2))

# Try to parse it through SchemaRoot
try:
    schema_root = SchemaRoot(**fixture_data)
    print("\nSchemaRoot parsing succeeded")
    print(f"Number of generics: {len(schema_root.generics)}")
    if schema_root.generics:
        generic = schema_root.generics[0]
        print(f"Generic name: {generic.name}")
        print(f"Generic model dump: {generic.model_dump()}")
        print(f"Does generic have restricted_namespaces attr? {hasattr(generic, 'restricted_namespaces')}")
        
        # Try convert_api
        print("\nTrying convert_api()...")
        api = generic.convert_api()
        print(f"GenericSchemaAPI model dump: {api.model_dump()}")
        print(f"Does API have restricted_namespaces? {api.restricted_namespaces}")
except Exception as e:
    print(f"Error parsing: {type(e).__name__}: {e}")
EOF

Repository: opsmill/infrahub-sdk-python

Length of output: 628


🏁 Script executed:

pip install ujson pydantic graphql-core -q && python3 <<'EOF'
import sys
sys.path.insert(0, '.')

from infrahub_sdk.schema.main import SchemaRoot, GenericSchema, GenericSchemaAPI
import json

# Load the fixture
with open('tests/fixtures/models/valid_generic_schema.json') as f:
    fixture_data = json.load(f)

print("=== Fixture data ===")
print(json.dumps(fixture_data['generics'][0], indent=2))

# Try to parse it through SchemaRoot
print("\n=== Parsing through SchemaRoot ===")
try:
    schema_root = SchemaRoot(**fixture_data)
    print(f"✓ SchemaRoot parsing succeeded")
    print(f"  Number of generics: {len(schema_root.generics)}")
    if schema_root.generics:
        generic = schema_root.generics[0]
        print(f"  Generic name: {generic.name}")
        dump = generic.model_dump()
        print(f"  Keys in model_dump(): {list(dump.keys())}")
        print(f"  Has 'restricted_namespaces' key? {'restricted_namespaces' in dump}")
        
        # Try convert_api
        print("\n=== Calling convert_api() ===")
        api = generic.convert_api()
        api_dump = api.model_dump()
        print(f"  GenericSchemaAPI keys: {list(api_dump.keys())}")
        print(f"  restricted_namespaces value: {api.restricted_namespaces}")
        print(f"  hierarchical value: {api.hierarchical}")
except Exception as e:
    import traceback
    print(f"✗ Error: {type(e).__name__}: {e}")
    traceback.print_exc()
EOF

Repository: opsmill/infrahub-sdk-python

Length of output: 767


🏁 Script executed:

pip install pydantic -q && python3 <<'EOF'
import sys
sys.path.insert(0, '.')

# Just import the schema module directly
from infrahub_sdk.schema.main import SchemaRoot, GenericSchema, GenericSchemaAPI
import json

# Load the fixture
with open('tests/fixtures/models/valid_generic_schema.json') as f:
    fixture_data = json.load(f)

print("=== Fixture data ===")
print(json.dumps(fixture_data['generics'][0], indent=2))

# Try to parse it through SchemaRoot
print("\n=== Parsing through SchemaRoot ===")
try:
    schema_root = SchemaRoot(**fixture_data)
    print(f"✓ SchemaRoot parsing succeeded")
    print(f"  Number of generics: {len(schema_root.generics)}")
    if schema_root.generics:
        generic = schema_root.generics[0]
        print(f"  Generic name: {generic.name}")
        dump = generic.model_dump()
        print(f"  Keys in model_dump(): {sorted(list(dump.keys()))}")
        print(f"  Has 'restricted_namespaces' key? {'restricted_namespaces' in dump}")
        
        # Try convert_api
        print("\n=== Calling convert_api() ===")
        api = generic.convert_api()
        api_dump = api.model_dump()
        print(f"  GenericSchemaAPI keys: {sorted(list(api_dump.keys()))}")
        print(f"  restricted_namespaces value: {api.restricted_namespaces}")
        print(f"  hierarchical value: {api.hierarchical}")
except Exception as e:
    import traceback
    print(f"✗ Error: {type(e).__name__}: {e}")
    traceback.print_exc()
EOF

Repository: opsmill/infrahub-sdk-python

Length of output: 767


🏁 Script executed:

# Check Pydantic's default behavior for extra fields in v2
sed -n '1,20p' infrahub_sdk/schema/main.py

Repository: opsmill/infrahub-sdk-python

Length of output: 520


🏁 Script executed:

# Check if there's any validation or extra field handling
rg -n "ConfigDict" infrahub_sdk/schema/main.py | head -20

Repository: opsmill/infrahub-sdk-python

Length of output: 625


🏁 Script executed:

# Check if fields are validated elsewhere
rg -n "class GenericSchema" --type=py -A 10

Repository: opsmill/infrahub-sdk-python

Length of output: 1175


🏁 Script executed:

# Check all parent classes to be sure neither BaseSchema nor BaseSchemaAttrRel define these fields
rg -n "class BaseSchema\|class BaseSchemaAttrRel" --type=py -A 20

Repository: opsmill/infrahub-sdk-python

Length of output: 53


🏁 Script executed:

# Get BaseSchema and BaseSchemaAttrRel definitions
sed -n '260,290p' infrahub_sdk/schema/main.py

Repository: opsmill/infrahub-sdk-python

Length of output: 1080


🏁 Script executed:

# Get BaseSchemaAttrRel definition
sed -n '154,162p' infrahub_sdk/schema/main.py

Repository: opsmill/infrahub-sdk-python

Length of output: 438


🏁 Script executed:

# Check if there's any test that tries to create GenericSchema with restricted_namespaces
rg -n "GenericSchema.*restricted" --type=py

Repository: opsmill/infrahub-sdk-python

Length of output: 53


🏁 Script executed:

# Check if the issue is actually about API response vs input - search for where GenericSchemaAPI is used
rg -n "GenericSchemaAPI" --type=py | head -20

Repository: opsmill/infrahub-sdk-python

Length of output: 2049


🏁 Script executed:

# Check how the CLI actually handles schema loading to understand if it goes through GenericSchema
sed -n '375,390p' infrahub_sdk/ctl/cli_commands.py

Repository: opsmill/infrahub-sdk-python

Length of output: 677


🏁 Script executed:

# Check if there's a conversion step that uses GenericSchema.convert_api()
rg -n "\.convert_api()" --type=py -B 3 -A 3 | grep -A 3 -B 3 "Generic"

Repository: opsmill/infrahub-sdk-python

Length of output: 462


🏁 Script executed:

# Check the test to see if the raw JSON or parsed objects are sent to the API
sed -n '150,175p' tests/unit/ctl/test_schema_app.py

Repository: opsmill/infrahub-sdk-python

Length of output: 967


Add hierarchical and restricted_namespaces fields to GenericSchema.

GenericSchema lacks both fields that exist in GenericSchemaAPI, causing them to be silently dropped when schemas are parsed through SchemaRoot. The fixture valid_generic_schema.json includes restricted_namespaces: ["Dog"], which should be preserved. Add:

hierarchical: bool | None = None
restricted_namespaces: list[str] | None = None

to GenericSchema to match its API counterpart and ensure consistent round-tripping.

🤖 Prompt for AI Agents
In `@infrahub_sdk/schema/main.py` around lines 292 - 294, GenericSchema is missing
the hierarchical and restricted_namespaces attributes present in
GenericSchemaAPI, causing those values (e.g., restricted_namespaces from
valid_generic_schema.json) to be dropped when parsed via SchemaRoot; add the two
fields to GenericSchema by declaring hierarchical: bool | None = None and
restricted_namespaces: list[str] | None = None on the GenericSchema
dataclass/model so it matches GenericSchemaAPI and preserves round-trip parsing
of those properties.



class BaseNodeSchema(BaseSchema):
Expand Down
30 changes: 30 additions & 0 deletions tests/fixtures/models/valid_generic_schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"version": "1.0",
"generics": [
{
"name": "Animal",
"namespace": "Testing",
"attributes": [
{
"name": "name",
"kind": "Text"
}
],
"restricted_namespaces": ["Dog"]
}
],
"nodes": [
{
"name": "Dog",
"namespace": "Dog",
"inherit_from": ["TestingAnimal"],
"attributes": [
{
"name": "breed",
"kind": "Text",
"optional": true
}
]
}
]
}
45 changes: 45 additions & 0 deletions tests/unit/ctl/test_schema_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,48 @@ def test_schema_load_notvalid_namespace(httpx_mock: HTTPXMock) -> None:
fixture_file.read_text(encoding="utf-8"),
)
assert content_json == {"schemas": [fixture_file_content]}


def test_load_valid_generic_schema(httpx_mock: HTTPXMock) -> None:
"""A test which ensures that a generic schema is correctly loaded when loaded from infrahubctl command"""

# Arrange
fixture_file = get_fixtures_dir() / "models" / "valid_generic_schema.json"

httpx_mock.add_response(
method="POST",
url="http://mock/api/schema/load?branch=main",
status_code=200,
json={
"hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"previous_hash": "d3f7f4e7161f0ae6538a01d5a42dc661",
"diff": {
"added": {
"TestingAnimal": {"added": {}, "changed": {}, "removed": {}},
"DogDog": {"added": {}, "changed": {}, "removed": {}},
},
"changed": {},
"removed": {},
},
"schema_updated": True,
},
)

# Act
result = runner.invoke(app=app, args=["load", str(fixture_file)])

# Assert
assert result.exit_code == 0
assert f"schema '{fixture_file}' loaded successfully" in remove_ansi_color(result.stdout.replace("\n", ""))

content = httpx_mock.get_requests()[0].content.decode("utf8")
content_json = yaml.safe_load(content)
fixture_file_content = yaml.safe_load(
fixture_file.read_text(encoding="utf-8"),
)
assert content_json == {"schemas": [fixture_file_content]}

# Verify restricted_namespaces is present in the payload sent to the API
sent_generics = content_json["schemas"][0]["generics"]
assert len(sent_generics) == 1
assert sent_generics[0]["restricted_namespaces"] == ["Dog"]
54 changes: 54 additions & 0 deletions tests/unit/sdk/test_schema.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inspect
import re
from io import StringIO
from unittest import mock
from unittest.mock import MagicMock
Expand Down Expand Up @@ -482,3 +483,56 @@ def test_schema_base__get_schema_name__returns_correct_schema_name_for_protocols
assert InfrahubSchemaBase._get_schema_name(schema=BuiltinIPAddressSync) == "BuiltinIPAddress"
assert InfrahubSchemaBase._get_schema_name(schema=BuiltinIPAddress) == "BuiltinIPAddress"
assert InfrahubSchemaBase._get_schema_name(schema="BuiltinIPAddress") == "BuiltinIPAddress"


async def test_schema_load_rejected_when_node_namespace_violates_generic_restricted_namespaces(
client: InfrahubClient, httpx_mock: HTTPXMock
) -> None:
"""Validate that loading a schema with a node whose namespace violates the generic's restricted_namespaces
is rejected. One test is already testing the API internal behavior
tests.integration.schema_lifecycle.test_restricted_namespaces_validation.
TestRestrictedNamespacesValidation.test_change_restriction_should_fail"""
# Arrange
schema_payload = {
"version": "1.0",
"generics": [
{
"name": "Animal",
"namespace": "Testing",
"attributes": [{"name": "name", "kind": "Text"}],
"restricted_namespaces": ["Dog"],
}
],
"nodes": [
{
"name": "Cat",
"namespace": "Cat",
"inherit_from": ["TestingAnimal"],
"attributes": [{"name": "breed", "kind": "Text", "optional": True}],
}
],
}

error_message = (
"Generic node 'TestingAnimal' has restricted namespaces: ['Dog']. "
"The node 'CatCat' does not comply with this restriction as its namespace is 'Cat'."
)

httpx_mock.add_response(
method="POST",
url="http://mock/api/schema/load?branch=main",
status_code=422,
json={
"data": None,
"errors": [{"message": error_message, "extensions": {"code": 422}}],
},
)

# Act
response = await client.schema.load(schemas=[schema_payload])

# Assert
assert response.errors
error_message = response.errors["errors"][0]["message"]
assert re.search(r"(?s)restricted namespaces(?=.*Dog)(?=.*Cat)", error_message)
assert not response.schema_updated