Skip to content

feat(spec): reference generic peers by HFID in object files - #1432

Open
minitriga wants to merge 11 commits into
stablefrom
atg/strict-waves-stop
Open

minitriga wants to merge 11 commits into
stablefrom
atg/strict-waves-stop

Conversation

@minitriga

@minitriga minitriga commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Why

Some relationships point to a generic rather than to one node kind. An example is a sub-interface's parent_interface, which can be a physical interface or a port-channel. When the generic has no human-friendly ID (HFID) but the node kinds that inherit from it do, an object file cannot reference an existing peer by its HFID. The SDK sends the HFID without a kind, and the server looks it up on the generic and fails with "schema '' does not have a HFID defined".

The workarounds change the schema only to suit the loader:

  • Point the relationship at one node kind. Port-channels can then no longer be parents or tunnel sources.
  • Add an HFID to the generic. This forces the HFID's attributes and relationships onto the generic.

Goal: object file authors can reference these peers without changing the schema.

Non-goals: no change to the Infrahub server, and no synchronous version of the object file API, which is asynchronous only today.

What changed

What users will notice:

  • A reference can name the node kind and the HFID:

    parent_interface:
      kind: InterfacesPhysicalInterface
      hfid: [router1, Gi0/0/3]

    This works for cardinality-one and cardinality-many relationships. The SDK passes kind with hfid to the server, which uses RelatedNodeInput.kind to look up HFIDs on generic relationships.

  • When the generic has no HFID, a reference can be the HFID alone, as in parent_interface: [router1, Gi0/0/3]. The loader searches the kinds in the generic's used_by whose HFID has the same number of values. It links the node when exactly one matches. When none or more than one matches, the load stops with an error that names the kinds and asks for kind:.

  • infrahubctl object validate reports the reference errors that the schema alone can detect:

    • a kind that does not inherit from the peer
    • an unknown kind
    • a kind without an HFID
    • the wrong number of HFID values
    • keys other than kind and hfid

    Validation does not check that the peer exists, because the peer can be created earlier in the same load. A missing or ambiguous peer is reported at load time.

What stays the same:

  • References to node peers, references to generics that have an HFID, UUID references, and nested objects (kind plus data) behave as before. A unit test covers nested objects on a generic peer.

How to review

  1. Start with infrahub_sdk/spec/object.py, which holds all logic changes:
    • get_relationship_info (how values are classified)
    • _validate_kind_hfid_reference (schema checks)
    • _clean_reference and _resolve_generic_hfid (load-time resolution)
  2. Then read tests/unit/sdk/spec/test_object.py and TestSpecObjectGenericPeer in tests/integration/test_spec_object.py.
  3. docs/docs/python-sdk/topics/object_file.mdx has the new section "Reference a node through a generic relationship".
  4. dev/specs/002-generic-peer-hfid/ holds the spec, plan, tasks and implementation report. These files are not code.

Parts that need extra scrutiny:

  • A plain string reference (not a UUID) to a generic without an HFID used to be sent as an id. It is now resolved as an HFID and fails if nothing matches. It is not known whether the server accepted the old form.
  • An HFID-alone reference makes one query per candidate kind and caches nothing. A code comment notes this limit.

Alternatives considered (see research.md):

  • A prefixed string such as Kind:router1/Gi0/0/3. Rejected because names can contain / and :.
  • A file-level parameter that maps each relationship to one kind. Rejected because one file can mix peer kinds.
  • Resolving the reference on the server. Out of scope.

How to test

uv run pytest tests/unit/sdk/spec/ tests/unit/ctl/object/ -q
uv run pytest tests/integration/test_spec_object.py -k GenericPeer -v

Local results after the review fixes:

  • Unit tests: 104 passed.
  • Integration tests: the 4 GenericPeer tests passed.
  • Before the review fixes, the full test_spec_object.py passed (16 tests).

Impact & rollout

  • Backward compatibility: existing object files load as before, except for the non-UUID string case described under "How to review".
  • Server version: references with kind, and HFID-alone references to generics, need Infrahub 1.1.0 or later. That release added RelatedNodeInput.kind (server commit c4fa580582). The compatibility page still lists 0.16.x and 1.0.x, and this PR does not change it.
  • Performance: no extra queries for references that name a kind.

Checklist

  • Tests added/updated
  • Changelog entry added (changelog/+generic-peer-hfid.added.md)
  • External docs updated (docs/docs/python-sdk/topics/object_file.mdx)
  • Internal .md docs updated (not done; the knowledge extraction is still pending)

Summary by cubic

Lets object files reference existing nodes through relationships whose peer is a generic, by giving the node's kind and hfid together. Previously these peers could only be referenced through schema workarounds: narrowing the relationship to one node kind or adding an HFID to the generic.

