Skip to content

Commit 4cae72e

Browse files
kvapsclaude
andcommitted
feat(api): typed Node port/minor ranges + RD AutoTieBreaker (Phase 10.3 steps)
Three more typed-fields migrations replacing string-keyed Props: * Node.Spec.DRBDPortRange / DRBDMinorRange (PortRange { Min, Max int32 }) replace Props["DrbdOptions/TcpPortRange"] / "MinorNrRange". ResourceReconciler's portRangeForNode / minorRangeForNode now resolve via the typed pointer first; legacy "min-max" Props key is the forward-compat fallback. * DRBDResourceOptions.AutoTieBreaker *bool replaces Props["DrbdOptions/AutoAddQuorumTiebreaker"]. The transcoder routes the section-less upstream key into the typed slot via a new applySectionlessKey helper; isAutoTieBreakerEnabled reads typed first, Props fallback. The legacy keys stay in Props on GET (k8s store re-emit) so golinstor and any pre-migration reader keep seeing the same wire shape. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent bec4463 commit 4cae72e

11 files changed

Lines changed: 224 additions & 27 deletions

api/v1alpha1/drbd_options.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ type DRBDResourceOptions struct {
155155
// +kubebuilder:validation:Enum=io-error;suspend-io;freeze-io
156156
// +optional
157157
OnNoQuorum string `json:"onNoQuorum,omitempty"`
158+
159+
// autoTieBreaker gates auto-creation of a DISKLESS+TIE_BREAKER
160+
// witness when a parent RD has an even number of diskful
161+
// replicas and no operator-placed diskless replica. Replaces
162+
// `Props["DrbdOptions/AutoAddQuorumTiebreaker"]`. nil inherits
163+
// the cluster-wide default (true). Phase 10.3.
164+
// +optional
165+
AutoTieBreaker *bool `json:"autoTieBreaker,omitempty"`
158166
}
159167

160168
// EncryptionConfig configures LUKS encryption for a ResourceDefinition.

api/v1alpha1/node_types.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,35 @@ type NodeSpec struct {
5757
// path is gone.
5858
// +optional
5959
SatelliteEndpoint string `json:"satelliteEndpoint,omitempty"`
60+
61+
// drbdPortRange is the inclusive [min, max] TCP port range the
62+
// allocator picks DRBD listen ports from for replicas placed on
63+
// this node. Replaces `Props["DrbdOptions/TcpPortRange"]`. nil
64+
// inherits the controller-wide default (7000–7999). Phase 10.3.
65+
// +optional
66+
DRBDPortRange *PortRange `json:"drbdPortRange,omitempty"`
67+
68+
// drbdMinorRange is the inclusive [min, max] /dev/drbd<N> minor
69+
// range the allocator picks from. Replaces
70+
// `Props["DrbdOptions/MinorNrRange"]`. nil inherits the
71+
// controller-wide default (1000–1099). Phase 10.3.
72+
// +optional
73+
DRBDMinorRange *PortRange `json:"drbdMinorRange,omitempty"`
74+
}
75+
76+
// PortRange is an inclusive [Min, Max] integer range. Used for
77+
// DRBD TCP port ranges and /dev/drbd<N> minor ranges. Empty (nil)
78+
// means "inherit cluster-wide default".
79+
type PortRange struct {
80+
// min is the lower bound (inclusive).
81+
// +kubebuilder:validation:Minimum=0
82+
// +required
83+
Min int32 `json:"min"`
84+
85+
// max is the upper bound (inclusive). Must be ≥ Min.
86+
// +kubebuilder:validation:Minimum=0
87+
// +required
88+
Max int32 `json:"max"`
6089
}
6190

