Skip to content

Commit dd01cc9

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

File tree

2 files changed

+246
-2
lines changed

2 files changed

+246
-2
lines changed

controllers/openstackserver_controller.go

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

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)