Behavior

  • References can be {kind: <NodeKind>, hfid: [...]} on cardinality-one and cardinality-many relationships; kind is passed with hfid so the server resolves the generic peer against the named kind.
  • When the generic has no HFID, an HFID-alone reference is looked up among the node kinds that inherit from the generic, and the matching node's id is sent, so released servers fill computed attributes without another lookup. The load fails with a message naming the candidate kinds when zero or more than one match.
  • When the generic has a default filter, a string reference is sent as before. Profile and template kinds in the generic's used_by are skipped.
  • infrahubctl object validate reports schema-detectable errors: a kind that does not inherit from the peer, an unknown kind, a kind without an HFID, a wrong number of HFID values, and keys other than kind and hfid.

Rollout and compatibility

  • Existing object files load as before, except non-UUID string references to a generic without an HFID, which are now resolved as HFIDs and fail when nothing matches instead of being sent as an id.
  • Kind-plus-HFID references need Infrahub 1.1.0 or later, which added RelatedNodeInput.kind; HFID-alone references on generics do not, because the SDK sends the resolved id.

Written for commit 89a513c. Summary will update on new commits.

View guided diff Turn on auto-fix

@minitriga minitriga added the type/feature New feature or request label Oct 19, 2026
@github-actions github-actions Bot added the type/documentation Improvements or additions to documentation label Oct 19, 2026
@minitriga
minitriga marked this pull request as ready for review October 19, 2026 09:48
@minitriga
minitriga requested a review from a team as a code owner October 19, 2026 09:48
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 89a513c
Status: ✅  Deploy successful!
Preview URL: https://0d0bc6ab.infrahub-sdk-python.pages.dev
Branch Preview URL: https://atg-strict-waves-stop.infrahub-sdk-python.pages.dev

View logs

@codecov

codecov Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
infrahub_sdk/spec/object.py 95.83% 1 Missing and 2 partials ⚠️
@@            Coverage Diff             @@
##           stable    #1432      +/-   ##
==========================================
+ Coverage   85.06%   85.18%   +0.12%     
==========================================
  Files         148      148              
  Lines       13303    13347      +44     
  Branches     1960     1965       +5     