6291
// NodeNetInterface is one advertised endpoint of a satellite.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/blockstor.io.blockstor.io_nodes.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,48 @@ spec:
3939
spec:
4040
description: spec defines the desired state of Node
4141
properties:
42+
drbdMinorRange:
43+
description: |-
44+
drbdMinorRange is the inclusive [min, max] /dev/drbd<N> minor
45+
range the allocator picks from. Replaces
46+
`Props["DrbdOptions/MinorNrRange"]`. nil inherits the
47+
controller-wide default (1000–1099). Phase 10.3.
48+
properties:
49+
max:
50+
description: max is the upper bound (inclusive). Must be ≥ Min.
51+
format: int32
52+
minimum: 0
53+
type: integer
54+
min:
55+
description: min is the lower bound (inclusive).
56+
format: int32
57+
minimum: 0
58+
type: integer
59+
required:
60+
- max
61+
- min
62+
type: object
63+
drbdPortRange:
64+
description: |-
65+
drbdPortRange is the inclusive [min, max] TCP port range the
66+
allocator picks DRBD listen ports from for replicas placed on
67+
this node. Replaces `Props["DrbdOptions/TcpPortRange"]`. nil
68+
inherits the controller-wide default (7000–7999). Phase 10.3.
69+
properties:
70+
max:
71+
description: max is the upper bound (inclusive). Must be ≥ Min.
72+
format: int32
73+
minimum: 0
74+
type: integer
75+
min:
76+
description: min is the lower bound (inclusive).
77+
format: int32
78+
minimum: 0
79+
type: integer
80+
required:
81+
- max
82+
- min
83+
type: object
4284
flags:
4385
description: |-
4486
flags is the desired set of LINSTOR flags. Most node flags are

config/crd/bases/blockstor.io.blockstor.io_resourcedefinitions.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ spec:
178178
automatically when a consumer opens the device, provided no
179179
other peer is already Primary. Cluster-wide default true.
180180
type: boolean
181+
autoTieBreaker:
182+
description: |-
183+
autoTieBreaker gates auto-creation of a DISKLESS+TIE_BREAKER
184+
witness when a parent RD has an even number of diskful
185+
replicas and no operator-placed diskless replica. Replaces
186+
`Props["DrbdOptions/AutoAddQuorumTiebreaker"]`. nil inherits
187+
the cluster-wide default (true). Phase 10.3.
188+
type: boolean
181189
onNoQuorum:
182190
description: |-
183191
onNoQuorum is the action when quorum is lost. `io-error`

config/crd/bases/blockstor.io.blockstor.io_resourcegroups.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ spec:
179179
automatically when a consumer opens the device, provided no
180180
other peer is already Primary. Cluster-wide default true.
181181
type: boolean
182+
autoTieBreaker:
183+
description: |-
184+
autoTieBreaker gates auto-creation of a DISKLESS+TIE_BREAKER
185+
witness when a parent RD has an even number of diskful
186+
replicas and no operator-placed diskless replica. Replaces
187+
`Props["DrbdOptions/AutoAddQuorumTiebreaker"]`. nil inherits
188+
the cluster-wide default (true). Phase 10.3.
189+
type: boolean
182190
onNoQuorum:
183191
description: |-
184192
onNoQuorum is the action when quorum is lost. `io-error`

config/crd/bases/blockstor.io.blockstor.io_resources.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ spec:
177177
automatically when a consumer opens the device, provided no
178178
other peer is already Primary. Cluster-wide default true.
179179
type: boolean
180+
autoTieBreaker:
181+
description: |-
182+
autoTieBreaker gates auto-creation of a DISKLESS+TIE_BREAKER
183+
witness when a parent RD has an even number of diskful
184+
replicas and no operator-placed diskless replica. Replaces
185+
`Props["DrbdOptions/AutoAddQuorumTiebreaker"]`. nil inherits
186+
the cluster-wide default (true). Phase 10.3.
187+
type: boolean
180188
onNoQuorum:
181189
description: |-
182190
onNoQuorum is the action when quorum is lost. `io-error`

