Skip to content

fix(node): refuse adding or removing a number pool range through the pool - #1469

Merged
polmichel merged 15 commits into
infrahub-release-1.12from
number-pool-ranges-add
Nov 3, 2026
Merged

polmichel merged 15 commits into
infrahub-release-1.12from
number-pool-ranges-add

Conversation

@polmichel

@polmichel polmichel commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The ranges of a CoreNumberPool are bounds in its mutation input, not references to range nodes. Three paths did not account for that:

  • add(), extend() and remove() on the ranges relationship did not change the pool: the first two dropped the range without an error, and remove() sent the remaining ranges as references, which Infrahub refused.
  • infrahubctl object update --set ranges=... lost the edit in silence.
  • Loading an object file that declares ranges as objects failed. The loader created the pool without its ranges, so Infrahub gave it the default range, then created each declared range as a node of its own, which Infrahub refused as overlapping: the default range on a new pool, or the range it already held on an existing one. Reported by Wim.

Close IFC-3543

What each entry point does

Nothing that produced the intended result is taken away. What was silent becomes explicit, and the object file starts working.

Entry point Before After
Any node or field other than the ranges of a number pool worked unchanged
pool.ranges.add(), .extend(), .remove() dropped the range, or sent references Infrahub refused raise FeatureNotSupportedError, naming the ways out
pool.start_range and pool.end_range worked, deprecated in Infrahub 1.12 unchanged
object create CoreNumberPool --set ranges='[{"start": 1, "end": 10}]' worked unchanged
object create CoreNumberPool --set ranges=range-1 sent a reference Infrahub refused refused, naming the fields it takes
object update CoreNumberPool --set ranges=... applied nothing, without an error refused, naming the create that upserts the pool
object create --file, object update --file, object load failed: the pool was created without its ranges, then each range on its own, which Infrahub refused as overlapping the ranges travel with the pool

An object file sets the ranges, on a new pool and on one that already exists:

ranges:
  data:
    - start: 64512
      end: 65534

The rule is keyed on the (name, peer) pair, so another relationship to CoreNumberPoolRange keeps references, add() and remove().

Not covered

  • pool.ranges.peers: editing the list is not supported and the SDK does not report it.
  • add_relationships(), remove_relationships() and infrahubctl load of an export reach Infrahub through other paths and bypass the refusal.

The table of relationships is a stopgap. The lasting fix belongs in Infrahub: a schema that states the mutation takes the peer's fields, or a mutation that accepts references like every other relationship.

Tests

Unit tests cover each refused method for both clients, both CLI guards, and the object file in each form it accepts. The object-file failure lands red in its own commit before the fix.

Run against a live Infrahub 1.12.0.post1, with testcontainers:

uv run pytest tests/integration/test_number_pool_attach_ranges.py     8 passed in 2:08

What that covers: the ranges a create and an upsert declare, the way out the refusal names, a range node added, changed and deleted on its own, an object file loaded twice against the same pool, and attaching and detaching numbers.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the type/documentation Improvements or additions to documentation label Nov 2, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 04bc1bc
Status: ✅  Deploy successful!
Preview URL: https://54ca50ea.infrahub-sdk-python.pages.dev
Branch Preview URL: https://number-pool-ranges-add.infrahub-sdk-python.pages.dev

View logs

@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@                    Coverage Diff                    @@
##           infrahub-release-1.12    #1469      +/-   ##
=========================================================
+ Coverage                  87.74%   87.81%   +0.07%     
=========================================================
  Files                        155      157       +2     
  Lines                      15517    15575      +58     
  Branches                    2085     2094       +9     
=========================================================
+ Hits                       13615    13677      +62     
+ Misses                      1345     1341       -4     
  Partials                     557      557              
Flag Coverage Δ
integration-tests 42.52% <38.70%> (+<0.01%) ⬆️
python-3.10 62.70% <74.19%> (+0.15%) ⬆️
python-3.11 62.70% <74.19%> (+0.15%) ⬆️
python-3.12 62.70% <74.19%> (+0.15%) ⬆️
python-3.13 62.70% <74.19%> (+0.17%) ⬆️
python-3.14 62.71% <74.19%> (+0.17%) ⬆️
python-3.15 62.70% <74.19%> (+0.15%) ⬆️
python-filler-3.12 23.15% <24.19%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
infrahub_sdk/ctl/object/create.py 97.72% <100.00%> (+0.10%) ⬆️
infrahub_sdk/ctl/object/inline_peers.py 100.00% <100.00%> (ø)
infrahub_sdk/ctl/object/update.py 76.28% <100.00%> (+0.49%) ⬆️
infrahub_sdk/node/constants.py 100.00% <100.00%> (ø)
infrahub_sdk/node/inline_peers.py 100.00% <100.00%> (ø)
infrahub_sdk/node/related_node.py 95.41% <100.00%> (+0.01%) ⬆️
infrahub_sdk/node/relationship.py 93.82% <100.00%> (+0.34%) ⬆️
infrahub_sdk/spec/object.py 86.39% <100.00%> (+0.44%) ⬆️

... and 1 file with indirect coverage changes

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

@polmichel
polmichel force-pushed the number-pool-ranges-add branch from acf4338 to 2624fe1 Compare November 2, 2026 17:21
polmichel and others added 5 commits November 2, 2026 18:50
…s fields

The mutation of such a relationship takes the fields of a peer, not a reference to
one, so add(), extend() and remove() could not reach Infrahub: add() and extend()
dropped the peer without an error, and remove() sent the remaining peers as
references, which Infrahub refused. They now raise FeatureNotSupportedError and name
the ways out that work. The relationships concerned are listed as (name, peer) pairs,
so another relationship to the same kind of node stays editable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A relationship whose mutation takes the peers' fields does reach Infrahub on a
create, so object create refuses only a value that names peers by reference and
lets a JSON list of their fields through. An update sends the peers the node was
fetched with, so object update refuses every value and names the create that
upserts the node. Both report the refusal before any change is applied, and the
reason they give comes from the constant the SDK message is built from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d on their own

The two tests fail: loading the file sends a CoreNumberPoolRange mutation of its own
next to the pool, which Infrahub refuses as overlapping the range the pool holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A relationship whose mutation takes the peers' fields now travels in the payload of
the node that declares it, in both forms an object file accepts for a list of
objects. A list of references stays a list of references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@polmichel
polmichel force-pushed the number-pool-ranges-add branch from 2624fe1 to 93effe5 Compare November 2, 2026 18:48
… them

The branch that sends the peers' fields with their node read the list under `data`
as it stands, while the validation pass runs it through the processors first. A file
with `parameters: {expand_range: true}` was therefore validated expanded and saved
unexpanded. Both passes now read the same peers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@polmichel
polmichel force-pushed the number-pool-ranges-add branch from 93effe5 to 1292c6f Compare November 2, 2026 19:00
@polmichel
polmichel marked this pull request as ready for review November 2, 2026 19:45
@polmichel
polmichel requested a review from a team as a code owner November 2, 2026 19:45

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 15 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/relationship.mdx">

<violation number="1" location="docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/relationship.mdx:43">
P2: This says saving an existing node sends the peers it was fetched with, but the serializer omits an unchanged relationship from the update instead. Document that local peer-list edits are ignored on an existing node and Infrahub's existing peers remain unchanged.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

relationship.
relationship. For a relationship that takes its peers as fields instead of references, that
holds only until the node exists: a create sends the list as it stands, while a save of a node
Infrahub already holds sends the peers it was fetched with.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: This says saving an existing node sends the peers it was fetched with, but the serializer omits an unchanged relationship from the update instead. Document that local peer-list edits are ignored on an existing node and Infrahub's existing peers remain unchanged.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/relationship.mdx, line 43:

<comment>This says saving an existing node sends the peers it was fetched with, but the serializer omits an unchanged relationship from the update instead. Document that local peer-list edits are ignored on an existing node and Infrahub's existing peers remain unchanged.</comment>

<file context>
@@ -38,7 +38,9 @@ peers(self) -> list[RelatedNode[PeerT] | RelatedNodeSync[PeerT]]
-relationship.
+relationship. For a relationship that takes its peers as fields instead of references, that
+holds only until the node exists: a create sends the list as it stands, while a save of a node
+Infrahub already holds sends the peers it was fetched with.
 
 **Returns:**
</file context>

Comment thread infrahub_sdk/spec/object.py Resolved
Comment thread infrahub_sdk/ctl/object/utils.py Resolved
polmichel and others added 5 commits November 2, 2026 22:49
The create guard read a list of objects as the fields of the peers, so an object
holding an id, an hfid or a node went through and failed against the server instead
of reporting the refusal. The keys that name a peer now have one definition, which
the node side reads as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The branch that sends the peers' fields read the parameters of the relationship
alone, while the validation pass falls back to the parameters of the file. A file
that sets expand_range for everything it holds was therefore validated expanded and
saved unexpanded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Describing what each save path does with an edit invited readers to rely on one of
them. The guide and the docstring now name the complete list as the way to set the
ranges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each one now carries only what the code does not say: why these peers do not go
through the deferred branch, and why the parameters fall back to the file's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +259 to +261
async def test_changing_the_peer_list_before_the_pool_exists_sets_what_is_sent(
clients: BothClients, number_pool_schema: NodeSchemaAPI
) -> None:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This test covers something the deprecation guide advises against: it shows that all the methods manipulating .peers do nothing. It passes, so I think it’s still good to keep it, even though the documentation tells users not to do this. Otherwise, we'd need to state that this does nothing for add, remove, ... but not for append.


**Raises:**

- `FeatureNotSupportedError`: If the relationship takes its peers as fields instead of references.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe naming this error differently would be more appropriate/accurate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this isn't really fixed b/c it was never released

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, I'll remove it

Comment thread infrahub_sdk/node/constants.py Outdated