==========================================
+ Hits        11316    11370      +54     
+ Misses       1422     1411      -11     
- Partials      565      566       +1     
Flag Coverage Δ
integration-tests 38.86% <72.22%> (+0.07%) ⬆️
python-3.10 58.41% <95.83%> (+0.52%) ⬆️
python-3.11 58.41% <95.83%> (+0.52%) ⬆️
python-3.12 58.41% <95.83%> (+0.51%) ⬆️
python-3.13 58.41% <95.83%> (+0.51%) ⬆️
python-3.14 58.41% <95.83%> (+0.51%) ⬆️
python-3.15 58.41% <95.83%> (?)
python-filler-3.12 23.68% <0.00%> (-0.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
infrahub_sdk/spec/object.py 87.08% <95.83%> (+1.84%) ⬆️

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ogenstad

Copy link
Copy Markdown
Contributor

This comment was written by an AI (Claude Code). The findings come from reading the code in this PR at 92a6806 and in Infrahub at 4d76118 (release-1.12). None of the findings were reproduced against a running Infrahub server.

Summary

HFID-only references to a generic peer fail with a ValueError when the peer is CoreNode, for example group members. Released Infrahub servers also do not pass a generic peer given as kind + hfid to computed attributes. Several error messages are misleading or incomplete. It is not decided whether HFID-only references to a generic peer should be resolved in the SDK, as this PR does, or by the Infrahub server.

Bugs introduced by this PR

1. An HFID-only reference to a CoreNode peer fails with a ValueError

  • Group members has the peer CoreNode (group.py). CoreNode is a generic without an HFID, so members: [atl1-edge1] goes to _resolve_generic_hfid (object.py#L511-L514).
  • Infrahub adds every profile kind and template kind to CoreNode.used_by (schema_branch.py#L2674-L2692, #L3223). Profiles have the 1-part HFID profile_name__value (schema_branch.py#L2760), so they become candidates for any 1-part HFID.
  • client.get raises ValueError: Cannot filter by HFID if the node doesn't have an HFID defined for any schema that is not a NodeSchemaAPI (client.py#L765-L769). ProfileSchemaAPI and TemplateSchemaAPI are not NodeSchemaAPI.
  • The list comprehension at object.py#L539-L543 queries every candidate. The load fails even when an earlier kind matches.
  • Before this PR the same file also failed, but on the server with NodeNotFoundError, because CoreNode has no default_filter. So no file that loaded before now fails. However, the new documentation says an HFID without kind works for any generic peer without an HFID, and this case does not.
  • Suggested fix: keep only candidates that are NodeSchemaAPI. Add a unit test where used_by includes a profile kind.

2. A generic peer with default_filter and no HFID no longer resolves through default_filter

  • Before this PR, a non-UUID string for a generic peer without an HFID was sent as {id: <string>}. The server then looked it up with the generic's default_filter (relationship/model.py#L590-L595, manager.py#L615-L641).
  • The string now goes to _resolve_generic_hfid instead. If the HFIDs of the inheriting kinds do not match the string, the load fails with no X with HFID [...].
  • This only applies to a generic that has a default_filter and no unique attribute, because Infrahub derives an HFID from a unique attribute. The core generics and the schemas in models/ all have a unique attribute or an HFID, so they are not affected. A custom schema of this type that loaded before can fail after this PR.

3. Generic peers given as kind + hfid are not passed to computed attributes on released servers

  • Both new paths send {"hfid": [...], "kind": "<concrete kind>"} to the server (object.py#L509-L510, #L561).
  • Relationship.resolve() on the server already used the given kind for a generic peer. Relationship._get_peer(), which computed attributes use, did not. It looked the HFID up on the generic kind, caught NodeNotFoundError and set the peer to None. Infrahub fixed this in e9e42d4 (issue 11018).
  • That commit is on stable, develop and release-1.12, but not in any release tag, including infrahub-v1.11.9. On every released server, a Jinja2 computed attribute that reads a generic peer given as kind + hfid is rendered without the peer's value. The test in that commit covers this case.
  • Suggested fix: return {"id": node.id} from _resolve_generic_hfid, which avoids the issue for HFID-only references. For explicit kind + hfid references, document the minimum Infrahub version.

4. A nested object without the data: key now gets an error about peer references

  • is_hfid_reference_dict treats any dict that has kind and no data as a peer reference (object.py#L26-L27).
  • Example: parent_interface: {kind: InterfacesPhysicalInterface, name: Gi0/0/3, device: router1}, where data: is missing. Before this PR the error was Relationship parent_interface must be a dict with 'data', which names the fix. It is now only kind and hfid are allowed in a peer reference, and both are required, got device, kind, name (object.py#L472), which does not mention data.

5. The error for a failed HFID-only reference does not name the list item, and has a space before ;

  • _resolve_generic_hfid builds the position as [*position, rel_info.name] (object.py#L545). In a cardinality-many relationship, the error does not say which item failed. Validation errors include the index, for example 1.source_interfaces.2.
  • When there is no candidate kind, the message is built as f"... {hfid} {searched}" and searched starts with ;. The message then reads no X with HFID ['router9'] ; no kind that inherits... (object.py#L547-L555).

6. HFID-only resolution sends one query per candidate kind for every reference

  • For each reference, _resolve_generic_hfid calls client.get once for each candidate kind, one after another, without a cache (object.py#L538-L543). Each call fetches the full node.
  • The node that is found is not used. The SDK sends {hfid, kind}, so the server looks the node up again.
  • A file with 1,000 references and 2 candidate kinds sends 2,000 queries. In Infrahub this runs on every repository sync. Returning the node id, as suggested in finding 3, removes the second lookup.

Code and documentation clean-up

  • normalize_hfid_references has no callers left (object.py#L63). The comment at object.py#L618 still says the branch only normalizes string HFIDs. That branch now also resolves generic peers, which sends GraphQL queries.
  • 17 added lines in dev/specs/002-generic-peer-hfid/ contain en dashes or em dashes, for example the task ranges in opsmill-implement-report.md and tasks.md.

Test coverage

  • The new unit tests replace client.get with monkeypatch and return a result for every kind (test_object.py#L752). As a result, they cannot detect the ValueError in finding 1, and they do not check the GraphQL query that is sent. Existing tests in this file already replace client.create and InfrahubNode.save the same way. Replacing client.get is new in this PR.
  • tests/AGENTS.md asks for the httpx_mock fixture for HTTP mocking.

Checked and found to already exist before this PR

  • A missing or ambiguous peer is only detected when the file is processed, after validation. Infrahub's git integrator validates all files before it processes any of them (integrator.py#L1903-L1917). An error during processing can therefore leave the nodes that were created earlier in the import. This already happened before this PR for a reference to a missing peer. The only new case is a reference that matches more than one kind.
  • create_node already called get_relationship_info twice per relationship before this PR. This PR adds one schema fetch for the declared peer (object.py#L171), which the schema cache serves.

Not decided: whether the SDK or the Infrahub server resolves HFID-only references to generic peers

This PR resolves HFID-only references in the SDK, and only for object files. The same reference still fails through client.create, GraphQL mutations and the web interface.

The alternative is to resolve them on the server, in Relationship._get_hfid_lookup_kind and NodeManager.get_one_by_hfid (relationship/model.py#L436-L441). The server would search the node kinds that inherit from the generic in one database query, and return an error when more than one node matches.

  • Resolving in the SDK works with Infrahub servers that are already released. It sends several queries per reference, and the SDK has to exclude profile and template kinds itself.
  • Resolving on the server gives the same behaviour to every client and needs one query per reference. It requires a new Infrahub release.

One option is to keep the explicit kind + hfid support in this PR, because the server already handles it apart from finding 3. HFID-only resolution would then move to Infrahub. That would remove findings 1, 2, 5 and 6 from this PR.

@minitriga

Copy link
Copy Markdown
Contributor Author

Infrahub CI passed against this branch: opsmill/infrahub#11303 (Infrahub stable with python_sdk at 92a6806).

  • Gate for huge-runner jobs passed, so the backend jobs ran.
  • All backend unit, component, integration, functional and Docker integration jobs passed, as did the E2E jobs and validate-generated-documentation.
  • No job failed. The skipped jobs are frontend and other checks that only run when their files change.

- Search only node kinds, so profile and template kinds in the generic's
  used_by no longer make client.get raise a ValueError.
- Send the id of the matching node, so released servers fill computed
  attributes and do not look the node up again.
- Send a plain string as before when the generic has a default_filter.
- Name the list item in errors for cardinality-many relationships.
- Mention data: when a dict has kind and other keys but no hfid.
- Remove the unused normalize_hfid_references helper.
- Mock GraphQL with httpx_mock in the HFID-only tests.
@minitriga

Copy link
Copy Markdown
Contributor Author

Reply to #1432 (comment). All findings were checked against the code, and all six are correct. Fixed in 31faec0 and 89a513c.

  1. An HFID-only reference to a CoreNode peer fails with a ValueError. Fixed. _resolve_generic_hfid now searches only kinds whose schema is NodeSchemaAPI, so profile and template kinds are never queried. The test fixture now has a profile kind with a 1-part HFID in the generic's used_by. Without the fix, test_hfid_only_reference_to_generic_peer_no_match[one_part_no_candidate] fails with this ValueError.
  2. A generic peer with default_filter and no HFID no longer resolves through default_filter. Fixed. When the generic has a default_filter, a single non-UUID string is sent unchanged, as before this PR, and the server looks it up with the default_filter. A list HFID is still resolved by the SDK. New test: test_string_reference_to_generic_peer_with_default_filter_is_sent_as_id.
  3. Generic peers given as kind + hfid are not passed to computed attributes on released servers.
    • HFID-only references: fixed. _resolve_generic_hfid now returns {"id": node.id}.
    • Explicit kind + hfid references: object_file.mdx now states that on Infrahub 1.11 and earlier, a computed attribute that reads such a peer is rendered without its value.
  4. A nested object without the data: key gets an error about peer references. Fixed. When the dict has no hfid, the error now ends with "to create a new peer, put its attributes under data:".
  5. The error does not name the list item, and has a space before ;.
    • The error position now includes the 1-based list index, as validation errors do, for example 1.source_interfaces.2.
    • The stray space before ; is removed.
  6. One query per candidate kind for every reference. Partly fixed. Returning the node id removes the second lookup on the server. One query per candidate kind remains. The docs now say this, and the cache is left for later.

Clean-up

  • normalize_hfid_references is removed.
  • The comment above the cardinality-many branch is corrected.
  • The dashes in dev/specs/002-generic-peer-hfid/ are replaced.

Test coverage: the HFID-only tests now answer the GraphQL requests with httpx_mock, instead of replacing client.get. Each test checks which kinds are queried, and the mock checks that every query filters by hfid. The generic-peer integration tests pass against Infrahub.

SDK or server: we keep HFID-only resolution in the SDK for now, because it works with Infrahub servers that are already released. Moving it to the server, in Relationship._get_hfid_lookup_kind and NodeManager.get_one_by_hfid, can follow in a later Infrahub release.

@minitriga

Copy link
Copy Markdown
Contributor Author

Infrahub CI passed against the review fixes: opsmill/infrahub#11303, with Infrahub stable and python_sdk at 89a513c.

  • Gate for huge-runner jobs passed, so the backend jobs ran.
  • All backend unit, component, integration, functional and Docker integration jobs passed, and so did the E2E jobs.
  • On the first run, two jobs failed. Both passed when re-run, with no change:
    • backend-tests-component (other): test_concurrent_save.py::test_updated_node_avoids_duplicate_edges raised an IndexError in Infrahub's node query code.
    • backend-benchmark: the benchmark process crashed with exit code 139.

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

Labels

type/documentation Improvements or additions to documentation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants