Skip to content

bug: schema fails to load when a schemanode defines a single uniqueness constraint on a relationship, without an HFID  #4483

Description

@wvandeun

Component

API Server / GraphQL

Infrahub version

0.16.1

Current Behavior

When you load the following schema:

---
version: "1.0"
nodes:
  - name: One
    namespace: Node
    attributes:
      - name: name
        kind: Text
        optional: false
  - name: Two
    namespace: Node
    uniqueness_constraints:
      - ["one"]
    relationships:
      - name: one
        peer: NodeOne
        kind: Attribute
        optional: false
        cardinality: one

You get the following error message:

HTTP communication failure: Server error '500 Internal Server Error' for url 'http://localhost:8000/api/schema/load?branch=main'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 on POST to http://localhost:8000/api/schema/load?branch=main

The following exception can be found in the logs of the API server

{"event": "Exception in ASGI application
", "timestamp": "2024-09-27T22:21:10.715780Z", "logger": "uvicorn.error", "level": "error", "exception": "  + Exception Group Traceback (most recent call last):
  |   File \"/usr/local/lib/python3.12/site-packages/starlette/_utils.py\", line 77, in collapse_excgroups
  |     yield
  |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 186, in __call__
  |     async with anyio.create_task_group() as task_group:
  |                ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File \"/usr/local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py\", line 680, in __aexit__
  |     raise BaseExceptionGroup(
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File \"/usr/local/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py\", line 401, in run_asgi
    |     result = await app(  # type: ignore[func-returns-value]
    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py\", line 70, in __call__
    |     return await self.app(scope, receive, send)
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/fastapi/applications.py\", line 1054, in __call__
    |     await super().__call__(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/applications.py\", line 113, in __call__
    |     await self.middleware_stack(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py\", line 187, in __call__
    |     raise exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py\", line 165, in __call__
    |     await self.app(scope, receive, _send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/gzip.py\", line 20, in __call__
    |     await responder(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/gzip.py\", line 39, in __call__
    |     await self.app(scope, receive, self.send_with_gzip)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/cors.py\", line 85, in __call__
    |     await self.app(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette_exporter/middleware.py\", line 499, in __call__
    |     raise exception
    |   File \"/usr/local/lib/python3.12/site-packages/starlette_exporter/middleware.py\", line 405, in __call__
    |     await self.app(scope, receive, wrapped_send)
    |   File \"/usr/local/lib/python3.12/site-packages/asgi_correlation_id/middleware.py\", line 90, in __call__
    |     await self.app(scope, receive, handle_outgoing_request)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 185, in __call__
    |     with collapse_excgroups():
    |          ^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/contextlib.py\", line 158, in __exit__
    |     self.gen.throw(value)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/_utils.py\", line 83, in collapse_excgroups
    |     raise exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 187, in __call__
    |     response = await self.dispatch_func(request, call_next)
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/source/backend/infrahub/server.py\", line 160, in add_telemetry_span_exception
    |     return await call_next(request)
    |            ^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 163, in call_next
    |     raise app_exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 149, in coro
    |     await self.app(scope, receive_or_disconnect, send_no_error)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 185, in __call__
    |     with collapse_excgroups():
    |          ^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/contextlib.py\", line 158, in __exit__
    |     self.gen.throw(value)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/_utils.py\", line 83, in collapse_excgroups
    |     raise exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 187, in __call__
    |     response = await self.dispatch_func(request, call_next)
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/source/backend/infrahub/server.py\", line 149, in add_process_time_header
    |     response = await call_next(request)
    |                ^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 163, in call_next
    |     raise app_exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 149, in coro
    |     await self.app(scope, receive_or_disconnect, send_no_error)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 185, in __call__
    |     with collapse_excgroups():
    |          ^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/contextlib.py\", line 158, in __exit__
    |     self.gen.throw(value)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/_utils.py\", line 83, in collapse_excgroups
    |     raise exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 187, in __call__
    |     response = await self.dispatch_func(request, call_next)
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/source/backend/infrahub/server.py\", line 142, in logging_middleware
    |     response = await call_next(request)
    |                ^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 163, in call_next
    |     raise app_exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 149, in coro
    |     await self.app(scope, receive_or_disconnect, send_no_error)
    |   File \"/usr/local/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/__init__.py\", line 631, in __call__
    |     await self.app(scope, otel_receive, otel_send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/exceptions.py\", line 62, in __call__
    |     await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 62, in wrapped_app
    |     raise exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 51, in wrapped_app
    |     await app(scope, receive, sender)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 715, in __call__
    |     await self.middleware_stack(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 735, in app
    |     await route.handle(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 288, in handle
    |     await self.app(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 76, in app
    |     await wrap_app_handling_exceptions(app, request)(scope, receive, send)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 62, in wrapped_app
    |     raise exc
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 51, in wrapped_app
    |     await app(scope, receive, sender)
    |   File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 73, in app
    |     response = await f(request)
    |                ^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/fastapi/routing.py\", line 301, in app
    |     raw_response = await run_endpoint_function(
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/fastapi/routing.py\", line 212, in run_endpoint_function
    |     return await dependant.call(**values)
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/source/backend/infrahub/api/schema.py\", line 253, in load_schema
    |     candidate_schema, result = evaluate_candidate_schemas(branch_schema=branch_schema, schemas_to_evaluate=schemas)
    |                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/source/backend/infrahub/api/schema.py\", line 125, in evaluate_candidate_schemas
    |     candidate_schema.process()
    |   File \"/source/backend/infrahub/core/schema_manager.py\", line 473, in process
    |     self.process_post_validation()
    |   File \"/source/backend/infrahub/core/schema_manager.py\", line 509, in process_post_validation
    |     self.process_human_friendly_id()
    |   File \"/source/backend/infrahub/core/schema_manager.py\", line 1071, in process_human_friendly_id
    |     schema_path = node.parse_schema_path(path=constraint_path, schema=node)
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File \"/source/backend/infrahub/core/schema/basenode_schema.py\", line 427, in parse_schema_path
    |     schema_path.related_schema = schema.get(name=relationship_schema.peer, duplicate=True)
    |                                  ^^^^^^^^^^
    |   File \"/usr/local/lib/python3.12/site-packages/pydantic/main.py\", line 811, in __getattr__
    |     raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
    | AttributeError: 'NodeSchema' object has no attribute 'get'
    +------------------------------------

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File \"/usr/local/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py\", line 401, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py\", line 70, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/fastapi/applications.py\", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/applications.py\", line 113, in __call__
    await self.middleware_stack(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py\", line 187, in __call__
    raise exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py\", line 165, in __call__
    await self.app(scope, receive, _send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/gzip.py\", line 20, in __call__
    await responder(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/gzip.py\", line 39, in __call__
    await self.app(scope, receive, self.send_with_gzip)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/cors.py\", line 85, in __call__
    await self.app(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette_exporter/middleware.py\", line 499, in __call__
    raise exception
  File \"/usr/local/lib/python3.12/site-packages/starlette_exporter/middleware.py\", line 405, in __call__
    await self.app(scope, receive, wrapped_send)
  File \"/usr/local/lib/python3.12/site-packages/asgi_correlation_id/middleware.py\", line 90, in __call__
    await self.app(scope, receive, handle_outgoing_request)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 185, in __call__
    with collapse_excgroups():
         ^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/contextlib.py\", line 158, in __exit__
    self.gen.throw(value)
  File \"/usr/local/lib/python3.12/site-packages/starlette/_utils.py\", line 83, in collapse_excgroups
    raise exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 187, in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/source/backend/infrahub/server.py\", line 160, in add_telemetry_span_exception
    return await call_next(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 163, in call_next
    raise app_exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 149, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 185, in __call__
    with collapse_excgroups():
         ^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/contextlib.py\", line 158, in __exit__
    self.gen.throw(value)
  File \"/usr/local/lib/python3.12/site-packages/starlette/_utils.py\", line 83, in collapse_excgroups
    raise exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 187, in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/source/backend/infrahub/server.py\", line 149, in add_process_time_header
    response = await call_next(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 163, in call_next
    raise app_exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 149, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 185, in __call__
    with collapse_excgroups():
         ^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/contextlib.py\", line 158, in __exit__
    self.gen.throw(value)
  File \"/usr/local/lib/python3.12/site-packages/starlette/_utils.py\", line 83, in collapse_excgroups
    raise exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 187, in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/source/backend/infrahub/server.py\", line 142, in logging_middleware
    response = await call_next(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 163, in call_next
    raise app_exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/base.py\", line 149, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File \"/usr/local/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/__init__.py\", line 631, in __call__
    await self.app(scope, otel_receive, otel_send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/middleware/exceptions.py\", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 62, in wrapped_app
    raise exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 51, in wrapped_app
    await app(scope, receive, sender)
  File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 715, in __call__
    await self.middleware_stack(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 735, in app
    await route.handle(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 288, in handle
    await self.app(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 76, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 62, in wrapped_app
    raise exc
  File \"/usr/local/lib/python3.12/site-packages/starlette/_exception_handler.py\", line 51, in wrapped_app
    await app(scope, receive, sender)
  File \"/usr/local/lib/python3.12/site-packages/starlette/routing.py\", line 73, in app
    response = await f(request)
               ^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/fastapi/routing.py\", line 301, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/fastapi/routing.py\", line 212, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/source/backend/infrahub/api/schema.py\", line 253, in load_schema
    candidate_schema, result = evaluate_candidate_schemas(branch_schema=branch_schema, schemas_to_evaluate=schemas)
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/source/backend/infrahub/api/schema.py\", line 125, in evaluate_candidate_schemas
    candidate_schema.process()
  File \"/source/backend/infrahub/core/schema_manager.py\", line 473, in process
    self.process_post_validation()
  File \"/source/backend/infrahub/core/schema_manager.py\", line 509, in process_post_validation
    self.process_human_friendly_id()
  File \"/source/backend/infrahub/core/schema_manager.py\", line 1071, in process_human_friendly_id
    schema_path = node.parse_schema_path(path=constraint_path, schema=node)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \"/source/backend/infrahub/core/schema/basenode_schema.py\", line 427, in parse_schema_path
    schema_path.related_schema = schema.get(name=relationship_schema.peer, duplicate=True)
                                 ^^^^^^^^^^
  File \"/usr/local/lib/python3.12/site-packages/pydantic/main.py\", line 811, in __getattr__
    raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
AttributeError: 'NodeSchema' object has no attribute 'get'"}

If you modify the schema (add HFID) then it load fine

---
version: "1.0"
nodes:
  - name: One
    namespace: Node
    attributes:
      - name: name
        kind: Text
        optional: false
  - name: Two
    namespace: Node
    uniqueness_constraints:
      - ["one"]
    human_friendly_id:
      - one__name__value
    relationships:
      - name: one
        peer: NodeOne
        kind: Attribute
        optional: false
        cardinality: one

Expected Behavior

If this is considered to be an invalid schema, then we should return a proper error message to the user informing them of the exact issue and how it can be corrected. If this is considered a valid schema then we should load it without any error message.

Steps to Reproduce

  • start an Infrahub instance
  • try loading the following schema
```yaml
---
version: "1.0"
nodes:
  - name: One
    namespace: Node
    attributes:
      - name: name
        kind: Text
        optional: false
  - name: Two
    namespace: Node
    uniqueness_constraints:
      - ["one"]
    relationships:
      - name: one
        peer: NodeOne
        kind: Attribute
        optional: false
        cardinality: one

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    group/backendIssue related to the backend (API Server, Git Agent)type/bugSomething isn't working as expected

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions