Skip to content

Commit f48b95d

Browse files
kvapsclaude
andcommitted
feat(dispatcher): RD VolumeDefinitions land as DesiredVolumes on satellite
Apply now also fetches the parent ResourceDefinition. Its VolumeDefinitions become DesiredVolumes for the target Resource — DISKLESS replicas still get an empty list (they don't allocate local storage). The StoragePool is sourced from the Resource's StorPoolName prop with the RD's prop as fallback. Lays the path for the volume-side smoke: once a satellite registers a Provider matching that pool name, applyStorage allocates the LV and drbdadm picks it up. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 91bb343 commit f48b95d

3 files changed

Lines changed: 71 additions & 11 deletions

File tree

internal/controller/resource_controller.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,12 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
9393
return ctrl.Result{}, err
9494
}
9595

96-
result, err := r.Dispatcher.Apply(ctx, &target, peers, nodeList.Items)
96+
rdPtr, err := r.lookupRD(ctx, target.Spec.ResourceDefinitionName)
97+
if err != nil {
98+
return ctrl.Result{}, err
99+
}
100+
101+
result, err := r.Dispatcher.Apply(ctx, &target, peers, nodeList.Items, rdPtr)
97102
if err != nil {
98103
log.Error(err, "Apply RPC failed", "resource", target.Name, "node", target.Spec.NodeName)
99104

@@ -111,6 +116,24 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
111116
return ctrl.Result{}, nil
112117
}
113118

119+
// lookupRD fetches the parent ResourceDefinition. A NotFound is
120+
// converted to (nil, nil) so the dispatcher can still push the
121+
// .res for connection setup; any other error bubbles.
122+
func (r *ResourceReconciler) lookupRD(ctx context.Context, name string) (*blockstoriov1alpha1.ResourceDefinition, error) {
123+
var rd blockstoriov1alpha1.ResourceDefinition
124+
125+
err := r.Get(ctx, client.ObjectKey{Name: name}, &rd)
126+
if err != nil {
127+
if errors.IsNotFound(err) {
128+
return nil, nil //nolint:nilnil // intentional: caller treats nil RD as no-volume push
129+
}
130+
131+
return nil, err
132+
}
133+
134+
return &rd, nil
135+
}
136+
114137
// SetupWithManager sets up the controller with the Manager.
115138
func (r *ResourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
116139
return ctrl.NewControllerManagedBy(mgr).

pkg/dispatcher/dispatcher.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"context"
2525
"crypto/sha256"
2626
"encoding/binary"
27+
"slices"
2728
"sort"
2829
"strconv"
2930
"strings"
@@ -82,18 +83,21 @@ func New(dialer Dialer) *Dispatcher {
8283
}
8384

8485
// Apply builds the DesiredResource for this Resource (looking up its
85-
// peers from the full RD-wide list) and sends it to the target
86-
// satellite. Returns the per-resource result the satellite reported.
86+
// peers from the full RD-wide list and its volumes from the parent
87+
// RD) and sends it to the target satellite. Returns the per-resource
88+
// result the satellite reported.
8789
//
8890
// nodes is the full Node CRD list — Apply uses it to resolve each
89-
// peer's SatelliteEndpoint property.
90-
func (d *Dispatcher) Apply(ctx context.Context, target *blockstoriov1alpha1.Resource, peers []blockstoriov1alpha1.Resource, nodes []blockstoriov1alpha1.Node) (*satellitepb.ResourceApplyResult, error) {
91+
// peer's SatelliteEndpoint property. rd may be nil; when present the
92+
// RD's VolumeDefinitions become DesiredVolumes for non-DISKLESS
93+
// replicas (DISKLESS replicas ignore them).
94+
func (d *Dispatcher) Apply(ctx context.Context, target *blockstoriov1alpha1.Resource, peers []blockstoriov1alpha1.Resource, nodes []blockstoriov1alpha1.Node, rd *blockstoriov1alpha1.ResourceDefinition) (*satellitepb.ResourceApplyResult, error) {
9195
endpoint := lookupEndpoint(target.Spec.NodeName, nodes)
9296
if endpoint == "" {
9397
return nil, errors.Errorf("no SatelliteEndpoint for node %q", target.Spec.NodeName)
9498
}
9599

96-
desired := buildDesired(target, peers, nodes)
100+
desired := buildDesired(target, peers, nodes, rd)
97101

98102
client, closer, err := d.dialer.Dial(ctx, endpoint)
99103
if err != nil {
@@ -135,7 +139,7 @@ func lookupEndpoint(nodeName string, nodes []blockstoriov1alpha1.Node) string {
135139
// nodes is consulted for each peer's `SatelliteEndpoint` prop so
136140
// `peer.<name>.address` carries a real (pod) IP rather than a 0.0.0.0
137141
// placeholder; drbd-9 won't replicate to 0.0.0.0.
138-
func buildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alpha1.Resource, nodes []blockstoriov1alpha1.Node) *satellitepb.DesiredResource {
142+
func buildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alpha1.Resource, nodes []blockstoriov1alpha1.Node, rd *blockstoriov1alpha1.ResourceDefinition) *satellitepb.DesiredResource {
139143
rdName := target.Spec.ResourceDefinitionName
140144
port := derivePort(rdName)
141145
minor := deriveMinor(rdName)
@@ -189,10 +193,43 @@ func buildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alp
189193
Flags: target.Spec.Flags,
190194
Props: target.Spec.Props,
191195
Peers: dropped,
196+
Volumes: buildVolumes(rd, target),
192197
DrbdOptions: drbdOpts,
193198
}
194199
}
195200

201+
// buildVolumes turns the parent ResourceDefinition's VolumeDefinitions
202+
// into DesiredVolumes for the satellite. DISKLESS replicas get an
203+
// empty list — they don't allocate local storage. The StoragePool name
204+
// comes from the Resource's `StorPoolName` prop (LINSTOR-compatible
205+
// key) with the RD-level fallback.
206+
func buildVolumes(rd *blockstoriov1alpha1.ResourceDefinition, target *blockstoriov1alpha1.Resource) []*satellitepb.DesiredVolume {
207+
if rd == nil {
208+
return nil
209+
}
210+
211+
if slices.Contains(target.Spec.Flags, "DISKLESS") {
212+
return nil
213+
}
214+
215+
pool := target.Spec.Props["StorPoolName"]
216+
if pool == "" {
217+
pool = rd.Spec.Props["StorPoolName"]
218+
}
219+
220+
out := make([]*satellitepb.DesiredVolume, 0, len(rd.Spec.VolumeDefinitions))
221+
222+
for _, vd := range rd.Spec.VolumeDefinitions {
223+
out = append(out, &satellitepb.DesiredVolume{
224+
VolumeNumber: vd.VolumeNumber,
225+
SizeKib: vd.SizeKib,
226+
StoragePool: pool,
227+
})
228+
}
229+
230+
return out
231+
}
232+
196233
// peerAddress looks up `nodeName`'s SatelliteEndpoint and returns
197234
// just the host part (no port). Falls back to the 0.0.0.0 placeholder
198235
// when the node is unknown or hasn't registered yet — the satellite

pkg/dispatcher/dispatcher_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestApplyDialsTargetSatellite(t *testing.T) {
8181
},
8282
}}
8383

84-
result, err := d.Apply(t.Context(), target, nil, nodes)
84+
result, err := d.Apply(t.Context(), target, nil, nodes, nil)
8585
if err != nil {
8686
t.Fatalf("Apply: %v", err)
8787
}
@@ -108,7 +108,7 @@ func TestApplyMissingEndpoint(t *testing.T) {
108108
Spec: blockstoriov1alpha1.NodeSpec{Type: "SATELLITE"},
109109
}}
110110

111-
_, err := d.Apply(t.Context(), target, nil, nodes)
111+
_, err := d.Apply(t.Context(), target, nil, nodes, nil)
112112
if err == nil {
113113
t.Fatalf("expected error when endpoint missing")
114114
}
@@ -141,7 +141,7 @@ func TestApplyBuildsPeers(t *testing.T) {
141141
},
142142
}}
143143

144-
_, err := d.Apply(t.Context(), target, peers, nodes)
144+
_, err := d.Apply(t.Context(), target, peers, nodes, nil)
145145
if err != nil {
146146
t.Fatalf("Apply: %v", err)
147147
}
@@ -175,7 +175,7 @@ func TestApplyDialError(t *testing.T) {
175175
},
176176
}}
177177

178-
_, err := d.Apply(t.Context(), target, nil, nodes)
178+
_, err := d.Apply(t.Context(), target, nil, nodes, nil)
179179
if err == nil {
180180
t.Fatalf("expected dial error")
181181
}

0 commit comments

Comments
 (0)