@@ -18,6 +18,7 @@ package controller
1818
1919import (
2020 "context"
21+ "slices"
2122 "time"
2223
2324 "k8s.io/apimachinery/pkg/api/errors"
@@ -30,6 +31,11 @@ import (
3031 "github.com/cozystack/blockstor/pkg/dispatcher"
3132)
3233
34+ // resourceFinalizer guards on-disk + DRBD-side teardown when the
35+ // Resource CRD is deleted. Without it the satellite would never see
36+ // the delete, leaving an orphan .res file + LV / loopfile.
37+ const resourceFinalizer = "blockstor.io.blockstor.io/resource"
38+
3339// ResourceReconciler dispatches Resource CRD changes to the right
3440// satellite via the Dispatcher. It collects same-RD peers and the
3541// full Node list (for endpoint resolution) on every reconcile —
@@ -51,8 +57,6 @@ type ResourceReconciler struct {
5157// to the satellite that hosts it. Per-replica errors land in the
5258// log; transport faults trigger a 10s requeue.
5359func (r * ResourceReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
54- log := logf .FromContext (ctx )
55-
5660 if r .Dispatcher == nil {
5761 // envtest scaffolding (suite_test.go) constructs the reconciler
5862 // without a Dispatcher — keep the original no-op behaviour for
@@ -71,9 +75,37 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
7175 return ctrl.Result {}, err
7276 }
7377
78+ // Deletion path: tell the satellite to drop the resource, then
79+ // remove the finalizer so kube-apiserver finishes the delete.
80+ if ! target .DeletionTimestamp .IsZero () {
81+ return r .runDelete (ctx , & target )
82+ }
83+
84+ // Ensure finalizer is present on every live Resource so the
85+ // delete path above can run.
86+ if ! slices .Contains (target .Finalizers , resourceFinalizer ) {
87+ target .Finalizers = append (target .Finalizers , resourceFinalizer )
88+
89+ err = r .Update (ctx , & target )
90+ if err != nil {
91+ return ctrl.Result {}, err
92+ }
93+
94+ return ctrl.Result {Requeue : true }, nil
95+ }
96+
97+ return r .runApply (ctx , & target )
98+ }
99+
100+ // runApply is the apply branch of Reconcile. Pulled out to keep
101+ // Reconcile under the funlen budget — the body deals with the
102+ // finalizer dance, this with the actual gRPC dispatch.
103+ func (r * ResourceReconciler ) runApply (ctx context.Context , target * blockstoriov1alpha1.Resource ) (ctrl.Result , error ) {
104+ log := logf .FromContext (ctx )
105+
74106 var resList blockstoriov1alpha1.ResourceList
75107
76- err = r .List (ctx , & resList )
108+ err : = r .List (ctx , & resList )
77109 if err != nil {
78110 return ctrl.Result {}, err
79111 }
@@ -98,7 +130,7 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
98130 return ctrl.Result {}, err
99131 }
100132
101- result , err := r .Dispatcher .Apply (ctx , & target , peers , nodeList .Items , rdPtr )
133+ result , err := r .Dispatcher .Apply (ctx , target , peers , nodeList .Items , rdPtr )
102134 if err != nil {
103135 log .Error (err , "Apply RPC failed" , "resource" , target .Name , "node" , target .Spec .NodeName )
104136
@@ -116,6 +148,53 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
116148 return ctrl.Result {}, nil
117149}
118150
151+ // runDelete is the finalizer-driven teardown. We dial the satellite
152+ // to drop the resource (drbdadm down → DeleteVolume → rm .res), then
153+ // strip the finalizer so kube-apiserver completes the delete.
154+ // Failures requeue with a 10 s back-off so the resource isn't stuck
155+ // half-gone if a satellite is briefly unreachable.
156+ func (r * ResourceReconciler ) runDelete (ctx context.Context , target * blockstoriov1alpha1.Resource ) (ctrl.Result , error ) {
157+ log := logf .FromContext (ctx )
158+
159+ if ! slices .Contains (target .Finalizers , resourceFinalizer ) {
160+ return ctrl.Result {}, nil
161+ }
162+
163+ rdPtr , _ := r .lookupRD (ctx , target .Spec .ResourceDefinitionName )
164+
165+ var nodeList blockstoriov1alpha1.NodeList
166+
167+ err := r .List (ctx , & nodeList )
168+ if err != nil {
169+ return ctrl.Result {}, err
170+ }
171+
172+ resp , err := r .Dispatcher .DeleteResource (ctx , target , rdPtr , nodeList .Items )
173+ if err != nil {
174+ log .Error (err , "DeleteResource RPC failed" ,
175+ "resource" , target .Name , "node" , target .Spec .NodeName )
176+
177+ return ctrl.Result {RequeueAfter : 10 * time .Second }, nil
178+ }
179+
180+ if ! resp .GetOk () {
181+ log .Info ("satellite rejected delete" , "msg" , resp .GetMessage (),
182+ "resource" , target .Name , "node" , target .Spec .NodeName )
183+
184+ return ctrl.Result {RequeueAfter : 10 * time .Second }, nil
185+ }
186+
187+ target .Finalizers = slices .DeleteFunc (target .Finalizers ,
188+ func (s string ) bool { return s == resourceFinalizer })
189+
190+ err = r .Update (ctx , target )
191+ if err != nil {
192+ return ctrl.Result {}, err
193+ }
194+
195+ return ctrl.Result {}, nil
196+ }
197+
119198// lookupRD fetches the parent ResourceDefinition. A NotFound is
120199// converted to (nil, nil) so the dispatcher can still push the
121200// .res for connection setup; any other error bubbles.
0 commit comments