@@ -17,20 +17,29 @@ limitations under the License.
1717package controllers
1818
1919import (
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