Skip to content

fix(store): find replicas and snapshots in any name spelling, store layer stacks in canonical case - #217

Open
Andrei Kvapil (kvaps) wants to merge 48 commits into
mainfrom
fix/name-case-folding
Open

Andrei Kvapil (kvaps) wants to merge 48 commits into
mainfrom
fix/name-case-folding

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

LINSTOR names are case-insensitive, and the store keeps an object under the CR name its LINSTOR name maps to: the lowercase name inside RFC 1123, a hash of the name as given otherwise. So PVC-X and pvc-x are one definition, and My_Res and my_res are two. A replica or snapshot names its definition and node in spec, though, and a field selector compares that value exactly, so a listing asked in another spelling came back empty. On rd d PVC-X over pvc-x the snapshot gate passed over snapshots it could not see, the cascade reaped nothing, and the folded delete took the definition and left its replicas and snapshots behind, with DRBD still up under them. The node-scoped listing had the same gap for node names (#212).

The fix has two halves.

The store: it lists a definition's replicas and snapshots, and a node's replicas, under the given, the CR and the registered spelling. It stores new replicas and snapshots with their parents' CR names in spec and keeps the registered spellings in annotations that only the REST surface reads, so clients see the spelling they compare. It derives those annotations from the parents it resolved and never takes them from a caller. A parent it cannot read is an error, never "not there". linstor-migrate writes the same shape.

Everything above the store: one identity per object, naming.Key, in a package of its own (pkg/naming). Controllers, the satellite, the dispatcher, the placer, the REST handlers and the CLI compare object names only with naming.Same and key maps only by naming.Key; the per-site helpers and the case folding they replaced are gone. That covers what earlier fixes reached one place at a time, and a few places they had not reached yet: the evacuation drain target, the placer's min-rsc-count weight, the controller's read of a replica's definition, the satellite heartbeat and cold-start orphan set, the list filters and restore markers, which folded case and so joined two definitions such as My_Res and my_res, migrate-disk, which took Node-1 and node-1 for two nodes and so could migrate a replica onto itself and prune the source, the node-connection list filter, resource-connection paths, which opened a second entry for one pair, the placer's lookup of an existing replica's pool, and the peer-forget acknowledgment key (the wait that reads it has no caller yet). The migration controller drops a stored migration marker that names the destination's own node instead of pruning the destination, and REST migrate-disk refuses one node in two spellings. A snapshot's status entries for one node in two spellings are folded into one on every write, so the satellite and the controller read the same acknowledgment. The satellite no longer takes a peer written in another spelling in its .res file for a removed peer, and the DRBD renderer matches a connection's paths to its hosts by key. The in-memory store resolves every name through the key and keys replicas and snapshots by the same name the Kubernetes store gives their CRs.

A test in pkg/naming keeps the class from coming back. It type-checks the tree and follows an object name from where it enters (the name fields of the wire and CRD types, the REST path and query parameters and CLI flags that carry names, and anything handed to the store as a name) through assignments, ranges, concatenation and formatting, struct fields, calls into functions, closures and interface methods, and returns, and fails where one reaches a comparison with another name, a switch case, a prefix test, a map key, a search, a case fold or a CR address without naming.Key or naming.Same on the way. It goes by where a value comes from rather than what it is called; its doc comment lists what it follows and what it cannot see, and a planted package checks that it catches every shape under arbitrary variable names and leaves the correct ones alone. Reviewed sites where both sides come from one listing or one intent are allowlisted with the reason.

Nothing on disk is renamed. The satellite names a resource, its volumes and snapshots by the definition's CR name, which for every row written before is the name already in use; rows in a mixed-case spelling were never applied before this. The dispatcher still emits a peer under the spelling its row stores, and the satellite renders its own name as configured; only the lookups key them.

Rows written before this in a fourth spelling, neither the asked, the CR nor the registered one, stay out of reach of the scoped listings. Nothing is deployed on such data yet, so there is no migration for it.

The layer stack had the same problem (#204): rd create, rg create, rg modify and the CLI stored a layer stack as the caller spelled it while some readers compare exactly, so the satellite controller read drbd,storage as a stack without DRBD. Every write path now stores the canonical spelling, the autoplace paths that give a definition its first stack included, and the satellite check folds layer names for objects written before or applied by hand.

Fixes #194
Fixes #204
Fixes #212

Testing

  • TestRDDeleteInAnotherCaseSeesSnapshotsAndReplicas (envtest) fails with the spelling set reverted and passes with it.
  • The store conformance subtests for children and node-scoped listings under any spelling, for definitions, nodes, replicas and snapshots addressed in any spelling, and for two case variants outside RFC 1123 being two definitions, run on both stores; an in-memory test pins the hashed child key the CRDs keep off a cluster.
  • Each name-handling fix has a test that fails with the fix reverted; the reproducers from review are among them.
  • pkg/naming: the key is idempotent and keeps case variants outside RFC 1123 apart; the guard reports all 28 planted shapes and none of the planted correct ones, fails on any new site or stale allowlist entry, turns red when any rule is switched off, and turns red when the migrate-disk, node-connection, placer or resource-connection fix is reverted.
  • go test ./... with envtest assets: green apart from the known pkg/rest shutdown flake (test: make the REST and e2e harnesses reliable off the project's own stands #199).
  • golangci-lint (the CI pin): clean.
  • Integration locally against linstor: TestGroupG/SnapCreateListDelete fails 1 run in 3 on this branch and on main alike; ResourceConnectionPathCreate passes 6 of 6.
  • Not run on a stand.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Explain Change →

Important

Review skipped

Too many files!

This PR contains 124 files, which is 24 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Check out review usage here.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e179ca0f-8624-49f8-9a8f-335b83aef0b6
Commits

Reviewing files that changed from the base of the PR and between 67e44e5 and f10a0d5.

Files selected for processing: 124
  • cmd/satellite/coldstart_owned.go
  • cmd/satellite/coldstart_owned_test.go
  • docs/linstor-migration.md
  • go.mod
  • internal/cli/definition.go
  • internal/cli/flags.go
  • internal/cli/guards_test.go
  • internal/cli/handlers.go
  • internal/cli/name_filter_test.go
  • internal/cli/physical.go
  • internal/cli/place.go
  • internal/cli/pool.go
  • internal/cli/resource.go
  • internal/cli/snapshot.go
  • internal/cli/write_more.go
  • internal/controller/auto_diskful_controller.go
  • internal/controller/autosnapshot_controller.go
  • internal/controller/export_test.go
  • internal/controller/mixed_case_replicas_test.go
  • internal/controller/name_key_test.go
  • internal/controller/node_controller.go
  • internal/controller/node_heartbeat_controller.go
  • internal/controller/node_set_identity_test.go
  • internal/controller/node_spelling_lookups_test.go
  • internal/controller/replica_of.go
  • internal/controller/resource_controller.go
  • internal/controller/resource_migration_controller.go
  • internal/controller/resource_migration_controller_test.go
  • internal/controller/resource_state_projection_controller.go
  • internal/controller/resourcedefinition_controller.go
  • internal/controller/resourcedefinition_layer_stack.go
  • internal/controller/resourcegroup_controller.go
  • internal/controller/rg_rebalance_controller.go
  • internal/controller/snapshot_controller.go
  • internal/controller/snapshot_timeout.go
  • internal/controller/stored_cr_names_test.go
  • pkg/dispatcher/dispatcher.go
  • pkg/dispatcher/node_spelling_test.go
  • pkg/drbd/conffile.go
  • pkg/drbd/lookup_paths_spelling_test.go
  • pkg/effectiveprops/effective.go
  • pkg/linstormigrate/convert.go
  • pkg/naming/guard_test.go
  • pkg/naming/naming.go
  • pkg/naming/naming_test.go
  • pkg/naming/testdata/planted/planted.go
  • pkg/placer/name_key_internal_test.go
  • pkg/placer/placer.go
  • pkg/rest/advise.go
  • pkg/rest/autoplace.go
  • pkg/rest/error_reports.go
  • pkg/rest/input_validation_bug_94_97_test.go
  • pkg/rest/multivalue_query_test.go
  • pkg/rest/n_d_registered_spelling_race_test.go
  • pkg/rest/name_filter_internal_test.go
  • pkg/rest/name_spelling_cr_test.go
  • pkg/rest/node_connections.go
  • pkg/rest/node_connections_test.go
  • pkg/rest/node_lifecycle.go
  • pkg/rest/node_spelling_wire_test.go
  • pkg/rest/nodes.go
  • pkg/rest/peer_delete_sync.go
  • pkg/rest/physical_storage.go
  • pkg/rest/query_size_info.go
  • pkg/rest/rd_clone.go
  • pkg/rest/resource_connections.go
  • pkg/rest/resource_definitions.go
  • pkg/rest/resource_group_extras.go
  • pkg/rest/resource_groups.go
  • pkg/rest/resource_toggle_disk.go
  • pkg/rest/resources.go
  • pkg/rest/rg_deleted_race.go
  • pkg/rest/snapshot_restore.go
  • pkg/rest/snapshots.go
  • pkg/rest/storage_pool_definitions.go
  • pkg/rest/storage_pools.go
  • pkg/rest/storage_pools_test.go
  • pkg/rest/upstream_parity_bug_225_229.go
  • pkg/satellite/controllers/cr_names.go
  • pkg/satellite/controllers/discovered_storage.go
  • pkg/satellite/controllers/heartbeat.go
  • pkg/satellite/controllers/heartbeat_test.go
  • pkg/satellite/controllers/layer_stack_pin.go
  • pkg/satellite/controllers/metadata_created_backfill.go
  • pkg/satellite/controllers/mixed_case_replica_apply_test.go
  • pkg/satellite/controllers/observer_cr_name_test.go
  • pkg/satellite/controllers/physicaldevice.go
  • pkg/satellite/controllers/physicaldevice_discovery.go
  • pkg/satellite/controllers/resource.go
  • pkg/satellite/controllers/resource_toggle_disk.go
  • pkg/satellite/controllers/snapshot.go
  • pkg/satellite/controllers/snapshot_fetcher.go
  • pkg/satellite/controllers/snapshot_fetcher_test.go
  • pkg/satellite/controllers/snapshot_node_spelling_test.go
  • pkg/satellite/controllers/storage_sweeper.go
  • pkg/satellite/controllers/storagepool.go
  • pkg/satellite/controllers/sweeper.go
  • pkg/satellite/controllers/sweeper_resfile.go
  • pkg/satellite/reconciler.go
  • pkg/satellite/removed_peers_spelling_internal_test.go
  • pkg/store/cascade.go
  • pkg/store/clone_snapshot.go
  • pkg/store/inmemory.go
  • pkg/store/inmemory_name_key_test.go
  • pkg/store/inmemory_resource.go
  • pkg/store/inmemory_resource_definition.go
  • pkg/store/inmemory_resource_group.go
  • pkg/store/inmemory_snapshot.go
  • pkg/store/inmemory_storage_pool.go
  • pkg/store/inmemory_volume_definition.go
  • pkg/store/k8s/crdname.go
  • pkg/store/k8s/field_index_test.go
  • pkg/store/k8s/spellings.go
  • pkg/store/k8s/storage_pools.go
  • pkg/store/k8s/volume_definitions.go
  • pkg/store/leftover.go
  • pkg/store/local_props.go
  • pkg/store/restore_marker_test.go
  • pkg/store/restore_source.go
  • pkg/store/restore_target.go
  • pkg/store/store.go
  • pkg/store/storetest/storetest.go
  • pkg/validate/resource_delete.go
  • pkg/validate/snapshot.go

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The changes normalize Kubernetes resource and node names across storage, controllers, placement, REST, dispatcher, and satellite paths while preserving registered display spellings. They also canonicalize layer stacks and update migration guidance to use converted CR names.

Changes: Resource and Node Name Identity

Layer / File(s) Summary
Kubernetes storage and spelling resolution
pkg/store/k8s/*, pkg/store/inmemory_*, pkg/store/store.go, pkg/store/storetest/*, pkg/linstormigrate/*, docs/linstor-migration.md, site/content/en/docs/migration.md
The Kubernetes stores resolve alternate parent-name spellings, save normalized CR names in resource and snapshot specs, and retain registered spellings for wire conversion. LINSTOR conversion and migration guidance use normalized names.
Controller replica and node matching
internal/controller/*, pkg/satellite/controllers/*
Controllers match resources, definitions, nodes, and snapshots across accepted spellings. Satellite CR-name resolution helpers are used by replica and snapshot operations.
Placement, REST, and dispatcher matching
pkg/placer/*, pkg/rest/*, pkg/dispatcher/*, internal/cli/*, pkg/validate/*, pkg/satellite/reconciler.go, tests/integration/*
Name comparisons and lookup keys are normalized across placement, REST, dispatcher, CLI, validation, and satellite rendering. Added tests cover spelling variants and deletion with dependents.

Canonical Layer Stacks

Layer / File(s) Summary
Layer-stack canonicalization and validation
pkg/validate/layers.go, pkg/rest/layer_validation.go, pkg/rest/resource_definitions.go, pkg/rest/resource_groups.go, pkg/rest/autoplace.go, internal/cli/write_more.go, pkg/satellite/controllers/resource.go, pkg/rest/layer_stack_case_test.go, internal/cli/layer_list_case_test.go, pkg/satellite/controllers/rd_needs_drbd_case_test.go
Layer-stack validation returns canonical layer names, and REST and CLI paths store canonical stacks. Resource-group rebalance checks ignore case-only differences. Tests cover canonical storage and case-insensitive DRBD detection.

Estimated code review effort: 5 (Critical) | ~90 minutes

Change: Bug fix · Severity of issue fixed: High

Suggested reviewers: lexfrei

Merge Risk: Moderate · previous review 67e44

Resolve the placement identity mismatch before merging: it can bypass provider and shared-storage restrictions. Correct the migration naming guidance and keep distinct definitions isolated in the in-memory store.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title accurately summarizes the two main changes: spelling-insensitive replica and snapshot lookup, and canonical layer-stack storage.
Linked Issues check Passed Directly linked issues: #194, #204, #212. No contextual or parent issues apply.

[#194, #212] Child-resource listings. At head, resources.ListByNode resolves the node spellings, then queries each sp…
Out of Scope Changes check Passed Most changed files apply the same case-insensitive name handling that the three linked issues require. The changes fall into four groups:

1. Readers that compare stored spec names exactly. The stor…
Docstring Coverage Passed Docstring coverage is 88.99% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentati…
Full details: Linked Issues check

Explanation

Directly linked issues: #194, #204, #212. No contextual or parent issues apply.

[#194, #212] Child-resource listings. At head, resources.ListByNode resolves the node spellings, then queries each spelling through listUnderSpellings on spec.nodeName. ListByDefinition uses listByDefinitionThrough, which applies the same spelling set. The summary states that snapshots.ListByDefinition resolves all accepted RD spellings, queries each one, and falls back to an exhaustive filter that matches any spelling. Create stores the CR names of the definition and node in spec and labels, and keeps the registered spellings in annotations. crdToWireResource returns the registered spellings to clients. The envtest TestRDDeleteInAnotherCaseSeesSnapshotsAndReplicas and the conformance subtests ChildrenOfDefinitionUnderAnySpelling and ListByNodeUnderAnySpelling cover the delete gate and the node-scoped listing. The in-memory store folds comparisons to match.

[#204] Layer-stack canonicalization. The summary states that rd create, rg create, rg modify, autoplace, resource create, and make-available store the canonical stack through canonicalizeLayerStack and storedLayerStack. The satellite rdNeedsDRBD check uses apiv1.LayerInStack, which folds case. Tests TestRDCreateStoresLayerStackInCanonicalCase, TestRGCreateAndModifyStoreLayerStackInCanonicalCase, TestCSILayerListIsStoredInCanonicalCase, and TestRDNeedsDRBDIgnoresCase cover these paths.

Residual limit, not named by the issues: rows written before this change in a fourth spelling (neither the asked, folded, nor registered spelling) are not found. The PR states this and states that no migration is included. The #194 reproduction (PVC-X over pvc-x) is covered for rows written by the fixed code.

Full details: Out of Scope Changes check

Explanation

Most changed files apply the same case-insensitive name handling that the three linked issues require. The changes fall into four groups:

  1. Readers that compare stored spec names exactly. The store now writes CR names into spec (for example Resource.Spec.NodeName and Spec.ResourceDefinitionName). Controllers, satellite, dispatcher, and placer must match those CR names. Without these changes, a mixed-case definition would stop matching its replicas. This is a direct consequence of the #194 design.

  2. Node-name comparisons of the same class as #212 (SameName, isNode, sameNode, sameDefinition, nodeKey), including REST evacuation, error-report filters, pool lookups, and the physical-device ListForNode query.

  3. Layer-stack handling for #204, including the rebalance comparison in rgNeedsRebalance.

  4. linstor-migrate (pkg/linstormigrate/convert.go) and its documentation. These write parent CR names so that adopted children match the store's new layout.

Tests accompany the production changes. The PR objectives name the same areas (replica-event enqueueing, snapshot and node checks, pool and peer lookups, evacuation, projection, migration). Some changes (for example waitForPeerDeletionAcks, validate.RestoreNodesMissingSnapshot) reach beyond the three issues' named call paths. They apply the same normalization, and the PR does not contain an unrelated feature or refactor. A separate peer-deletion acknowledgment issue is explicitly left for follow-up and is not part of this PR's code.

No change was demonstrated to be unrelated to the linked issues.

Full details: Docstring Coverage

Explanation

Docstring coverage is 88.99% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 109 functions across 50 files. (44 skipped: 2 unsupported, 42 over the file limit.)

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Andrei Kvapil (@kvaps) NOT LGTM. The store listings work, but the new write spelling moves replicas of a mixed-case definition out of the controller's view, and three new test files have no license header.

Business context: a definition's replicas and snapshots must be found in any spelling of its name, or rd d passes the snapshot gate on zero rows and leaves replicas with live DRBD behind; layer stacks are stored in canonical case so exact readers see DRBD.

I tested 6e36a49, which sits on current main, so the trial merge is the head itself. pkg/store/..., pkg/validate, internal/cli/... and pkg/satellite/controllers are green with envtest, and the two new conformance subtests run on both stores, not skip. TestRDDeleteInAnotherCaseSeesSnapshotsAndReplicas passes. pkg/rest fails only on server did not stop within 2s after cancel, the known harness flake. The red Integration tests check is TestGroupG/SnapCreateListDelete, which #219 and #220 hit in the same window, so it's not this diff. #200, #206 and #215 to #222 merge cleanly with this branch.

Every fix I reverted one by one turns a named test red: the spelling set, the registered spelling, the raw replica and snapshot writes, the in-memory folds, both layer-stack helpers, the rd create / rg create / rg modify write paths, the CLI parser and rdNeedsDRBD. Two reverts survive, see follow-ups 1 and 2.

I also tried to get two different definitions to share children. Name() hashes the original string when the lowercased name is not RFC 1123, so Pvc_X and pvc_x are two separate definitions. They can't merge here: the Resource and Snapshot CEL rule wants metadata.name to equal <rd>.<node> case-insensitively, so names like that never get children at all.

Blockers

B1: the controller stops seeing replicas written under the registered spelling

pkg/store/k8s/resources.go:184

The controller doesn't use the store listings. It lists every Resource and compares spec.resourceDefinitionName exactly with rd.Name, which is the folded metadata.name. Three places do this:

  • ensureTiebreaker via listReplicasDirect(ctx, rd.Name) (internal/controller/resourcedefinition_controller.go:221, compare at :686), so the diskful and witness count, witness create and reap, quorum
  • stampResizePending (internal/controller/resourcedefinition_controller.go:1658)
  • hostingNodesForRD(ctx, rd.Name) (internal/controller/resource_controller.go:1550, compare at :1764)

Before this PR createOneResource set res.Name from the URL (pkg/rest/autoplace.go:1506) and the store wrote it as is. So r c pvc-mix n1 over a definition registered as Pvc-Mix stored pvc-mix, the slug, and the controller counted it. Now the same call stores Pvc-Mix and none of the three places sees it. The tiebreaker logic counts zero replicas for that definition and resize-pending is never stamped. Reverting line 184 turns ChildrenOfDefinitionUnderAnySpelling red, so the write really lands on the registered spelling.

This path worked before. And because the registered spelling is now the stored form, every mixed-case definition's replicas end up outside these lists. Comparing with store.FoldName on both sides there would fix it, plus a controller test on a definition registered in mixed case.

The satellite has the same shape for nodes at pkg/satellite/controllers/resource.go:164. A node registered as Node-1 with a satellite running NODE_NAME=node-1 used to apply r c pvc node-1 and now skips it, because the replica is stored as Node-1. Autoplace already wrote the registered spelling there, so that setup was half broken before. I'd still fold that compare in the same change.

B2: new test files without the SPDX header

internal/cli/layer_list_case_test.go:1, pkg/rest/layer_stack_case_test.go:1, pkg/satellite/controllers/rd_needs_drbd_case_test.go:1

AGENTS.md asks for // SPDX-License-Identifier: Apache-2.0 on every blockstor-authored Go file. On main the only Go file without it is the generated zz_generated.deepcopy.go. These three start right at package.

Non-blocking follow-ups

  1. Nothing pins the folded spelling in spellingSet (pkg/store/k8s/spellings.go:116). Without add(store.FoldName(given)) every new test still passes, the envtest one too, because Create always writes the registered spelling. It has a real writer: the autosnapshot controller creates snapshots with ResourceDefinitionName: rd.Name (internal/controller/autosnapshot_controller.go:298). A conformance case that writes a child CRD directly under the slug next to a mixed-case definition would pin it.
  2. Nothing pins the spelling filter in the exhaustive fallback either (pkg/store/k8s/snapshots.go:562). With == rdName there the package stays green, because envtest always serves the field selector and the fallback never runs.
  3. rgNeedsRebalance compares stacks with slices.Equal (pkg/rest/resource_groups.go:416). An RG stored as drbd,storage before this change gets one extra rebalance of all its children on the first rg modify that sends the same stack, since the patch now comes in canonical. Harmless, but a case-insensitive set compare would avoid it.
  4. Rows already stored in a fourth spelling, like a pre-PR r c Pvc-X over pvc-x or a hand-applied CRD, stay invisible to the listings and so to the rd d snapshot gate. The body says so, and I read that as the accepted limit. If it ever needs closing, the CEL rule already guarantees metadata.name is the folded <rd>.<node>, which a field index could use instead of the spec value.
  5. "Stacked on #190" in the body is out of date: #190 is merged and the base is main.

Comment thread pkg/store/k8s/resources.go Outdated
// The replica is stored under its definition's registered spelling, so
// a listing of the definition finds it whatever spelling it is asked in.
canonical := *in
canonical.Name = canonicalRDName(ctx, s.c, in.Name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With this line r c pvc-mix n1 over a definition registered as Pvc-Mix stores Pvc-Mix instead of the slug. The controller's replica lists (listReplicasDirect, stampResizePending, hostingNodesForRD) compare spec.resourceDefinitionName exactly with rd.Name, so they stop seeing this replica. See B1.

@@ -0,0 +1,22 @@
package cli

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This file needs // SPDX-License-Identifier: Apache-2.0 at the top, like every other Go file in the repo (see B2).

@@ -0,0 +1,193 @@
package rest

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This file needs // SPDX-License-Identifier: Apache-2.0 at the top, like every other Go file in the repo (see B2).

@@ -0,0 +1,25 @@
package controllers

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This file needs // SPDX-License-Identifier: Apache-2.0 at the top, like every other Go file in the repo (see B2).

@kvaps

Copy link
Copy Markdown
Member Author

Thanks, head f64cf3b.

B1: every exact compare of a replica's definition name now folds both sides the way the store addresses the definition: the three you named, plus the sibling fan-out, the node-id scan, the snapshot timeout, and the replica-event mapper, which enqueued Pvc-Mix and so never reconciled the CR stored as pvc-mix. On the satellite, every "is this node me" compare folds too: the event predicate, the reconcile check, the local-replica lookups, the metadata backfill, the pool and physical-device paths, and both sweepers, which would otherwise have taken a Node-1 replica on node-1 for an orphan. TestControllerSeesReplicasOfAMixedCaseDefinition, TestReplicaEventEnqueuesTheFoldedDefinition and TestSatelliteTakesItsReplicaInAnySpelling pin them; reverting each helper turns its test red.

B2: the three files carry the license header.

Follow-ups 1 to 3 are in: an envtest case writes a snapshot and a replica straight under the folded name next to a mixed-case definition, a fake-client case runs the scan fallback, and rgNeedsRebalance compares stacks without case. Each one is red with its guard removed. The fourth spelling stays the documented limit.

Locally the integration run failed only on TestGroupG/SnapCreateListDelete and TestGroupFRToggleDiskless2Diskful, both 30s waits. On a rerun the first failed once in three on this head and once in three on main, and the second passed three times.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Andrei Kvapil (@kvaps) NOT LGTM. The compares from B1 now fold, and the license headers and follow-ups 1 to 3 are in, each pinned by a test. But the controller and the satellite also look a replica's definition up by spec.resourceDefinitionName as stored. With the registered spelling there, a replica of a mixed-case definition is never applied, and the controller deletes it as an orphan.

I tested f64cf3b on a trial merge with main at 7bb6606, which now carries #206. pkg/store/..., pkg/validate, internal/cli, internal/controller and pkg/satellite/... are green with envtest, and pkg/rest fails only on the 2s shutdown flake. The two red checks are not this diff. The tiebreaker test TestGroupFRToggleDiskful2DisklessReapsTieBreaker failed the same way on three unrelated branches (runs 40139309268, 40136179262, 40125497272), and it uses the lowercase td-tb, where every new fold is a no-op. recovery-deleting-convert failed on #216's run 40477975069.

Last round, point by point. B1 as written (the compares) is fixed: reverting isReplicaOf, the enqueue fold or isNode turns its test red. B2 is fixed. Follow-ups 1, 2 and 3 are fixed, and each goes red with its guard reverted. 4 stays the documented limit. 5 is fixed in the body.

Blockers

B1: lookups keyed by the stored spelling miss the definition

pkg/satellite/controllers/resource.go:440, internal/controller/resource_controller.go:285

Every replica is now stored under its definition's registered spelling, Pvc-Mix, and the CR is pvc-mix. Two Gets use the spec value as the CR key:

  • The satellite's runApply does r.Get(ctx, client.ObjectKey{Name: res.Spec.ResourceDefinitionName}, &rd). On NotFound it returns ctrl.Result{}, nil, without a requeue, so the replica is never applied.
  • The controller's handleOrphan does the same Get, reads NotFound as "parent deleted" and calls Delete once resourceWasApplied is true. The satellite makes it true by stamping its finalizer before it reaches runApply.

I checked both with a fake client: RD pvc-mix, a replica spelled Pvc-Mix with the satellite finalizer. handleOrphan returns orphaned=true and the replica gets a deletion timestamp. The satellite's Reconcile returns an empty result at that Get, while the same replica spelled pvc-mix goes on into prepareDRBDApply. Before this PR, r c pvc-mix n1 over Pvc-Mix stored pvc-mix (res.Name = rdName from the URL) and worked. Now every replica of a mixed-case definition takes this path, autoplace included. lookupVolumeNumbers (resource.go:1541) has the same Get on the delete path.

The satellite also still compares the definition name exactly. enqueueResourcesForRD (resource.go:422) compares with rd.Name, so an RD edit such as a resize never reaches a local Pvc-Mix replica. listPeerResources (:1164), adoptAuthoritativeFlags (:1195) and enqueueLocalSiblings (:1041) split a definition whose replicas carry two spellings, such as a pre-PR pvc-mix row next to a new Pvc-Mix one.

Main now has one more of these. #206 landed after this head and adds rdHoldsData (internal/controller/resourcedefinition_layer_stack.go:228) with the same exact compare. It returns false for a Pvc-Mix replica with MetadataCreated true. Today that is hidden only because such a replica never reaches disk. Once the Get is fixed, the pin's "already holds data" check would miss it.

Names on disk would also change. Once applied, the satellite names the DRBD resource and the volumes from the spec value (pkg/dispatcher/dispatcher.go:267, then <pool>/<name>_00000). A Pvc-Mix replica would get Pvc-Mix_00000, while #205 documents lowercase names on disk and a pre-PR peer has pvc-mix_00000.

Since this many readers key on the CR name, I'd store the CR name (k8s.Name) in spec.resourceDefinitionName, which the listing already asks under, and map back to the registered spelling only for display. Folding every reader also works, but the list above is what it takes, and the b618d42 message ("Every such compare now folds both sides") has to be true afterwards. Either way I'd want a test that runs a mixed-case replica through handleOrphan and the satellite's apply entry.

Non-blocking follow-ups

  1. Nothing pins sameDefinition (internal/controller/replica_of.go:38). With a == b there internal/controller stays green with envtest. It feeds the peer list in runApply, collectTakenNodeIDs, requestsForRD and siblingReplicasUpToDate.
  2. Node names have the same shape on the controller side. takenPortsOnNode (resource_controller.go:1859) and the projection's enqueueResourcesForNode (resource_state_projection_controller.go:271) compare exactly, and the projection Gets the Node by spec.nodeName (:122), so a replica stored as Node-1 is never projected. r c pvc node-1 over Node-1 used to store node-1.
  3. The f64cf3b body ends with "The new test files carry the license header.", which describes a fix to earlier commits of this PR. With squash it lands in main and means nothing there.
  4. This conflicts with #200 in pkg/store/k8s/snapshots.go.

Comment thread pkg/store/k8s/resources.go Outdated
// The replica is stored under its definition's registered spelling, so
// a listing of the definition finds it whatever spelling it is asked in.
canonical := *in
canonical.Name = canonicalRDName(ctx, s.c, in.Name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With this spelling in spec, the satellite's runApply Get (pkg/satellite/controllers/resource.go:440) and the controller's handleOrphan Get (internal/controller/resource_controller.go:285) both answer NotFound for a Pvc-Mix replica. The satellite never applies it, and the controller deletes it as an orphan once the finalizer is on. See B1.

Comment thread pkg/satellite/controllers/resource.go Outdated
@@ -420,7 +420,7 @@ func (r *ResourceReconciler) enqueueResourcesForRD(ctx context.Context, obj clie
for i := range localList.Items {
local := &localList.Items[i]
if local.Spec.ResourceDefinitionName == rd.Name &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

rd.Name is the CR name, pvc-mix, and a replica written now carries Pvc-Mix, so an RD edit never enqueues the local replica. This compare needs the same fold as the node one below it.

@kvaps

Copy link
Copy Markdown
Member Author

Thanks, head 8423f5d, rebased on main with #206 and #219.

B1: went with your suggestion. Resources().Create and Snapshots().Create now store the CR names in spec.resourceDefinitionName and spec.nodeName, and the registered spelling goes to blockstor.io/linstor-resource-definition-name / blockstor.io/linstor-node-name, read back only for the wire. Update and Patch keep the spec names. So runApply, handleOrphan, lookupVolumeNumbers, rdHoldsData and the projection's node Get meet the CR again, and the DRBD resource and volumes stay pvc-mix / pvc-mix_00000. The folds stay for rows stored before this or adopted from LINSTOR. linstor-migrate wrote the display spelling into spec the same way, it now writes what the store writes.

Pinned by TestOrphanCheckFindsTheMixedCaseDefinition, TestSatelliteAppliesAReplicaOfAMixedCaseDefinition, TestMixedCaseReplicaKeepsLowercaseNamesOnDisk and TestAdoptedChildrenStoreTheirParentsUnderCRNames.

Follow-ups: 1 is TestSameDefinitionFoldsCase. 2 is TestTakenPortsSeeAReplicaCreatedInAnotherNodeSpelling and TestProjectionFollowsTheNodeOfAMixedCaseReplica. 3: the commits were rewritten, none of them talks about earlier rounds now. 4: I'll resolve it on whichever of #200 and #217 lands second.

TestGroupKWFHappyPath failed 4 of 12 local runs on this branch and 2 of 15 on main, the failures are the RG default rebalance, not names. Not sure the difference is real with that few runs, so tell me if you see it in CI.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Andrei Kvapil (@kvaps) NOT LGTM. Storing the parents' CR names in spec fixes B1: the orphan check and the satellite apply now find a mixed-case definition, and the DRBD and volume names stay lowercase. But one reader outside the diff depended on the old spelling. The dispatcher finds a peer's address by the node's registered name, so a peer on a node registered as Node-2 now gets 0.0.0.0.

I tested 8423f5d. The branch already sits on main at ffd7b90, so the head is the trial merge. pkg/store/..., pkg/validate, internal/cli/..., internal/controller, pkg/satellite/..., pkg/linstormigrate and pkg/dispatcher are green with envtest, and pkg/rest fails only on the 2s shutdown flake (TestBug145ResourceCreateIsAtomic). The red E2E lane 3 is recovery-deleting-convert (exit 2). It failed the same way on run 40477975069 for fix/create-spawn-prop-fields, and the scenario uses only lowercase names, where every new fold is a no-op.

Last round, point by point. B1 is fixed for every row the store and the migrator write: putting the display spelling back into spec turns TestOrphanCheckFindsTheMixedCaseDefinition, TestTakenPortsSeeAReplicaCreatedInAnotherNodeSpelling and TestProjectionFollowsTheNodeOfAMixedCaseReplica red. The satellite's exact compares now meet the CR name they expect, and the lowercase DRBD names are pinned. Follow-up 1 is fixed: a == b in sameDefinition reddens TestSameDefinitionFoldsCase. 2 is fixed with tests, 3 went away with the rewrite, and 4 still stands: this conflicts with #200 in pkg/store/k8s/snapshots.go.

Blocker

B1: a peer on a node registered in another spelling loses its address

pkg/dispatcher/dispatcher.go:628, :644, :1055, :611

lookupPrefNic, lookupNetInterfaceAddress and lookupEndpoint match the node name from the replica's spec against k8s.OriginalName of the Node. The comment on lookupEndpoint says this is on purpose, so that non-RFC1123 names resolve. That held while spec carried the registered spelling. Now it carries the CR name, and the match fails for every node whose registered name differs from its CR name. The pool loop at :611 compares the pool's spec.nodeName exactly, so a PrefNic on the pool is missed too.

I ran BuildDesired over two replicas created through the store, with a SatelliteEndpoint on each node:

registered node main this PR
node-2 peer.node-2.address = 192.0.2.2 same
Node-2 peer.Node-2.address = 192.0.2.2 peer.node-2.address = 0.0.0.0
my_node peer.my_node.address = 192.0.2.2 peer.e74a1c5e-my-node.address = 0.0.0.0

0.0.0.0 is the placeholder the satellite replaces only for its own address. A peer's address goes into the .res as is (pkg/satellite/reconciler.go:5610), so the connection points nowhere. Comparing k8s.Name(nodeName) with nodes[i].Name, and folding the pool compare the way isNode does, would match both old and new rows. TestMixedCaseReplicaKeepsLowercaseNamesOnDisk passes nil nodes to BuildDesired, so it can't catch this; a test with a peer on Node-2 would.

Non-blocking

  1. rdHoldsData (internal/controller/resourcedefinition_layer_stack.go:228, from #206) is the one controller compare of a definition name that stays exact, while 8e6e4b2 folds the others for rows adopted or written before. A replica spelled Pvc-Mix with MetadataCreated true and no Initialized latch makes stackToPin return the group's stack; spelled pvc-mix, the default. I don't see such a row coming up in practice, since an RD with adopted data is latched, but isReplicaOf there is one line.
  2. 8e6e4b2 says "a replica adopted from LINSTOR keeps the display spellings", and so do the comments on isReplicaOf, isNode and TestSameDefinitionFoldsCase. After 8423f5d that holds only for replicas adopted before this change, and both messages land in main with the squash.
  3. Two guards are not pinned. Dropping the two new keys from mergeUserAnnotationsInto in Update stays green, because the test sends nil annotations and the merge returns early; a REST update carrying any annotation would lose the spelling without them. Dropping add(found.display) from spellingSet also stays green, though a826b50 names the registered spelling as what still finds older rows.
  4. The PR body still says the store "stores new replicas under their definition's and their node's registered spelling". The commits now do the reverse.
  5. A name outside RFC 1123 that linstorName accepts, like my_res, now gets the hashed slug in spec, and so in the DRBD and volume names. Such definitions were never applied before, so nothing regresses, but docs/linstor-migration.md:48 and the migration page in #205 say "lowercase".
  6. #221 moves the blockstor.io/ keys to blockstor.org/; the two annotations added here need to go with them.


crd := wireToCRDResource(&shown)
crd.Spec.ResourceDefinitionName = rd.crName
crd.Spec.NodeName = node.crName

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is where the node's CR name goes into spec. The dispatcher still matches a peer's Node by its registered spelling (pkg/dispatcher/dispatcher.go:628, :644, :1055), so a peer on Node-2 or my_node now renders with address 0.0.0.0. See B1.

@kvaps

Copy link
Copy Markdown
Member Author

Rebased on main after #200, head 9d15b16. The only conflict was the import block in pkg/store/k8s/snapshots.go. PatchSnapshotProps from #200 rewrites only the props of the CR it read, so the CR name in spec and the spelling annotation stay as Create wrote them, and #200's own name compares already go through EqualFold.

@kvaps

Copy link
Copy Markdown
Member Author

Thanks, head 0158d61, on main after #200.

B1: the dispatcher's endpoint, PrefNic and NetInterface lookups now match k8s.Name(spec.nodeName) against the Node's CR name, and still the registered spelling in any case for older rows. The pool compare folds too. TestPeerAddressResolvesUnderEverySpellingOfItsNode covers node-2, Node-2 and my_node, each with the CR name and the registered spelling in spec, and TestPeerPrefNicResolvesUnderTheCRNameOfItsNode covers the pool PrefNic. Both go red with the old compares.

Non-blocking:

  1. rdHoldsData uses isReplicaOf now, pinned by a Pvc-Pin case in TestPinLayerStackKeepsTheAppliedStackOfAnRDThatHoldsData.
  2. The message and the comments say a replica written or adopted before this may carry the display spelling.
  3. Both pinned: the store tests send a user annotation on Update and Patch for replicas and snapshots, and TestARowStoredUnderTheRegisteredSpellingIsFound lists a Pvc-Reg row as PVC-REG. Each goes red with its guard dropped.
  4. Body fixed.
  5. Both migration pages say the on-disk name is the CR name now, lowercase or a hashed slug like 35d21682-my-res for my_res.
  6. Will move them with the rest in refactor(api)!: move the API group to blockstor.org #221.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Andrei Kvapil (@kvaps) NOT LGTM. The dispatcher now finds a peer's node under both spellings, so last round's blocker is fixed. The same change reaches four more readers, though. Each of them compares a replica's spec name, now the CR name, with a name in the registered spelling, and none of them is in the diff.

I tested 0158d61. It sits on main at 536e935, and git merge-tree with main gives the head's own tree, so the head is the trial merge and the #200 conflict is gone. internal/cli/..., internal/controller, pkg/dispatcher, pkg/linstormigrate, pkg/satellite/controllers, pkg/store/... and pkg/validate are green with envtest. pkg/rest fails only with "server did not stop within 2s after cancel". go vet is clean. In CI only Integration tests is red, on two tests. TestGroupG/SnapCreateListDelete fails the same way on fix/unclean-al-apply (run 41438685172). TestGroupKWFPoolDestroyedDropsFromPlacer fails with the same line 473 message on feat/site 2a29a55 (run 41437867460), and that branch changes no placer code.

Last round, point by point:

  1. B1 is fixed. Going back to an exact OriginalName compare in isNodeCalled gives 0.0.0.0 in both new dispatcher tests, and a == b in sameNodeName turns the PrefNic test red.
  2. Follow-up 1 is fixed. An exact compare in rdHoldsData makes the Pvc-Pin subtest pin the group's stack.
  3. Follow-ups 2, 4 and 5 are fixed. 35d21682-my-res is what k8s.Name("my_res") returns.
  4. Follow-up 3 is fixed. Dropping the two keys from the resource Update merge reddens TestReplicaStoresTheCRNamesOfItsParents, dropping the RD key from the snapshot merge reddens TestSnapshotStoresTheCRNameOfItsDefinition, and dropping add(found.display) reddens TestARowStoredUnderTheRegisteredSpellingIsFound.
  5. Follow-up 6 is still open. #221 merges #217 as of 6e36a49, so the two new annotations are not in it yet.

Blocker

B1: four controller readers still compare the CR name with the registered spelling

The REST surface shows a replica's node in the registered spelling. Snapshot.Spec.Nodes and the wire listings carry that spelling, and so does a storage pool. A replica's spec now carries the CR name. I checked each place below with a fake client stand built by mixedCaseStand, a node registered as Node-1:

place what I ran result
snapshot_timeout.go:339 replica created through the store on Node-1, disk state Outdated, Spec.Nodes from the wire listing wire Node-1, spec node-1, siblingReplicasUpToDate returns true
resource_controller.go:486 pool created through the store on Node-1, firstAvailablePool with the replica's spec node ""
resource_controller.go:1859 older replica with nodeName: Node-1 and port 7001, as main writes it for r c pvc-old Node-1, new replica on Node-1 takenPortsOnNode returns []
node_controller.go:122 replicas created as node-1 and as Node-1 both list as Node-1, node.Name is node-1

The first one matters most. That gate is the one that refuses to snapshot a replica that is not UpToDate (snapshot_controller.go:305 and :459). On a node registered as Node-1 or my_node it now skips every replica, so the snapshot goes ahead. The second leaves an in-use diskless replica on such a node diskless for good. The third hands a new replica a port that an older replica on the same node already holds. TestTakenPortsSeeAReplicaCreatedInAnotherNodeSpelling seeds the older row as node-1, a spelling that already matched. The fourth makes evacuate and lost on such a node migrate nothing. On main a replica created as node-1 matched there.

Comparing k8s.Name of both sides, the way isNode does on the satellite, would cover all four, old rows included.

Non-blocking

  1. Nothing pins the second clause of isNodeCalled. Without || strings.EqualFold(k8s.OriginalName(...), name) both dispatcher tests stay green, because k8s.Name already maps every spelling in the table to the CR name. The clause only matters for a case variant of a name outside RFC 1123: k8s.Name("my_node") is e74a1c5e-my-node, k8s.Name("My_Node") is fefe7665-my-node. A case with a peer stored as My_Node on a node registered as my_node would pin it.
  2. Outside this diff: since that hash is case-sensitive, My_Res and my_res become two CRs, while the body opens with "the store addresses one object through the folded CRD slug".

@kvaps

Copy link
Copy Markdown
Member Author

Thanks, head bc5dc14, rebased on main with #222 and #224.

B1: the controller has one sameNode now, which folds both sides and also matches two case variants of a name outside RFC 1123. It is used in the snapshot gate, firstAvailablePool in both the resource and the auto-diskful controller, takenPortsOnNode, evacuation in the node controller, and the projection's enqueueResourcesForNode. Tests, each red with its compare back to exact:

  • TestSnapshotGateSeesAReplicaUnderItsNodesCRName: an Outdated replica stored as node-1, Spec.Nodes Node-1.
  • TestFirstAvailablePoolMatchesTheReplicasCRNodeName: a pool created on Node-1, both controllers.
  • TestTakenPortsSeeAReplicaCreatedInAnotherNodeSpelling seeds the older row as Node-1 now.
  • TestEvacuationReachesReplicasInEverySpellingOfTheNode: replicas created as NODE-1 and node-1.
  • TestProjectionReachesAReplicaStoredInTheRegisteredSpelling: an older row with Node-1 in spec.

Other compares I went through and left: the REST and CLI ones (node_lifecycle.go, autoplace.go, peer_delete_sync.go, storage_pools.go, validate/resource_delete.go, internal/cli) compare wire against wire, both in the registered spelling. The dispatcher's own peer loop at :97 and :122 compares replicas of one definition, and one node cannot appear there twice since the CR name folds the pair. The satellite's CR to CR compares in resource.go are unchanged. Snapshot.Spec.Nodes and the snapshot status entries against the satellite's own node name (snapshot.go, snapshot_fetcher.go, hasNodeStatusEntry) and findStoragePoolByName don't touch anything this PR writes, so I kept them out.

Non-blocking 1: a peer stored as My_Node on a node registered as my_node is in the dispatcher table, and TestSameNodeFoldsCase has the same pair. Both go red without the case-insensitive clause.

Non-blocking 2: right, the hash keeps case, so My_Res and my_res are two CRs. That predates this PR and I left it alone; the body's first sentence says so now.

The two annotations will move to blockstor.org/ with #221.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Andrei Kvapil (@kvaps) NOT LGTM. The five readers from the last round are fixed and pinned. A sweep of the rest of the tree still finds readers that compare a node name exactly, and two of the paths this commit fixes now reach the replica but miss it one step later.

I tested bc5dc14. It sits on main at e49b274, with #222 and #224, and git merge-tree with main gives the head's own tree. internal/cli/..., internal/controller, pkg/dispatcher, pkg/linstormigrate, pkg/satellite/..., pkg/store/... and pkg/validate are green with envtest. pkg/rest fails only with "server did not stop within 2s after cancel", and go vet is clean. In CI the six E2E lanes, Lint, Unit and Contract tests are green. Integration tests is red on TestGroupFRToggleDiskful2DisklessReapsTieBreaker, "diskful replica count never reached 2" at group_f_test.go:286. That test did not fail on another branch in the last 60 runs. The same helper times out with the same message in TestGroupFRAutoPlace2ReachesUpToDate on fix/luks-passphrase-secret-only 71cc110 (run 41509123263) and in TestGroupFRAutoPlace3WithTieBreaker on feat/site 2a29a55 (run 41437867460), so I don't count it against this PR.

Last round, point by point:

  1. B1 is fixed, for the four places I named and the fifth you found. Reverting each call site to the exact compare reddens its new test, and dropping either half of sameNode reddens TestSameNodeFoldsCase. The ports test now seeds the old row as Node-1.
  2. Non-blocking 1 is fixed. Without the EqualFold clause of isNodeCalled the new My_Node case fails.
  3. Non-blocking 2 is fixed. The body now says My_Res and my_res are two objects.

Blocker

B1: some readers still compare a node name exactly

The setup is the one isNode describes: a node registered as Node-1 whose satellite runs as node-1. I ran each row on the head with a fake client stand, mixedCaseStand or the same objects built by hand.

place what I ran result
node_controller.go:451, resourceCRDName is rd + "." + node, used by pruneSource and replicaUpToDate replica created as node-1 and as Node-1, victim taken from Store.Resources().List() the way migrateResource gets it victim is Pvc-Mix/Node-1, the Get is NotFound, pruneSource returns nil and deletes nothing, replicaUpToDate is false
resource_state_projection_controller.go:122, Get of the node by res.Spec.NodeName the old row from TestProjectionReachesAReplicaStoredInTheRegisteredSpelling, node offline, Reconcile on it no status patch; a row with node-1 in spec gets Unknown
satellite snapshot.go:90 and :740, controller allTargetsMatch replica created through the store as node-1, ListByDefinition wire node is Node-1 here and node-1 on main
pkg/rest/node_lifecycle.go:137, the evacuate in-use guard in-use replica created as node-1, POST /v1/nodes/{node}/evacuate node-1 gets 200 and EVICTED, Node-1 gets 409; main is the other way round
pkg/store/k8s/storage_pools.go:461 and :482 pool created on node-1, StoragePools().Get("Node-1", "pool") not found

The first row is the evacuate and lost path this commit fixes. The node controller now reaches every replica on the node. With LOST the source replica is still never deleted, and with EVICTED the replacement never counts as UpToDate, so the drain never ends. TestEvacuationReachesReplicasInEverySpellingOfTheNode stops at the no-group annotation and never gets to pruneSource. Building the name the way the store's resourceCRDName does would close it.

The second row is the same shape. The node event now enqueues the old row, and the reconcile then drops it. Its test asserts only the enqueue. k8s.Name(res.Spec.NodeName) in that Get closes it.

The third row breaks snapshots on such a node. CSI sends no nodes, so listDiskfulNodes fills Spec.Nodes from the wire listing. On main that list carried the spelling the replica was created with. Here it is always the registered one. The satellite compares it with its own NODE_NAME exactly and skips the snapshot, and the controller looks the per-node status up by the same value. Storing Spec.Nodes as CR names, like the replica spec, would fix both sides.

The last two were exact before too, and this PR only changes which spelling misses. The evacuate guard is what keeps an in-use volume from being drained, and now the Kubernetes node name is the spelling that bypasses it. Restore, autoplace and the placer pass the replica's wire node into the pool Get.

Non-blocking

  1. sameNodeName in the dispatcher is the same function as sameNode, and nothing pins its EqualFold half. Replacing it with a == b leaves pkg/dispatcher green.
  2. The site PR #205 conflicts with this one in step 5 of site/content/en/docs/migration.md. It still says <definition name, lowercase>, which this PR makes untrue for names outside RFC 1123. #226 conflicts in docs/linstor-migration.md. #221 does not merge with main yet and still has the old spellings.go without the two annotations.
  3. One more exact compare I only read: the DRBD path lookup in pkg/drbd/conffile.go:636. Path keys keep the operator's spelling and the local host is the satellite's NODE_NAME, so it already missed on main for such a node. It needs the same fold.

What I checked and left alone. The dispatcher maps idOf, byName and peerByName, and the satellite's adoptAuthoritativeFlags and peersMissingNodeID, key and look up by spec values of one definition's replicas. The CRD admits one object per definition and node, so those agree. linstormigrate/convert.go compares rows of one LINSTOR dump with each other. handleOrphan, runApply, rdAllocMu and the other Gets by Spec.ResourceDefinitionName miss an old row stored as Pvc-Mix. On main the satellite's Get of Pvc-Mix was NotFound too, so such a replica was never applied and holds no data. The REST and CLI compares between two wire values from one listing agree with each other.

The snapshot success check and the restore pool map folded node names
to lowercase. My_Node and my_node hash to two CR names and are two
nodes, so a snapshot reported on one read as taken on both, and an
entry reported under a node's CR name matched no registered node.
They now key by the CR name, the way the store addresses a node.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
…kful paths

A replica names its node by the CR name or by the spelling it was
registered under, while the node list shows the registered one. The
disabled and known sets and the witness exclusions compared the two
exactly, so a witness on an EVICTED node registered as Node-1 was
read as live and never moved, and auto-diskful counted a replica on
an evicted node. The direct node probe folded case instead and read
My_Node and my_node as one node.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
…y CR name

The disabled-node set is keyed by CR name, but the additional replica
count looked a replica's node up as the wire spells it. A replica on
an EVICTED node registered as Node-1 was counted as live, so +1 asked
for one replica too many. The error-report node filter folded case
and merged My_Node and my_node; it now compares CR names too.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
A replica written before the store kept CR names in spec names its
definition Pvc-Mix. Now that the satellite finds such a definition,
it laid the resource down under the raw spec value while snapshot
requests, and every replica written since, use the CR name pvc-mix:
the snapshot failed with "resource not known on this satellite". The
intent, teardown, snapshot phases, orphan sweepers, the derived port
and minor, the day0 generation check and a restore's source now all
use the CR name. A row that already stores it is unaffected.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The kernel and the apply chain know a resource by its definition's CR
name, a hash for a name outside RFC 1123, while the store names the
replica's Resource CR, and a Snapshot CR, from the spellings the
definition and the node were registered under. The observer, the
SkipDisk clearer, the cross-node snapshot fetcher and the
MetadataCreated and FilesystemFormatted stampers built the CR name
from the kernel name, so for such a definition status, KernelLoaded,
SkipDisk and the conditions were never written. They now map back
through the definition and the node, the way the store named the CR.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
An object is stored under the CR name its LINSTOR name maps to: the
lowercase name inside RFC 1123, a hash of the name as given otherwise.
That function lived in the Kubernetes store, so code above the store
reached for it, or folded case itself, which is not the same thing for
a name outside RFC 1123. pkg/naming now holds the key and Same, and
k8s.Name delegates to it.

Key is also made idempotent on every key it returns: a name long
enough to be cut could end on a dash, which no stored object can carry
and which Key hashed a second time.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Every controller, REST handler, the satellite, the dispatcher, the
placer and the CLI keyed and compared names through k8s.Name and
k8s.SameName, reaching into the store backend for it. They now use
naming.Key and naming.Same, the same function, so nothing changes in
behaviour; the next change removes the per-site helpers and the case
folding that sit beside them.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Code compared two spellings of a name as they were, or folded case where
the store hashes, in more places than any single fix reached. They came
from one habit: every site decided for itself how to compare a node,
definition, group, pool or snapshot name. Above the store they are now
compared with naming.Same and used as map keys through naming.Key, and
nothing else.

That removes the per-site helpers (sameNode, sameDefinition,
sameNodeName, isNodeCalled, isNode, listsNode, nodeKey,
nodeCRDMatchesWireName, k8s.SameName, store.FoldName) and fixes the
sites that still mixed spellings, among them:

- the evacuation drain target counted a replica on a LOST or EVICTED
  node when its row spelled the node differently from the node list;
- the placer's min-rsc-count weight read a pool's node in the pool's
  spelling against counts keyed by the replicas' spelling;
- list filters, restore markers, group and pool names folded case,
  which joins two definitions such as My_Res and my_res that the store
  keeps apart; the in-memory store did the same and now keys like the
  Kubernetes one.

The dispatcher keys peers by node and still emits the spelling a peer's
row stores, so no DRBD connection of a running resource is renamed.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
A test walks the Go sources under pkg, internal and cmd and fails on
case folding applied to an object name, on comparing a reference field
or variable (a replica's node or definition, a definition's group, a
pool, a snapshot's nodes) with another name as it is, on slices.Contains
over one, and on a map keyed by one, unless naming.Key or naming.Same
is on the path. Store backends, the LINSTOR dump converter and the
kernel side are out of reach, each with its reason; the few sites that
match and are right as they are carry theirs in the allowlist, which
also fails when an entry goes stale.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Code that reads a definition or node by the name a row stores, or by
the name a satellite was started with, passed that name straight to
the API server as metadata.name. A row written in another spelling, or
a satellite started as Node-1, then read nothing: the controller took
a replica's definition as missing, and the heartbeat stamped no Node.
These lookups now go through naming.Key, and the naming guard flags a
reference used as the Name of an ObjectKey, NamespacedName or
ObjectMeta.

The satellite's own name is keyed only where it addresses its Node, so
the name it renders into DRBD configuration is unchanged.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
…ing key

The satellite's cold start keyed the replicas it owns by the name the
store shows, the registered spelling, and looked kernel resources up
under their own name, the definition's key. A resource of a definition
registered as PVC-X read as another DRBD user's and was left up. The
CLI's pool filter compared names as given. Both go through naming now,
and the naming guard reads a replica's Name as the reference it is.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The filter comments said "case-insensitive", which the filters still
are for every name inside RFC 1123; outside it two case variants are
two objects, and the filters now keep them apart.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The self-migration guard compared the two node arguments as given, so
`r td node-1 pvc-x --migrate-from Node-1` passed it. The store resolves
both to one replica, the command marks that replica as migrating from
itself, and the reconcile then prunes the source: the data loss the
guard exists to refuse. The ends are compared with naming.Same.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The list filter compared node names as given, and the pair key was built
from the names as given, so `node-connection list node-1` missed a pair
written for Node-1, and a write that spelled the node node-1 started a
second pair beside it. Lookups now match a stored pair whose nodes are
naming.Same in either order, which also finds pairs written before; a
new pair keeps the caller's spellings, ordered by key.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The in-memory store kept definitions, groups, nodes, replicas,
snapshots, pools and volume definitions under the name as given, while
the Kubernetes store resolves any spelling to one CR. A test over the
in-memory store could pass code that hands a non-canonical spelling to
Get or Create. It now keys every map by naming.Key, and conformance
cases pin the shared behaviour: an object created as pvc-x is found as
Pvc-X and refuses a second create as Pvc-X, on both backends.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Such a name keyed to its hash followed by a dash, which is not a name
the API server takes and which Key hashed a second time. The dash is
left out when there is no slug. No stored object can carry the old
form, so no existing name changes.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The placer looked an existing replica's pool up by the node as the
replica shows it, in a map filled by the node as the pool listing
shows it. When the two spellings differed the pool was not found, so
a new replica could land on a pool sharing the existing replica's
LUN, and the provider-kind constraint started from nothing.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Paths were stored under a prop named by the two nodes as the caller
spelled them, so posting a path for Node-1/beta and another for
node-1/beta opened two entries for one pair, and the dispatcher handed
the satellite two connections between the same two hosts. A read or
delete in the other spelling missed the stored paths. The pair's prop
is now found by key, and a new one is ordered by key.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The wait asked for an annotation named after the removed node as the
request spelled it, so an acknowledgment stamped for node-1 was never
seen for Node-1 and the delete sat out the full timeout. An annotation
key cannot carry every spelling, so the peer is named by its key.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Every caller passes the definition's CR name, for which this is the
same comparison; it no longer depends on that.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The guard matched object names by what variables were called, so a
raw comparison under any other name went through: migrate-disk's
src == dst and the node-connection filter were two. It now type-checks
the tree and follows a name from where it enters, the name fields of
the wire and CRD types, the path parameters, query lists and CLI flags
that carry names, and whatever is passed to the store as a name,
through assignments, ranges, calls and returns, to the comparison, map
key, search, case fold or CR address it reaches. A planted package
checks every shape under arbitrary names, and the shapes it must
leave alone.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The in-memory store keyed a replica or snapshot by the keys of its two
names apart, while the Kubernetes store names the CR by the key of the
two joined. Outside RFC 1123 those differ: the joined key hashes the
whole, so a test over the in-memory store could pass code that the
Kubernetes store would send to another object. The in-memory store now
uses the CR name, a conformance case pins the RFC 1123 spellings on
both stores, and an in-memory test pins the hashed case the CRDs keep
off a cluster.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The guard saw a raw name comparison only in the shapes it knew: a key
built by Sprintf or inline concatenation, a name copied through a struct
field, a switch case, a closure, a call through an interface, a prefix
test or a map literal went through. It now also follows names through
Sprintf, Join and inline concatenation into map keys, string fields of
local structs, closures bound to a variable or called in place, the
function a slices.*Func call runs, methods of this module's interfaces,
append, the keys of a map keyed by a name, and strings.Cut; and reports
switch cases, map literal keys and prefix or substring tests between
names. A parameter a helper returns stands for what each call passes, so
a helper given a name taints only its own callers. Two reads of one
field count as one source by where they come from, one collection or a
sort's comparator, not by what the variables are called. The doc comment
lists what it follows and what it cannot see.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
A migration marker names its source node as given. A marker spelled
Node-1 on the replica on node-1 names that replica itself, and once it
reached UpToDate the controller deleted it as the source: the only
copy the migration made. The controller now drops such a marker and
leaves the replica, pruneSrc refuses to delete the replica it was
handed as the destination, and the REST migrate-disk path refuses one
node in two spellings before stamping anything.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Snapshot status can hold entries for one node under two spellings,
Node-1 and node-1, written before node names were compared by key. The
satellite updated the first of them while the controller, which reads
one entry per node, kept the last, so a suspend acknowledgment the
satellite wrote was never seen and the snapshot stalled. Every status
write now folds the node's entries into one, keeping what any of them
recorded.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The removed-peer diff compared the peers a .res file was rendered
with against the peers the new intent names, both as written. A peer
spelled Node-2 in the file and node-2 in the intent read as gone, and
the satellite ran del-peer against a peer that is still there. Both
sides are compared by key now; the names it returns are still the
ones the kernel knows.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
The renderer looked a pair's paths up by comparing the connection's
node names with the host names exactly. The satellite maps the
connection onto the hosts' spelling before rendering, so this held
only as long as that mapping did; matching by key here keeps the
paths of Node-1/beta on the host node-1 without depending on it.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Whole files and directories were out of the guard's reach, so a raw
comparison in the satellite reconciler, between connection names and
host names from another source, went unseen. Exclusions now name the
single functions that need one, with the reason, and the intent and
DRBD types are sources. The guard also reports a map entry deleted by
a raw name, and treats a helper returning naming.Key's result and a
CR's GetName() as keys, so comparing either with a raw name is
reported.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
@kvaps

Copy link
Copy Markdown
Member Author

Fixed. The migration controller drops a marker that names the destination's own node and never prunes the destination, and snapshot status writes fold a node's entries into one, so the satellite and the controller read the same ack. The guard now checks the reconciler and the DRBD renderer per function instead of skipping files, and catches raw delete, keys returned by helpers and GetName(); your hostSpelling and placer mutations turn it red. make lint is clean and the commit messages are about the naming problem only.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

2 participants