Skip to content

Commit 828af4f

Browse files
committed
fix: openstackserver reconciliation when cluster is paused
Signed-off-by: Bharath Nallapeta <[email protected]>
1 parent ee4be02 commit 828af4f

File tree

2 files changed

+259
-13
lines changed

2 files changed

+259
-13
lines changed

controllers/openstackserver_controller.go

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"sigs.k8s.io/cluster-api/util/annotations"
4040
v1beta1conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions"
4141
"sigs.k8s.io/cluster-api/util/patch"
42+
"sigs.k8s.io/cluster-api/util/predicates"
4243
ctrl "sigs.k8s.io/controller-runtime"
4344
"sigs.k8s.io/controller-runtime/pkg/builder"
4445
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -50,7 +51,7 @@ import (
5051
"sigs.k8s.io/controller-runtime/pkg/reconcile"
5152

5253
orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
53-
"github.com/k-orc/openstack-resource-controller/v2/pkg/predicates"
54+
orcpredicates "github.com/k-orc/openstack-resource-controller/v2/pkg/predicates"
5455

5556
infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
5657
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
@@ -78,6 +79,7 @@ type OpenStackServerReconciler struct {
7879

7980
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackservers,verbs=get;list;watch;create;update;patch;delete
8081
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackservers/status,verbs=get;update;patch
82+
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch
8183
// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddressclaims;ipaddressclaims/status,verbs=get;watch;create;update;patch;delete
8284
// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddresses;ipaddresses/status,verbs=get;list;watch
8385
// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=images,verbs=get;list;watch
@@ -105,17 +107,6 @@ func (r *OpenStackServerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
105107

106108
scope.Logger().Info("Reconciling OpenStackServer")
107109

108-
cluster, err := getClusterFromMetadata(ctx, r.Client, openStackServer.ObjectMeta)
109-
if err != nil {
110-
return reconcile.Result{}, err
111-
}
112-
if cluster != nil {
113-
if annotations.IsPaused(cluster, openStackServer) {
114-
scope.Logger().Info("OpenStackServer linked to a Cluster that is paused. Won't reconcile")
115-
return reconcile.Result{}, nil
116-
}
117-
}
118-
119110
patchHelper, err := patch.NewHelper(openStackServer, r.Client)
120111
if err != nil {
121112
return ctrl.Result{}, err
@@ -134,6 +125,7 @@ func (r *OpenStackServerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
134125
}
135126
}()
136127

128+
// Handle deleted servers
137129
if !openStackServer.DeletionTimestamp.IsZero() {
138130
// When moving a cluster, we need to populate the server status with the resources
139131
// that were in another object's status.
@@ -146,6 +138,18 @@ func (r *OpenStackServerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
146138
return reconcile.Result{}, r.reconcileDelete(scope, openStackServer)
147139
}
148140

141+
// Handle non-deleted servers
142+
cluster, err := getClusterFromMetadata(ctx, r.Client, openStackServer.ObjectMeta)
143+
if err != nil {
144+
return reconcile.Result{}, err
145+
}
146+
if cluster != nil {
147+
if annotations.IsPaused(cluster, openStackServer) {
148+
scope.Logger().Info("OpenStackServer linked to a Cluster that is paused. Won't reconcile")
149+
return reconcile.Result{}, nil
150+
}
151+
}
152+
149153
return r.reconcileNormal(ctx, scope, openStackServer)
150154
}
151155

@@ -218,7 +222,12 @@ func (r *OpenStackServerReconciler) SetupWithManager(ctx context.Context, mgr ct
218222
}
219223
return requests
220224
}),
221-
builder.WithPredicates(predicates.NewBecameAvailable(mgr.GetLogger(), &orcv1alpha1.Image{})),
225+
builder.WithPredicates(orcpredicates.NewBecameAvailable(mgr.GetLogger(), &orcv1alpha1.Image{})),
226+
).
227+
Watches(
228+
&clusterv1.Cluster{},
229+
handler.EnqueueRequestsFromMapFunc(r.requeueOpenStackServersForCluster(ctx)),
230+
builder.WithPredicates(predicates.ClusterPausedTransitionsOrInfrastructureProvisioned(mgr.GetScheme(), log)),
222231
).
223232
Watches(
224233
&ipamv1.IPAddressClaim{},
@@ -752,3 +761,50 @@ func OpenStackServerReconcileComplete(log logr.Logger) predicate.Funcs {
752761
GenericFunc: func(event.GenericEvent) bool { return false },
753762
}
754763
}
764+
765+
// requeueOpenStackServersForCluster returns a handler.MapFunc that watches for
766+
// Cluster changes and triggers reconciliation of all OpenStackServers in that cluster.
767+
func (r *OpenStackServerReconciler) requeueOpenStackServersForCluster(ctx context.Context) handler.MapFunc {
768+
log := ctrl.LoggerFrom(ctx)
769+
return func(ctx context.Context, o client.Object) []ctrl.Request {
770+
c, ok := o.(*clusterv1.Cluster)
771+
if !ok {
772+
panic(fmt.Sprintf("Expected a Cluster but got a %T", o))
773+
}
774+
775+
log := log.WithValues("objectMapper", "clusterToOpenStackServer", "namespace", c.Namespace, "cluster", c.Name)
776+
777+
// Don't handle deleted clusters - servers will be cleaned up via their own deletion flow
778+
if !c.DeletionTimestamp.IsZero() {
779+
log.V(4).Info("Cluster has a deletion timestamp, skipping mapping.")
780+
return nil
781+
}
782+
783+
// List all OpenStackServers in the cluster
784+
serverList := &infrav1alpha1.OpenStackServerList{}
785+
if err := r.Client.List(
786+
ctx,
787+
serverList,
788+
client.InNamespace(c.Namespace),
789+
client.MatchingLabels{clusterv1.ClusterNameLabel: c.Name},
790+
); err != nil {
791+
log.Error(err, "Failed to list OpenStackServers for cluster")
792+
return nil
793+
}
794+
795+
// Create reconcile requests for all servers
796+
requests := make([]ctrl.Request, 0, len(serverList.Items))
797+
for i := range serverList.Items {
798+
server := &serverList.Items[i]
799+
requests = append(requests, ctrl.Request{
800+
NamespacedName: client.ObjectKey{
801+
Namespace: server.Namespace,
802+
Name: server.Name,
803+
},
804+
})
805+
log.V(5).Info("Queueing OpenStackServer for reconciliation", "server", server.Name)
806+
}
807+
808+
return requests
809+
}
810+
}

controllers/openstackserver_controller_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"reflect"
2223
"testing"
@@ -33,9 +34,13 @@ import (
3334
"go.uber.org/mock/gomock"
3435
corev1 "k8s.io/api/core/v1"
3536
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
"k8s.io/apimachinery/pkg/runtime"
3638
"k8s.io/utils/ptr"
3739
clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1"
40+
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
3841
"sigs.k8s.io/cluster-api/util/conditions"
42+
"sigs.k8s.io/controller-runtime/pkg/client"
43+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3944

4045
infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
4146
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
@@ -194,6 +199,191 @@ var deleteRootVolume = func(r *recorders) {
194199
r.volume.DeleteVolume(rootVolumeUUID, volumes.DeleteOpts{}).Return(nil)
195200
}
196201

202+
func TestOpenStackServerReconciler_requeueOpenStackServersForCluster(t *testing.T) {
203+
tests := []struct {
204+
name string
205+
cluster *clusterv1.Cluster
206+
servers []*infrav1alpha1.OpenStackServer
207+
clusterDeleting bool
208+
wantRequests int
209+
wantServerNames []string
210+
}{
211+
{
212+
name: "returns reconcile requests for all servers in cluster",
213+
cluster: &clusterv1.Cluster{
214+
ObjectMeta: metav1.ObjectMeta{
215+
Name: "test-cluster",
216+
Namespace: "test-ns",
217+
},
218+
},
219+
servers: []*infrav1alpha1.OpenStackServer{
220+
{
221+
ObjectMeta: metav1.ObjectMeta{
222+
Name: "server-1",
223+
Namespace: "test-ns",
224+
Labels: map[string]string{
225+
clusterv1beta1.ClusterNameLabel: "test-cluster",
226+
},
227+
},
228+
Spec: infrav1alpha1.OpenStackServerSpec{
229+
Flavor: ptr.To("m1.small"),
230+
Image: infrav1.ImageParam{
231+
Filter: &infrav1.ImageFilter{Name: ptr.To("test-image")},
232+
},
233+
},
234+
},
235+
{
236+
ObjectMeta: metav1.ObjectMeta{
237+
Name: "server-2",
238+
Namespace: "test-ns",
239+
Labels: map[string]string{
240+
clusterv1beta1.ClusterNameLabel: "test-cluster",
241+
},
242+
},
243+
Spec: infrav1alpha1.OpenStackServerSpec{
244+
Flavor: ptr.To("m1.small"),
245+
Image: infrav1.ImageParam{
246+
Filter: &infrav1.ImageFilter{Name: ptr.To("test-image")},
247+
},
248+
},
249+
},
250+
},
251+
wantRequests: 2,
252+
wantServerNames: []string{"server-1", "server-2"},
253+
},
254+
{
255+
name: "returns empty for deleted cluster",
256+
cluster: &clusterv1.Cluster{
257+
ObjectMeta: metav1.ObjectMeta{
258+
Name: "test-cluster",
259+
Namespace: "test-ns",
260+
},
261+
},
262+
servers: []*infrav1alpha1.OpenStackServer{
263+
{
264+
ObjectMeta: metav1.ObjectMeta{
265+
Name: "server-1",
266+
Namespace: "test-ns",
267+
Labels: map[string]string{
268+
clusterv1beta1.ClusterNameLabel: "test-cluster",
269+
},
270+
},
271+
Spec: infrav1alpha1.OpenStackServerSpec{
272+
Flavor: ptr.To("m1.small"),
273+
Image: infrav1.ImageParam{
274+
Filter: &infrav1.ImageFilter{Name: ptr.To("test-image")},
275+
},
276+
},
277+
},
278+
},
279+
clusterDeleting: true,
280+
wantRequests: 0,
281+
},
282+
{
283+
name: "returns empty when no servers exist",
284+
cluster: &clusterv1.Cluster{
285+
ObjectMeta: metav1.ObjectMeta{
286+
Name: "test-cluster",
287+
Namespace: "test-ns",
288+
},
289+
},
290+
servers: []*infrav1alpha1.OpenStackServer{},
291+
wantRequests: 0,
292+
},
293+
{
294+
name: "only returns servers from same cluster",
295+
cluster: &clusterv1.Cluster{
296+
ObjectMeta: metav1.ObjectMeta{
297+
Name: "test-cluster",
298+
Namespace: "test-ns",
299+
},
300+
},
301+
servers: []*infrav1alpha1.OpenStackServer{
302+
{
303+
ObjectMeta: metav1.ObjectMeta{
304+
Name: "server-1",
305+
Namespace: "test-ns",
306+
Labels: map[string]string{
307+
clusterv1beta1.ClusterNameLabel: "test-cluster",
308+
},
309+
},
310+
Spec: infrav1alpha1.OpenStackServerSpec{
311+
Flavor: ptr.To("m1.small"),
312+
Image: infrav1.ImageParam{
313+
Filter: &infrav1.ImageFilter{Name: ptr.To("test-image")},
314+
},
315+
},
316+
},
317+
{
318+
ObjectMeta: metav1.ObjectMeta{
319+
Name: "server-2",
320+
Namespace: "test-ns",
321+
Labels: map[string]string{
322+
clusterv1beta1.ClusterNameLabel: "other-cluster",
323+
},
324+
},
325+
Spec: infrav1alpha1.OpenStackServerSpec{
326+
Flavor: ptr.To("m1.small"),
327+
Image: infrav1.ImageParam{
328+
Filter: &infrav1.ImageFilter{Name: ptr.To("test-image")},
329+
},
330+
},
331+
},
332+
},
333+
wantRequests: 1,
334+
wantServerNames: []string{"server-1"},
335+
},
336+
}
337+
338+
for i := range tests {
339+
tt := &tests[i]
340+
t.Run(tt.name, func(t *testing.T) {
341+
g := NewGomegaWithT(t)
342+
ctx := context.TODO()
343+
344+
// Set deletion timestamp and finalizers if needed
345+
if tt.clusterDeleting {
346+
now := metav1.Now()
347+
tt.cluster.DeletionTimestamp = &now
348+
tt.cluster.Finalizers = []string{"test-finalizer"}
349+
}
350+
351+
// Create a fake client with the test data
352+
scheme := runtime.NewScheme()
353+
g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed())
354+
g.Expect(infrav1alpha1.AddToScheme(scheme)).To(Succeed())
355+
356+
objs := []client.Object{tt.cluster}
357+
for _, server := range tt.servers {
358+
objs = append(objs, server)
359+
}
360+
361+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build()
362+
363+
// Create reconciler and call mapper function
364+
reconciler := &OpenStackServerReconciler{
365+
Client: fakeClient,
366+
}
367+
mapFunc := reconciler.requeueOpenStackServersForCluster(ctx)
368+
requests := mapFunc(ctx, tt.cluster)
369+
370+
// Verify results
371+
if tt.wantRequests == 0 {
372+
g.Expect(requests).To(Or(BeNil(), BeEmpty()))
373+
} else {
374+
g.Expect(requests).To(HaveLen(tt.wantRequests))
375+
if len(tt.wantServerNames) > 0 {
376+
gotNames := make([]string, len(requests))
377+
for i, req := range requests {
378+
gotNames[i] = req.Name
379+
}
380+
g.Expect(gotNames).To(ConsistOf(tt.wantServerNames))
381+
}
382+
}
383+
})
384+
}
385+
}
386+
197387
func TestOpenStackServer_serverToInstanceSpec(t *testing.T) {
198388
tests := []struct {
199389
name string

0 commit comments

Comments
 (0)