Skip to content

Commit 7d831b7

Browse files
authored
🌱 Increase requeueAfter No Available Host. And trigger Reconcile of hbmm when hbmh is free. (#1590)
1 parent 5224c9c commit 7d831b7

File tree

5 files changed

+382
-64
lines changed

5 files changed

+382
-64
lines changed

controllers/hetznerbaremetalmachine_controller.go

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ func (r *HetznerBareMetalMachineReconciler) SetupWithManager(ctx context.Context
230230
).
231231
Watches(
232232
&infrav1.HetznerBareMetalHost{},
233-
handler.EnqueueRequestsFromMapFunc(r.BareMetalHostToBareMetalMachines(log)),
233+
handler.EnqueueRequestsFromMapFunc(BareMetalHostToBareMetalMachines(r.Client, log)),
234234
).
235235
Watches(
236236
&clusterv1.Cluster{},
@@ -336,25 +336,69 @@ func (r *HetznerBareMetalMachineReconciler) ClusterToBareMetalMachines(ctx conte
336336

337337
// BareMetalHostToBareMetalMachines will return a reconcile request for a BareMetalMachine if the event is for a
338338
// BareMetalHost and that BareMetalHost references a BareMetalMachine.
339-
func (r *HetznerBareMetalMachineReconciler) BareMetalHostToBareMetalMachines(log logr.Logger) handler.MapFunc {
340-
return func(_ context.Context, obj client.Object) []reconcile.Request {
341-
if host, ok := obj.(*infrav1.HetznerBareMetalHost); ok {
342-
if host.Spec.ConsumerRef != nil &&
343-
host.Spec.ConsumerRef.Kind == "HetznerBareMetalMachine" &&
344-
host.Spec.ConsumerRef.GroupVersionKind().Group == infrav1.GroupVersion.Group {
345-
return []reconcile.Request{
346-
{
347-
NamespacedName: types.NamespacedName{
348-
Name: host.Spec.ConsumerRef.Name,
349-
Namespace: host.Spec.ConsumerRef.Namespace,
350-
},
351-
},
352-
}
353-
}
354-
} else {
339+
func BareMetalHostToBareMetalMachines(c client.Client, log logr.Logger) handler.MapFunc {
340+
return func(ctx context.Context, obj client.Object) []reconcile.Request {
341+
host, ok := obj.(*infrav1.HetznerBareMetalHost)
342+
if !ok {
355343
log.Error(fmt.Errorf("expected a BareMetalHost but got a %T", obj),
356344
"failed to get BareMetalMachine for BareMetalHost")
345+
return nil
346+
}
347+
348+
// If this host has a consumerRef (hbmm), then reconcile the corresponding hbmm.
349+
if host.Spec.ConsumerRef != nil {
350+
return []reconcile.Request{
351+
{
352+
NamespacedName: types.NamespacedName{
353+
Name: host.Spec.ConsumerRef.Name,
354+
Namespace: host.Spec.ConsumerRef.Namespace,
355+
},
356+
},
357+
}
358+
}
359+
360+
if host.Spec.Status.ErrorType != "" {
361+
return []reconcile.Request{}
362+
}
363+
364+
// We have a free host. Trigger a matching HetznerBareMetalMachine to be reconciled.
365+
hbmmList := infrav1.HetznerBareMetalMachineList{}
366+
err := c.List(ctx, &hbmmList, client.InNamespace(host.Namespace))
367+
if err != nil {
368+
log.Error(err, "failed to list HetznerBareMetalMachines")
369+
return []reconcile.Request{}
370+
}
371+
372+
// Search for a machines which would like to use this host.
373+
var found []reconcile.Request
374+
for i := range hbmmList.Items {
375+
hbmm := &hbmmList.Items[i]
376+
377+
// Skip if the hbmm is already in use.
378+
if hbmm.HasHostAnnotation() {
379+
continue
380+
}
381+
382+
hosts := []infrav1.HetznerBareMetalHost{*host}
383+
chosenHost, _, err := baremetal.ChooseHost(hbmm, hosts)
384+
if err != nil {
385+
log.Error(err, "failed to choose host for HetznerBareMetalMachine")
386+
continue
387+
}
388+
389+
// this hbmm does not match the host
390+
if chosenHost == nil {
391+
continue
392+
}
393+
394+
// We found a matching hbmm for the free host. Trigger Reconcile for the hbmm.
395+
found = append(found, reconcile.Request{
396+
NamespacedName: types.NamespacedName{
397+
Name: hbmm.Name,
398+
Namespace: hbmm.Namespace,
399+
},
400+
})
357401
}
358-
return []reconcile.Request{}
402+
return found
359403
}
360404
}

controllers/hetznerbaremetalmachine_controller_test.go

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

1919
import (
20+
"context"
21+
"testing"
2022
"time"
2123

24+
"github.com/go-logr/logr"
2225
. "github.com/onsi/ginkgo/v2"
2326
. "github.com/onsi/gomega"
2427
"github.com/stretchr/testify/mock"
28+
"github.com/stretchr/testify/require"
2529
"github.com/syself/hrobot-go/models"
2630
corev1 "k8s.io/api/core/v1"
2731
apierrors "k8s.io/apimachinery/pkg/api/errors"
2832
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/runtime"
2934
"k8s.io/apimachinery/pkg/selection"
35+
"k8s.io/apimachinery/pkg/types"
36+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3037
"k8s.io/utils/ptr"
3138
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3239
"sigs.k8s.io/cluster-api/util/patch"
3340
"sigs.k8s.io/controller-runtime/pkg/client"
41+
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
42+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3443

3544
infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
3645
"github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/baremetal"
@@ -825,4 +834,262 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
825834
})
826835
})
827836
})
837+
838+
Context("Test EnqueueRequestsFromMapFunc BareMetalHostToBareMetalMachines", func() {
839+
It("should enqueue the second HetznerBareMetalMachine, when the first gets deleted", func() {
840+
// We test this part in BareMetalHostToBareMetalMachines:
841+
// We have a free host. Trigger a matching HetznerBareMetalMachine to be reconciled.
842+
capiMachine := &clusterv1.Machine{
843+
ObjectMeta: metav1.ObjectMeta{
844+
GenerateName: "capi-machine-",
845+
Namespace: testNs.Name,
846+
Finalizers: []string{clusterv1.MachineFinalizer},
847+
Labels: map[string]string{
848+
clusterv1.ClusterNameLabel: capiCluster.Name,
849+
},
850+
},
851+
Spec: clusterv1.MachineSpec{
852+
ClusterName: capiCluster.Name,
853+
InfrastructureRef: corev1.ObjectReference{
854+
APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
855+
Kind: "HetznerBareMetalMachine",
856+
Name: bmMachineName,
857+
},
858+
FailureDomain: &defaultFailureDomain,
859+
Bootstrap: clusterv1.Bootstrap{
860+
DataSecretName: ptr.To("bootstrap-secret"),
861+
},
862+
},
863+
}
864+
Expect(testEnv.Create(ctx, capiMachine)).To(Succeed())
865+
866+
bmMachine = &infrav1.HetznerBareMetalMachine{
867+
ObjectMeta: metav1.ObjectMeta{
868+
Name: bmMachineName,
869+
Namespace: testNs.Name,
870+
Labels: map[string]string{
871+
clusterv1.ClusterNameLabel: capiCluster.Name,
872+
},
873+
OwnerReferences: []metav1.OwnerReference{
874+
{
875+
APIVersion: "cluster.x-k8s.io/v1beta1",
876+
Kind: "Machine",
877+
Name: capiMachine.Name,
878+
UID: capiMachine.UID,
879+
},
880+
},
881+
},
882+
Spec: getDefaultHetznerBareMetalMachineSpec(),
883+
}
884+
Expect(testEnv.Create(ctx, bmMachine)).To(Succeed())
885+
886+
// Create a second Capi- and HetznerBareMetalMachine
887+
bmMachineName2 := "hbmm2"
888+
capiMachine2 := &clusterv1.Machine{
889+
ObjectMeta: metav1.ObjectMeta{
890+
Name: utils.GenerateName(nil, "capimachine-name-"),
891+
Namespace: testNs.Name,
892+
Finalizers: []string{clusterv1.MachineFinalizer},
893+
Labels: map[string]string{
894+
clusterv1.ClusterNameLabel: capiCluster.Name,
895+
},
896+
},
897+
Spec: clusterv1.MachineSpec{
898+
ClusterName: capiCluster.Name,
899+
InfrastructureRef: corev1.ObjectReference{
900+
APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
901+
Kind: "HetznerBareMetalMachine",
902+
Name: bmMachineName2,
903+
},
904+
Bootstrap: clusterv1.Bootstrap{
905+
DataSecretName: ptr.To("bootstrap-secret"),
906+
},
907+
FailureDomain: &defaultFailureDomain,
908+
},
909+
}
910+
Expect(testEnv.Create(ctx, capiMachine2)).To(Succeed())
911+
912+
bmMachine2 := &infrav1.HetznerBareMetalMachine{
913+
ObjectMeta: metav1.ObjectMeta{
914+
Name: bmMachineName2,
915+
Namespace: testNs.Name,
916+
Labels: map[string]string{
917+
clusterv1.ClusterNameLabel: capiCluster.Name,
918+
},
919+
OwnerReferences: []metav1.OwnerReference{
920+
{
921+
APIVersion: "cluster.x-k8s.io/v1beta1",
922+
Kind: "Machine",
923+
Name: capiMachine2.Name,
924+
UID: capiMachine2.UID,
925+
},
926+
},
927+
},
928+
Spec: getDefaultHetznerBareMetalMachineSpec(),
929+
}
930+
Expect(testEnv.Create(ctx, bmMachine2)).To(Succeed())
931+
932+
osSSHSecret := helpers.GetDefaultSSHSecret("os-ssh-secret", testNs.Name)
933+
Expect(testEnv.Create(ctx, osSSHSecret)).To(Succeed())
934+
935+
host := helpers.BareMetalHost(
936+
hostName,
937+
testNs.Name,
938+
helpers.WithRootDeviceHintWWN(),
939+
helpers.WithHetznerClusterRef(hetznerClusterName),
940+
)
941+
Expect(testEnv.Create(ctx, host)).To(Succeed())
942+
943+
// Wait until bmMachine is provisioned.
944+
var pickedMachine *infrav1.HetznerBareMetalMachine
945+
var waitingMachine *infrav1.HetznerBareMetalMachine
946+
947+
// The mock should wait until we know which machine is picked.
948+
w := make(chan time.Time)
949+
osSSHClient.On("GetHostName").WaitUntil(w)
950+
951+
Eventually(func() bool {
952+
if err := testEnv.Get(ctx, client.ObjectKeyFromObject(host), host); err != nil {
953+
return false
954+
}
955+
if host.Spec.ConsumerRef == nil {
956+
return false
957+
}
958+
959+
if host.Spec.ConsumerRef.Name == bmMachineName {
960+
pickedMachine = bmMachine
961+
waitingMachine = bmMachine2
962+
} else {
963+
pickedMachine = bmMachine2
964+
waitingMachine = bmMachine
965+
}
966+
967+
if w != nil {
968+
// We need to change the mock on-the-fly. There are two
969+
// machines, and we need to adapt the mock to the one that is
970+
// picked.
971+
osSSHClient.On("GetHostName").Return(sshclient.Output{
972+
StdOut: infrav1.BareMetalHostNamePrefix + pickedMachine.Name,
973+
StdErr: "",
974+
Err: nil,
975+
})
976+
// Close the channel, so that mock "GetHostName" does no longer block.
977+
close(w)
978+
979+
// We updated the mock, and closed the channel. Set channel to nil,
980+
// so that we don't mock and close again.
981+
// Closing a closed channel would panic.
982+
w = nil
983+
}
984+
985+
return host.Spec.Status.ProvisioningState != infrav1.StateProvisioned
986+
}, timeout).Should(BeTrue())
987+
988+
// Ensure the second machine is not ready (waiting for a hbmh)
989+
Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(waitingMachine),
990+
waitingMachine)).To(Succeed())
991+
Expect(waitingMachine.Status.Ready).To(BeFalse())
992+
993+
// Delete the first machine
994+
Expect(testEnv.Delete(ctx, pickedMachine)).To(Succeed())
995+
996+
// Wait until the waiting machine is ready
997+
Eventually(func() bool {
998+
Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(waitingMachine),
999+
waitingMachine)).To(Succeed())
1000+
return waitingMachine.Status.Ready
1001+
}, timeout).Should(BeTrue())
1002+
})
1003+
})
8281004
})
1005+
1006+
func Test_BareMetalHostToBareMetalMachines(t *testing.T) {
1007+
ctx := context.Background()
1008+
scheme := runtime.NewScheme()
1009+
utilruntime.Must(infrav1.AddToScheme(scheme))
1010+
host := &infrav1.HetznerBareMetalHost{
1011+
ObjectMeta: metav1.ObjectMeta{
1012+
Name: "test-host",
1013+
Namespace: "test-ns",
1014+
},
1015+
}
1016+
hbmm := &infrav1.HetznerBareMetalMachine{
1017+
ObjectMeta: metav1.ObjectMeta{
1018+
Name: "test-machine-with-label-foo",
1019+
Namespace: "test-ns",
1020+
},
1021+
Spec: infrav1.HetznerBareMetalMachineSpec{
1022+
HostSelector: infrav1.HostSelector{
1023+
MatchLabels: map[string]string{
1024+
"key": "foo",
1025+
},
1026+
},
1027+
},
1028+
}
1029+
hbmmWithHostAnnotation := &infrav1.HetznerBareMetalMachine{
1030+
ObjectMeta: metav1.ObjectMeta{
1031+
Name: "test-machine-with-host-annotation",
1032+
Namespace: "test-ns",
1033+
Annotations: map[string]string{
1034+
infrav1.HostAnnotation: "test-host",
1035+
},
1036+
},
1037+
Spec: infrav1.HetznerBareMetalMachineSpec{
1038+
HostSelector: infrav1.HostSelector{
1039+
MatchLabels: map[string]string{
1040+
"key": "foo",
1041+
},
1042+
},
1043+
},
1044+
}
1045+
1046+
hbmmWithoutLabel := &infrav1.HetznerBareMetalMachine{
1047+
ObjectMeta: metav1.ObjectMeta{
1048+
Name: "test-machine-with-label-bar",
1049+
Namespace: "test-ns",
1050+
},
1051+
Spec: infrav1.HetznerBareMetalMachineSpec{
1052+
HostSelector: infrav1.HostSelector{
1053+
MatchLabels: map[string]string{
1054+
"key": "bar",
1055+
},
1056+
},
1057+
},
1058+
}
1059+
hbmmOtherNS := &infrav1.HetznerBareMetalMachine{
1060+
ObjectMeta: metav1.ObjectMeta{
1061+
Name: "test-machine-other-namespace",
1062+
Namespace: "test-other-namespace",
1063+
},
1064+
Spec: infrav1.HetznerBareMetalMachineSpec{
1065+
HostSelector: infrav1.HostSelector{
1066+
MatchLabels: map[string]string{
1067+
"key": "foo",
1068+
},
1069+
},
1070+
},
1071+
}
1072+
c := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(
1073+
host, hbmm, hbmmOtherNS, hbmmWithoutLabel, hbmmWithHostAnnotation).Build()
1074+
1075+
// host does not have a label.
1076+
f := BareMetalHostToBareMetalMachines(c, logr.Logger{})
1077+
result := f(ctx, host)
1078+
require.Nil(t, result)
1079+
host.Labels = map[string]string{
1080+
"key": "foo",
1081+
}
1082+
1083+
// host has label "foo"
1084+
err := c.Update(ctx, host)
1085+
require.NoError(t, err)
1086+
result = f(ctx, host)
1087+
require.Equal(t, []reconcile.Request{
1088+
{
1089+
NamespacedName: types.NamespacedName{
1090+
Name: "test-machine-with-label-foo",
1091+
Namespace: "test-ns",
1092+
},
1093+
},
1094+
}, result)
1095+
}

0 commit comments

Comments
 (0)