internal/controller/export_test.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,17 @@ func PtrEqI32(a, b *int32) bool {
108108
return ptrEqI32(a, b)
109109
}
110110

111-
// RangeProp exposes the Node-prop range parser the per-node port
112-
// and minor allocators use. Tests pin the three-tier fallback
113-
// (missing node → defaults; missing prop → defaults; bad format →
114-
// error) without spinning up the full allocator path.
111+
// RangeProp exposes the Node-range resolver the per-node port and
112+
// minor allocators use. Tests pin the four-tier fallback (missing
113+
// node → defaults; typed pointer set → wins; legacy Props key set
114+
// → falls through; bad format → error) without spinning up the
115+
// full allocator path. Backwards-compat shim for the existing
116+
// test signature: this wrapper resolves typed via the legacy
117+
// Props-key path so old tests keep working without rewriting.
115118
func (r *ResourceReconciler) RangeProp(ctx context.Context, nodeName, prop string, defLow, defHigh int32) (int32, int32, error) {
116-
return r.rangeProp(ctx, nodeName, prop, defLow, defHigh)
119+
return r.nodeRange(ctx, nodeName,
120+
func(*blockstoriov1alpha1.NodeSpec) *blockstoriov1alpha1.PortRange { return nil },
121+
prop, defLow, defHigh)
117122
}
118123

119124
// QuorumPolicy exposes upstream-LINSTOR's isQuorumFeasible

internal/controller/resource_controller.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -655,23 +655,38 @@ func (r *ResourceReconciler) allocateMinor(ctx context.Context, nodeName string)
655655
return drbd.LowestFreeMinor(taken, low, high)
656656
}
657657