# Relationships whose input in a mutation takes the peer's fields, not a reference to an existing peer,
# as (name, peer) pairs.
INLINE_INPUT_RELATIONSHIPS = frozenset({("ranges", "CoreNumberPoolRange")})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can always change it later if we need to, but I would expect this to be dict[str, list/tuple[str]] (or the frozen version of that) to allow linking multiple relationships to a single kind. so {"CoreNumberPoolRange": ["ranges"]}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I’ll do that! It’s definitely better to have some logic tying these strings together

Comment thread infrahub_sdk/node/constants.py Outdated
INLINE_INPUT_RELATIONSHIPS = frozenset({("ranges", "CoreNumberPoolRange")})


def _peers_are_inline_input(schema: RelationshipSchemaAPI) -> bool:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this lose the _? I know we use _ methods on the Node objects so we don't block attribute/relationship names, but I don't think we need it here. I could be missing something

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was just to not expose this method to users through SDK documentation, but it's fine to expose it I guess, and it's also weird to have this method inside constants.py.
I'll change it

Comment on lines +147 to +148
await obj.process(client=client)
await obj.process(client=client)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this to test idempotency?

Comment thread tests/unit/ctl/object/test_utils.py Outdated
result = prepare_relationship_data(data, schema)
assert result == {"name": "router1", "site": ["DC1"], "tags": [["blue"]]}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

these could probably move to their own test module. utils is a vague name to start with and test_utils is somehow worse


@pytest.fixture
async def range_policy_schema() -> NodeSchemaAPI:
"""A schema with its own relationship to CoreNumberPoolRange nodes."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is not something that I had considered, but I guess CoreNumberPoolRange is just a schema that any other schema can link to, although CoreNumberPoolRange is branch-agnostic, so not sure how useful it would be

assert 'ranges: [ { id: "range-1" }, ]' in query


async def test_object_file_sends_the_ranges_its_parameters_expand(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need a test for this? I don't think there's a scenario where using parameter expansion on pool ranges would actually work server-side

@polmichel polmichel Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That was mostly to prove that the code path doesn’t fork too early and still runs the code inside infrahub_sdk.spec.processors.factory.DataProcessorFactory.process_data, in case it contains behavior other than expansion. But I’m fine with removing it as it's hard to read why it is there.

polmichel and others added 4 commits November 3, 2026 09:19
The table, the message and the predicate that reads them now live in a module of
their own, keyed by peer kind so a kind can have more than one such relationship.
The predicate loses its leading underscore: four modules in three packages read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both guards sat in a module named for everything the object commands share, and
their tests in a test module named after it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…anges

Nothing was released without it, so it is part of what the release adds rather than
a fix. The integration test says why it processes the same file twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expansion turns a start into a string, which the Number attribute of a range does
not take, so the tests pinned a payload Infrahub has no reason to accept. The
parameters still travel, so validating and saving read the same peers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 16 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/inline_peers.mdx">

<violation number="1" location="docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/inline_peers.mdx:12">
P3: This page exposes `peers_are_inline_input` as a public SDK API even though the helper is internal and is not exported from `infrahub_sdk.node`. Remove this page from the generated public API reference, or explicitly promote and support the helper as part of the public API.

(Based on your team's feedback about curating generated SDK API references.) [905c8e52-ba52-4980-8997-2766c3641fb0]</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Turn on auto-fix | Re-trigger cubic


## Functions

### `peers_are_inline_input`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This page exposes peers_are_inline_input as a public SDK API even though the helper is internal and is not exported from infrahub_sdk.node. Remove this page from the generated public API reference, or explicitly promote and support the helper as part of the public API.

(Based on your team's feedback about curating generated SDK API references.) [905c8e52-ba52-4980-8997-2766c3641fb0]

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/inline_peers.mdx, line 12:

<comment>This page exposes `peers_are_inline_input` as a public SDK API even though the helper is internal and is not exported from `infrahub_sdk.node`. Remove this page from the generated public API reference, or explicitly promote and support the helper as part of the public API.

(Based on your team's feedback about curating generated SDK API references.) [905c8e52-ba52-4980-8997-2766c3641fb0]</comment>

<file context>
@@ -0,0 +1,18 @@
+
+## Functions
+
+### `peers_are_inline_input`
+
+```python
</file context>

@polmichel
polmichel merged commit c1cf8e2 into infrahub-release-1.12 Nov 3, 2026
31 checks passed
@polmichel
polmichel deleted the number-pool-ranges-add branch November 3, 2026 10:05
polmichel added a commit to opsmill/infrahub that referenced this pull request Nov 3, 2026
… from an object file (#11370)

The SDK commit 1.12 ships creates the pool first and each range afterwards as a node of
its own, which Infrahub refuses as overlapping the range the pool already holds. The SDK
now sends the ranges as the fields of the pool mutation (opsmill/infrahub-sdk-python#1469),
so an object file that declares `ranges` loads whether or not the pool exists.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ogenstad added a commit to opsmill/infrahub that referenced this pull request Nov 3, 2026
…1.12

The SDK now includes opsmill/infrahub-sdk-python#1470, which adds the
shared argument to GitRepo, and opsmill/infrahub-sdk-python#1469.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants