Skip to content

Commit ad342d0

Browse files
kvapsclaude
andcommitted
feat(satellite/controllers): wire ResourceReconciler.Reconcile body (Phase 10.1)
Fills in the load-bearing piece of the Phase 10.1 migration: the satellite-side Resource reconciler now actually applies state instead of just logging. Reconcile pipeline: 1. Get the Resource CRD. 2. Skip if Spec.NodeName != self (defensive — predicate should have filtered). 3. Skip if DeletionTimestamp set (finalizer path lands in a follow-up). 4. Walk parent RD + same-RD peers + Node list via the local watch cache. 5. Resolve effective DRBD options via the shared `pkg/effectiveprops` package. 6. Build the DesiredResource via the now-exported `dispatcher.BuildDesired` (one-letter rename — same body the gRPC dispatch path uses). 7. Call `Config.Apply.Apply(ctx, [desired])` — the existing satellite storage + DRBD + LUKS chain. This means the controller-runtime path now has functional parity with the gRPC ApplyResources path. They run in parallel during the transition; Phase 10.6 retires gRPC once we've flipped the satellite agent over to the c-r manager. Dispatcher.BuildDesired's export is intentional: Phase 10.6 moves the function (and its helpers) into a shared package when the dispatcher itself goes away, but until then the satellite controllers borrow it without duplication. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 3eba521 commit ad342d0

2 files changed

Lines changed: 84 additions & 13 deletions

File tree

pkg/dispatcher/dispatcher.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ type ApplyOptions struct {
9494
EffectiveProps map[string]string
9595

9696
// LayerStack is the resolved layer composition (RD → RG → default).
97-
// Empty falls back to RD.Spec.LayerStack inside buildDesired so
97+
// Empty falls back to RD.Spec.LayerStack inside BuildDesired so
9898
// older call sites that don't compute the stack keep their
9999
// behaviour. The satellite skips DRBD when the stack omits it.
100100
LayerStack []string
@@ -115,7 +115,7 @@ func (d *Dispatcher) Apply(ctx context.Context, target *blockstoriov1alpha1.Reso
115115
return nil, errors.Errorf("no SatelliteEndpoint for node %q", target.Spec.NodeName)
116116
}
117117

118-
desired := buildDesired(target, peers, nodes, rd, opts.EffectiveProps)
118+
desired := BuildDesired(target, peers, nodes, rd, opts.EffectiveProps)
119119
if len(opts.LayerStack) > 0 {
120120
desired.LayerStack = opts.LayerStack
121121
}
@@ -164,7 +164,7 @@ func lookupEndpoint(nodeName string, nodes []blockstoriov1alpha1.Node) string {
164164
return ""
165165
}
166166

167-
// buildDesired translates a Resource + its same-RD peers into the
167+
// BuildDesired translates a Resource + its same-RD peers into the
168168
// satellite-facing DesiredResource. Port/minor/node-id assignment is
169169
// deterministic from the RD name + sorted peer list — good enough for
170170
// the first stand smoke; the autoplacer will replace it with a real
@@ -173,7 +173,7 @@ func lookupEndpoint(nodeName string, nodes []blockstoriov1alpha1.Node) string {
173173
// nodes is consulted for each peer's `SatelliteEndpoint` prop so
174174
// `peer.<name>.address` carries a real (pod) IP rather than a 0.0.0.0
175175
// placeholder; drbd-9 won't replicate to 0.0.0.0.
176-
func buildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alpha1.Resource, nodes []blockstoriov1alpha1.Node, rd *blockstoriov1alpha1.ResourceDefinition, effectiveProps map[string]string) *satellitepb.DesiredResource {
176+
func BuildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alpha1.Resource, nodes []blockstoriov1alpha1.Node, rd *blockstoriov1alpha1.ResourceDefinition, effectiveProps map[string]string) *satellitepb.DesiredResource {
177177
// node-id and port/minor are persisted on Status by the controller
178178
// before Apply runs. Falling back to derive*() is a transitional
179179
// safety net for the case where the allocator hasn't run yet — it
@@ -238,7 +238,7 @@ func buildDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alp
238238
}
239239

240240
// assembleDesired packages the per-replica wire payload. Pulled out
241-
// of buildDesired so the caller stays under the funlen budget.
241+
// of BuildDesired so the caller stays under the funlen budget.
242242
func assembleDesired(target *blockstoriov1alpha1.Resource, peers []blockstoriov1alpha1.Resource, rd *blockstoriov1alpha1.ResourceDefinition, dropped []string, drbdOpts, effectiveProps map[string]string) *satellitepb.DesiredResource {
243243
_ = peers // peers info already folded into drbdOpts via addPeerEntries
244244

@@ -353,7 +353,7 @@ func peerPortOf(r *blockstoriov1alpha1.Resource, fallback int) int {
353353

354354
// addPeerEntries fills in `peer.<name>.{port,node-id,address}` keys
355355
// on the DesiredResource's drbd_options map. Pulled out of
356-
// buildDesired to keep the latter under the funlen budget — this
356+
// BuildDesired to keep the latter under the funlen budget — this
357357
// owns the per-peer fan-out plus the port/address lookups.
358358
func addPeerEntries(drbdOpts map[string]string, dropped []string, peers []blockstoriov1alpha1.Resource, nodes []blockstoriov1alpha1.Node, fallbackPort int, idOf map[string]int32) {
359359
peerByName := make(map[string]*blockstoriov1alpha1.Resource, len(peers))

pkg/satellite/controllers/resource.go

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import (
2929
"sigs.k8s.io/controller-runtime/pkg/predicate"
3030

3131
blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
32+
"github.com/cozystack/blockstor/pkg/dispatcher"
33+
"github.com/cozystack/blockstor/pkg/effectiveprops"
34+
satellitepb "github.com/cozystack/blockstor/pkg/satellite/proto"
3235
)
3336

3437
// ResourceReconciler watches Resource CRDs filtered to those
@@ -70,14 +73,29 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
7073
return ctrl.Result{}, nil
7174
}
7275

73-
logger.V(1).Info("observed Resource",
74-
"rd", res.Spec.ResourceDefinitionName,
75-
"flags", res.Spec.Flags,
76-
"deletionTimestamp", res.DeletionTimestamp)
76+
if !res.DeletionTimestamp.IsZero() {
77+
// Phase 10.1 follow-up: finalizer-driven tear-down.
78+
// Until then the controller-side ResourceReconciler's
79+
// runDelete path still owns deletion via gRPC.
80+
return ctrl.Result{}, nil
81+
}
82+
83+
desired, err := r.buildDesiredFromCRD(ctx, &res)
84+
if err != nil {
85+
return ctrl.Result{}, err
86+
}
87+
88+
results, err := r.Config.Apply.Apply(ctx, []*satellitepb.DesiredResource{desired})
89+
if err != nil {
90+
return ctrl.Result{}, errors.Wrap(err, "satellite Apply")
91+
}
92+
93+
for _, res := range results {
94+
if !res.GetOk() {
95+
logger.Info("Apply per-resource failure", "name", res.GetName(), "message", res.GetMessage())
96+
}
97+
}
7798

78-
// Phase 10.1 follow-up: translate to DesiredResource +
79-
// call r.Config.Apply.Apply(...). Until then the gRPC
80-
// `ApplyResources` consumer continues to drive applies.
8199
return ctrl.Result{}, nil
82100
}
83101

@@ -98,6 +116,59 @@ func (r *ResourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
98116
return nil
99117
}
100118

119+
// buildDesiredFromCRD packages a Resource CRD into the
120+
// satellite-facing DesiredResource by walking the apiserver
121+
// for parent RD + same-RD peers + Node list, then resolving
122+
// effective DRBD options via the shared
123+
// `pkg/effectiveprops` package and finally handing everything
124+
// to `dispatcher.BuildDesired`.
125+
//
126+
// This is the load-bearing replacement for the gRPC dispatch
127+
// path — the controller-runtime reconciler does exactly the
128+
// same work the controller's dispatcher did, just on the
129+
// satellite side. Phase 10.1.
130+
func (r *ResourceReconciler) buildDesiredFromCRD(ctx context.Context, target *blockstoriov1alpha1.Resource) (*satellitepb.DesiredResource, error) {
131+
var rd blockstoriov1alpha1.ResourceDefinition
132+
133+
err := r.Get(ctx, client.ObjectKey{Name: target.Spec.ResourceDefinitionName}, &rd)
134+
if err != nil {
135+
if apierrors.IsNotFound(err) {
136+
return nil, errors.Errorf("parent RD %q not found", target.Spec.ResourceDefinitionName)
137+
}
138+
139+
return nil, errors.Wrap(err, "get parent RD")
140+
}
141+
142+
var resList blockstoriov1alpha1.ResourceList
143+
144+
err = r.List(ctx, &resList)
145+
if err != nil {
146+
return nil, errors.Wrap(err, "list Resources for peer set")
147+
}
148+
149+
peers := make([]blockstoriov1alpha1.Resource, 0, len(resList.Items))
150+
151+
for i := range resList.Items {
152+
if resList.Items[i].Spec.ResourceDefinitionName == target.Spec.ResourceDefinitionName {
153+
peers = append(peers, resList.Items[i])
154+
}
155+
}
156+
157+
var nodeList blockstoriov1alpha1.NodeList
158+
159+
err = r.List(ctx, &nodeList)
160+
if err != nil {
161+
return nil, errors.Wrap(err, "list Nodes")
162+
}
163+
164+
effectiveProps, err := effectiveprops.Resolve(ctx, r.Client, target, &rd)
165+
if err != nil {
166+
return nil, errors.Wrap(err, "resolve effective props")
167+
}
168+
169+
return dispatcher.BuildDesired(target, peers, nodeList.Items, &rd, effectiveProps), nil
170+
}
171+
101172
// nodeNamePredicate filters events down to objects whose
102173
// `Spec.NodeName` matches the given node. Centralised so the
103174
// StoragePool reconciler can reuse the same shape.

0 commit comments

Comments
 (0)