658-
// portRangeForNode reads "DrbdOptions/TcpPortRange" off the named
659-
// Node CRD, falling back to the controller-wide default. Format
660-
// matches upstream: "min-max" decimal.
658+
// portRangeForNode reads the DRBD TCP port range off the named Node
659+
// CRD, falling back to the controller-wide default. Phase 10.3:
660+
// typed `Spec.DRBDPortRange` wins; legacy `Props["DrbdOptions/
661+
// TcpPortRange"]` is the forward-compat fallback. Default
662+
// (7000-7999) covers fresh clusters with neither set.
661663
func (r *ResourceReconciler) portRangeForNode(ctx context.Context, nodeName string) (int32, int32, error) {
662-
return r.rangeProp(ctx, nodeName, "DrbdOptions/TcpPortRange",
664+
return r.nodeRange(ctx, nodeName,
665+
func(s *blockstoriov1alpha1.NodeSpec) *blockstoriov1alpha1.PortRange { return s.DRBDPortRange },
666+
"DrbdOptions/TcpPortRange",
663667
drbd.DefaultPortMin, drbd.DefaultPortMax)
664668
}
665669

666670
func (r *ResourceReconciler) minorRangeForNode(ctx context.Context, nodeName string) (int32, int32, error) {
667-
return r.rangeProp(ctx, nodeName, "DrbdOptions/MinorNrRange",
671+
return r.nodeRange(ctx, nodeName,
672+
func(s *blockstoriov1alpha1.NodeSpec) *blockstoriov1alpha1.PortRange { return s.DRBDMinorRange },
673+
"DrbdOptions/MinorNrRange",
668674
drbd.DefaultMinorMin, drbd.DefaultMinorMax)
669675
}
670676

671-
// rangeProp reads a "min-max" prop off the Node CRD. Missing node or
672-
// missing prop falls back to defaults (the prop is optional). Bad
673-
// format returns an error so the operator notices the typo.
674-
func (r *ResourceReconciler) rangeProp(ctx context.Context, nodeName, prop string, defLow, defHigh int32) (int32, int32, error) {
677+
// nodeRange resolves a port/minor range for the named Node. Reads
678+
// the typed pointer first via the picker; on nil/missing falls back
679+
// to the legacy "min-max" Props key; on absent both, returns
680+
// defaults. Bad format on the Props side returns an error so the
681+
// operator notices a typo. Missing Node CRD silently uses defaults
682+
// (consistent with the legacy behaviour).
683+
func (r *ResourceReconciler) nodeRange(
684+
ctx context.Context,
685+
nodeName string,
686+
pick func(*blockstoriov1alpha1.NodeSpec) *blockstoriov1alpha1.PortRange,
687+
legacyProp string,
688+
defLow, defHigh int32,
689+
) (int32, int32, error) {
675690
var node blockstoriov1alpha1.Node
676691
if err := r.Get(ctx, client.ObjectKey{Name: nodeName}, &node); err != nil {
677692
if errors.IsNotFound(err) {
@@ -681,7 +696,11 @@ func (r *ResourceReconciler) rangeProp(ctx context.Context, nodeName, prop strin
681696
return 0, 0, err
682697
}
683698

684-
raw := node.Spec.Props[prop]
699+
if typed := pick(&node.Spec); typed != nil {
700+
return typed.Min, typed.Max, nil
701+
}
702+
703+
raw := node.Spec.Props[legacyProp]
685704
if raw == "" {
686705
return defLow, defHigh, nil
687706
}

internal/controller/resourcedefinition_controller.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -141,20 +141,26 @@ func (r *ResourceDefinitionReconciler) ensureTiebreaker(ctx context.Context, rd
141141
return r.setQuorum(ctx, rd, quorumPolicy(len(diskful), len(disklessAfter)))
142142
}
143143

144-
// isAutoTieBreakerEnabled gates witness auto-creation on the RD's
145-
// `DrbdOptions/AutoAddQuorumTiebreaker` prop. Default is enabled
146-
// (matches the effective cozystack / piraeus-operator behaviour
147-
// where ControllerProps seeds it true) — operators who explicitly
148-
// place a manual DISKLESS replica disable the auto path with
149-
// `DrbdOptions/AutoAddQuorumTiebreaker: "false"` on the RD spec.
144+
// isAutoTieBreakerEnabled gates witness auto-creation. Default is
145+
// enabled (matches the effective cozystack / piraeus-operator
146+
// behaviour where ControllerProps seeds it true). Operators who
147+
// explicitly place a manual DISKLESS replica disable the auto path
148+
// per-RD.
150149
//
151-
// We only check the RD prop bag here; the resolver (controller →
152-
// RG → RD → Resource hierarchy) doesn't run on the RD reconciler
153-
// path because that path doesn't dispatch to the satellite. A
154-
// cluster-wide ControllerProps default still propagates because
155-
// the REST POST /v1/resource-definitions handler folds parent-RG
156-
// + ControllerProps into the RD on create.
150+
// Phase 10.3: typed `Spec.DRBDOptions.Resource.AutoTieBreaker` wins;
151+
// legacy `Spec.Props["DrbdOptions/AutoAddQuorumTiebreaker"]` is the
152+
// forward-compat fallback. We only check the RD here; the resolver
153+
// (controller → RG → RD → Resource hierarchy) doesn't run on the
154+
// RD reconciler path because that path doesn't dispatch to the
155+
// satellite. A cluster-wide ControllerProps default still propagates
156+
// because the REST POST /v1/resource-definitions handler folds
157+
// parent-RG + ControllerProps into the RD on create.
157158
func isAutoTieBreakerEnabled(rd *blockstoriov1alpha1.ResourceDefinition) bool {
159+
if rd.Spec.DRBDOptions != nil && rd.Spec.DRBDOptions.Resource != nil &&
160+
rd.Spec.DRBDOptions.Resource.AutoTieBreaker != nil {
161+
return *rd.Spec.DRBDOptions.Resource.AutoTieBreaker
162+
}
163+
158164
const propKey = "DrbdOptions/AutoAddQuorumTiebreaker"
159165

160166
if rd.Spec.Props == nil {

0 commit comments

Comments
 (0)