diff --git a/pkg/controllers/sipcluster_controller_test.go b/pkg/controllers/sipcluster_controller_test.go index 1157743..6352aa1 100644 --- a/pkg/controllers/sipcluster_controller_test.go +++ b/pkg/controllers/sipcluster_controller_test.go @@ -54,10 +54,19 @@ var _ = Describe("SIPCluster controller", func() { // Create vBMH test objects nodes := []airshipv1.VMRole{airshipv1.VMControlPlane, airshipv1.VMControlPlane, airshipv1.VMControlPlane, airshipv1.VMWorker, airshipv1.VMWorker, airshipv1.VMWorker, airshipv1.VMWorker} + bmcUsername := "root" + bmcPassword := "test" for node, role := range nodes { vBMH, networkData := testutil.CreateBMH(node, testNamespace, role, 6) + bmcSecret := testutil.CreateBMCAuthSecret(vBMH.Name, vBMH.Namespace, bmcUsername, + bmcPassword) + + vBMH.Spec.BMC.CredentialsName = bmcSecret.Name + + Expect(k8sClient.Create(context.Background(), bmcSecret)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), vBMH)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), networkData)).Should(Succeed()) + } // Create SIP cluster diff --git a/pkg/vbmh/errors.go b/pkg/vbmh/errors.go index bf622b3..69f8006 100644 --- a/pkg/vbmh/errors.go +++ b/pkg/vbmh/errors.go @@ -54,3 +54,13 @@ type ErrorNetworkDataNotFound struct { func (e ErrorNetworkDataNotFound) Error() string { return fmt.Sprintf("vBMH Host %v does not define NetworkData, but is required for scheduling.", e.BMH) } + +// ErrMalformedManagementCredentials occurs when a BMC credentials secret does not contain username and password fields. +type ErrMalformedManagementCredentials struct { + SecretName string +} + +func (e ErrMalformedManagementCredentials) Error() string { + return fmt.Sprintf("secret %s contains malformed management credentials. Must contain '%s' and '%s' fields.", + e.SecretName, keyBMCUsername, keyBMCPassword) +} diff --git a/pkg/vbmh/machines.go b/pkg/vbmh/machines.go index adc5205..2d34fe8 100644 --- a/pkg/vbmh/machines.go +++ b/pkg/vbmh/machines.go @@ -78,6 +78,12 @@ const ( SipNodeTypeLabel = BaseAirshipSelector + "/" + SipNodeTypeLabelName ) +// Keys used to retrieve credentials from the BMC credentials secret +const ( + keyBMCUsername = "username" + keyBMCPassword = "password" +) + // MAchine represents an individual BMH CR, and the appropriate // attributes required to manage the SIP Cluster scheduling and // rocesing needs about thhem @@ -118,6 +124,8 @@ type MachineData struct { // Collect all IP's for the interfaces defined // In the list of Services IPOnInterface map[string]string + BMCUsername string + BMCPassword string } // MachineList contains the list of Scheduled or ToBeScheduled machines @@ -428,6 +436,26 @@ func (ml *MachineList) Extrapolate(sip airshipv1.SIPCluster, c client.Client) bo ml.ReadyForScheduleCount[machine.VMRole]-- extrapolateSuccess = false } + + // Retrieve BMC credentials + mgmtCredsSecret := &corev1.Secret{} + err = c.Get(context.Background(), client.ObjectKey{ + Namespace: bmh.Namespace, + Name: bmh.Spec.BMC.CredentialsName, + }, mgmtCredsSecret) + if err != nil { + machine.ScheduleStatus = UnableToSchedule + ml.ReadyForScheduleCount[machine.VMRole]-- + extrapolateSuccess = false + } + + // Parse BMC credentials from Secret + err = ml.getMangementCredentials(machine, mgmtCredsSecret) + if err != nil { + machine.ScheduleStatus = UnableToSchedule + ml.ReadyForScheduleCount[machine.VMRole]-- + extrapolateSuccess = false + } } fmt.Printf("Schedule.Extrapolate extrapolateSuccess:%t\n", extrapolateSuccess) return extrapolateSuccess @@ -623,6 +651,23 @@ func (ml *MachineList) getIP(machine *Machine, networkDataSecret *corev1.Secret, return nil } +// getManagementCredentials retrieves BMC credentials from a Kubernetes secret. +func (ml *MachineList) getMangementCredentials(machine *Machine, secret *corev1.Secret) error { + username, exists := secret.Data[keyBMCUsername] + if !exists { + return ErrMalformedManagementCredentials{SecretName: secret.Name} + } + machine.Data.BMCUsername = string(username) + + password, exists := secret.Data[keyBMCPassword] + if !exists { + return ErrMalformedManagementCredentials{SecretName: secret.Name} + } + machine.Data.BMCPassword = string(password) + + return nil +} + /* ScheduleSet is a simple object to encapsulate data that helps our poor man scheduler diff --git a/pkg/vbmh/vbmh_test.go b/pkg/vbmh/vbmh_test.go index 6766980..d42fa6e 100644 --- a/pkg/vbmh/vbmh_test.go +++ b/pkg/vbmh/vbmh_test.go @@ -95,12 +95,23 @@ var _ = Describe("MachineList", func() { It("Should retrieve the BMH IP from the BMH's NetworkData secret when infra services are defined", func() { // Create a BMH with a NetworkData secret - bmh, secret := testutil.CreateBMH(1, "default", airshipv1.VMControlPlane, 6) + bmh, networkData := testutil.CreateBMH(1, "default", airshipv1.VMControlPlane, 6) + + // Create BMH and NetworkData secret + var objsToApply []runtime.Object + objsToApply = append(objsToApply, bmh) + objsToApply = append(objsToApply, networkData) + + // Create BMC credential secret + username := "root" + password := "test" + bmcSecret := testutil.CreateBMCAuthSecret(bmh.Name, bmh.Namespace, username, password) + + bmh.Spec.BMC.CredentialsName = bmcSecret.Name + objsToApply = append(objsToApply, bmcSecret) + m, err := NewMachine(*bmh, airshipv1.VMControlPlane, NotScheduled) Expect(err).To(BeNil()) - var objs []runtime.Object - objs = append(objs, bmh) - objs = append(objs, secret) ml := &MachineList{ NamespacedName: types.NamespacedName{ @@ -126,21 +137,31 @@ var _ = Describe("MachineList", func() { }, }, } - k8sClient := mockClient.NewFakeClient(objs...) + k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.Extrapolate(*sipCluster, k8sClient)).To(BeTrue()) - // NOTE(drewwalters96): Interface data is b64 encoded in the testutil convenience function. Expect(ml.Machines[bmh.Name].Data.IPOnInterface).To(Equal(map[string]string{"oam-ipv4": "32.68.51.139"})) }) It("Should not retrieve the BMH IP from the BMH's NetworkData secret if no infraServices are defined", func() { // Create a BMH with a NetworkData secret - bmh, secret := testutil.CreateBMH(1, "default", airshipv1.VMControlPlane, 6) + bmh, networkData := testutil.CreateBMH(1, "default", airshipv1.VMControlPlane, 6) + + // Create BMH and NetworkData secret + var objsToApply []runtime.Object + objsToApply = append(objsToApply, bmh) + objsToApply = append(objsToApply, networkData) + + // Create BMC credential secret + username := "root" + password := "test" + bmcSecret := testutil.CreateBMCAuthSecret(bmh.Name, bmh.Namespace, username, password) + + bmh.Spec.BMC.CredentialsName = bmcSecret.Name + objsToApply = append(objsToApply, bmcSecret) + m, err := NewMachine(*bmh, airshipv1.VMControlPlane, NotScheduled) Expect(err).To(BeNil()) - var objs []runtime.Object - objs = append(objs, bmh) - objs = append(objs, secret) ml := &MachineList{ NamespacedName: types.NamespacedName{ @@ -153,10 +174,69 @@ var _ = Describe("MachineList", func() { Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), } - k8sClient := mockClient.NewFakeClient(objs...) sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster.Spec.Services = airshipv1.SIPClusterServices{ + LoadBalancer: []airshipv1.SIPClusterService{ + { + Image: "haproxy:latest", + NodeLabels: map[string]string{ + "test": "true", + }, + NodePort: 30000, + NodeInterface: "oam-ipv4", + }, + }, + } + k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.Extrapolate(*sipCluster, k8sClient)).To(BeTrue()) - Expect(len(ml.Machines[bmh.Name].Data.IPOnInterface)).To(Equal(0)) + + Expect(ml.Machines[bmh.Name].Data.BMCUsername).To(Equal(username)) + Expect(ml.Machines[bmh.Name].Data.BMCPassword).To(Equal(password)) + }) + + It("Should not process a BMH when its BMC secret is missing", func() { + var objsToApply []runtime.Object + + // Create BMH and NetworkData secret + bmh, networkData := testutil.CreateBMH(1, "default", "master", 6) + objsToApply = append(objsToApply, bmh) + objsToApply = append(objsToApply, networkData) + + bmh.Spec.BMC.CredentialsName = "foo-does-not-exist" + + m, err := NewMachine(*bmh, airshipv1.VMControlPlane, NotScheduled) + Expect(err).To(BeNil()) + + ml := &MachineList{ + NamespacedName: types.NamespacedName{ + Name: "vbmh", + Namespace: "default", + }, + Machines: map[string]*Machine{ + bmh.Name: m, + }, + ReadyForScheduleCount: map[airshipv1.VMRole]int{ + airshipv1.VMControlPlane: 1, + airshipv1.VMWorker: 0, + }, + Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), + } + + sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster.Spec.Services = airshipv1.SIPClusterServices{ + LoadBalancer: []airshipv1.SIPClusterService{ + { + Image: "haproxy:latest", + NodeLabels: map[string]string{ + "test": "true", + }, + NodePort: 30000, + NodeInterface: "oam-ipv4", + }, + }, + } + k8sClient := mockClient.NewFakeClient(objsToApply...) + Expect(ml.Extrapolate(*sipCluster, k8sClient)).To(BeFalse()) }) It("Should not retrieve the BMH IP if it has been previously extrapolated", func() { diff --git a/testutil/testutil.go b/testutil/testutil.go index 63172ea..cf4fa40 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -256,3 +256,22 @@ func CreateSIPCluster(name string, namespace string, controlPlanes int, workers Status: airshipv1.SIPClusterStatus{}, } } + +// CreateBMCAuthSecret creates a K8s Secret that matches the Metal3.io BaremetalHost credential format for use in test +// cases. +func CreateBMCAuthSecret(nodeName string, namespace string, username string, password string) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-bmc-credentials", nodeName), + Namespace: namespace, + }, + Data: map[string][]byte{ + "username": []byte(username), + "password": []byte(password), + }, + } +}