diff --git a/api/v1alpha1/bmc_types.go b/api/v1alpha1/bmc_types.go index 3bb52ee..4049775 100644 --- a/api/v1alpha1/bmc_types.go +++ b/api/v1alpha1/bmc_types.go @@ -19,7 +19,13 @@ const ( type BMCSpec struct { // EndpointRef is a reference to the Kubernetes object that contains the endpoint information for the BMC. // This reference is typically used to locate the BMC endpoint within the cluster. - EndpointRef v1.LocalObjectReference `json:"endpointRef"` + // +optional + EndpointRef *v1.LocalObjectReference `json:"endpointRef"` + + // Access allows inline configuration of network access details for the BMC. + // Use this field if access settings like address are to be configured directly within the BMC resource. + // +optional + Access *Access `json:"access,omitempty"` // BMCSecretRef is a reference to the Kubernetes Secret object that contains the credentials // required to access the BMC. This secret includes sensitive information such as usernames and passwords. @@ -35,6 +41,12 @@ type BMCSpec struct { ConsoleProtocol *ConsoleProtocol `json:"consoleProtocol,omitempty"` } +// Access defines inline network access configuration for the BMC. +type Access struct { + // Address is the IP or hostname used for accessing the BMC. + Address string `json:"address"` +} + // ConsoleProtocol defines the protocol and port used for console access to the BMC. type ConsoleProtocol struct { // Name specifies the name of the console protocol. diff --git a/api/v1alpha1/bmc_webhook.go b/api/v1alpha1/bmc_webhook.go new file mode 100644 index 0000000..a8e3b6d --- /dev/null +++ b/api/v1alpha1/bmc_webhook.go @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var bmclog = logf.Log.WithName("bmc-resource") + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *BMC) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:path=/validate-metal-ironcore-dev-v1alpha1-bmc,mutating=false,failurePolicy=fail,sideEffects=None,groups=metal.ironcore.dev,resources=bmcs,verbs=create;update,versions=v1alpha1,name=vbmc.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &BMC{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *BMC) ValidateCreate() (admission.Warnings, error) { + bmclog.Info("validate create", "name", r.Name) + + allErrs := field.ErrorList{} + allErrs = append(allErrs, ValidateCreateBMCSpec(r.Spec, field.NewPath("spec"))...) + + if len(allErrs) != 0 { + return nil, apierrors.NewInvalid( + schema.GroupKind{Group: "metal.ironcore.dev", Kind: "BMC"}, + r.Name, allErrs) + } + + return nil, nil +} + +func ValidateCreateBMCSpec(spec BMCSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if spec.EndpointRef != nil && spec.Access != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("endpointRef"), spec.EndpointRef, "only one of 'endpointRef' or 'access' should be specified")) + allErrs = append(allErrs, field.Invalid(fldPath.Child("access"), spec.Access, "only one of 'endpointRef' or 'access' should be specified")) + } + if spec.EndpointRef == nil && spec.Access == nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("endpointRef"), spec.EndpointRef, "either 'endpointRef' or 'access' must be specified")) + allErrs = append(allErrs, field.Invalid(fldPath.Child("access"), spec.Access, "either 'endpointRef' or 'access' must be specified")) + } + + return allErrs +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *BMC) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + bmclog.Info("validate update", "name", r.Name) + allErrs := field.ErrorList{} + + allErrs = append(allErrs, ValidateCreateBMCSpec(r.Spec, field.NewPath("spec"))...) + + if len(allErrs) != 0 { + return nil, apierrors.NewInvalid( + schema.GroupKind{Group: "metal.ironcore.dev", Kind: "BMC"}, + r.Name, allErrs) + } + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *BMC) ValidateDelete() (admission.Warnings, error) { + bmclog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil, nil +} diff --git a/api/v1alpha1/bmc_webhook_test.go b/api/v1alpha1/bmc_webhook_test.go new file mode 100644 index 0000000..26cf10f --- /dev/null +++ b/api/v1alpha1/bmc_webhook_test.go @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" +) + +var _ = Describe("BMC Webhook", func() { + _ = SetupTest() + + It("Should deny if the BMC has EndpointRef and Access spec fields", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-invalid", + }, + Spec: BMCSpec{ + EndpointRef: &v1.LocalObjectReference{Name: "foo"}, + Access: &Access{ + Address: "http://localhost:8080", + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(HaveOccurred()) + Eventually(Get(bmc)).Should(Satisfy(errors.IsNotFound)) + }) + + It("Should deny if the BMC has no EndpointRef and Access spec fields", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-empty", + }, + Spec: BMCSpec{}, + } + Expect(k8sClient.Create(ctx, bmc)).To(HaveOccurred()) + Eventually(Get(bmc)).Should(Satisfy(errors.IsNotFound)) + }) + + It("Should admit if the BMC has an EndpointRef but no Access spec field", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: BMCSpec{ + EndpointRef: &v1.LocalObjectReference{Name: "foo"}, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + }) + + It("Should deny if the BMC EndpointRef spec field has been removed", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: BMCSpec{ + EndpointRef: &v1.LocalObjectReference{Name: "foo"}, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + + Eventually(Update(bmc, func() { + bmc.Spec.EndpointRef = nil + })).Should(Not(Succeed())) + + Eventually(Object(bmc)).Should(SatisfyAll(HaveField( + "Spec.EndpointRef", &v1.LocalObjectReference{Name: "foo"}))) + }) + + It("Should admit if the BMC is changing EndpointRef to Access spec field", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: BMCSpec{ + EndpointRef: &v1.LocalObjectReference{Name: "foo"}, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + + Eventually(Update(bmc, func() { + bmc.Spec.EndpointRef = nil + bmc.Spec.Access = &Access{ + Address: "http://localhost:8080", + } + })).Should(Succeed()) + }) + + It("Should admit if the BMC has no EndpointRef but an Access spec field", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: BMCSpec{ + Access: &Access{ + Address: "http://localhost:8080", + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + }) + + It("Should deny if the BMC Access spec field has been removed", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: BMCSpec{ + Access: &Access{ + Address: "http://localhost:8080", + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + + Eventually(Update(bmc, func() { + bmc.Spec.Access = nil + })).Should(Not(Succeed())) + + Eventually(Object(bmc)).Should(SatisfyAll(HaveField( + "Spec.Access.Address", "http://localhost:8080"))) + }) + + It("Should admit if the BMC has is changing to an EndpointRef from an Access spec field", func() { + bmc := &BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: BMCSpec{ + Access: &Access{ + Address: "http://localhost:8080", + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + + Eventually(Update(bmc, func() { + bmc.Spec.EndpointRef = &v1.LocalObjectReference{Name: "foo"} + bmc.Spec.Access = nil + })).Should(Succeed()) + }) + +}) diff --git a/api/v1alpha1/bmcsecret_types.go b/api/v1alpha1/bmcsecret_types.go index d72a76c..8bcc27c 100644 --- a/api/v1alpha1/bmcsecret_types.go +++ b/api/v1alpha1/bmcsecret_types.go @@ -8,6 +8,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // BMCSecretUsernameKeyName is the secret key name for the username. + BMCSecretUsernameKeyName = "username" + // BMCSecretPasswordKeyName is the secret key name for the password.F + BMCSecretPasswordKeyName = "password" +) + //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:scope=Cluster diff --git a/api/v1alpha1/server_types.go b/api/v1alpha1/server_types.go index 6d8c3f7..b1c1122 100644 --- a/api/v1alpha1/server_types.go +++ b/api/v1alpha1/server_types.go @@ -48,8 +48,8 @@ type BMCAccess struct { // Protocol specifies the protocol to be used for communicating with the BMC. Protocol Protocol `json:"protocol"` - // Endpoint is the address of the BMC endpoint. - Endpoint string `json:"endpoint"` + // Address is the address of the BMC. + Address string `json:"address"` // BMCSecretRef is a reference to the Kubernetes Secret object that contains the credentials // required to access the BMC. This secret includes sensitive information such as usernames and passwords. diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go index 397a905..b73856a 100644 --- a/api/v1alpha1/webhook_suite_test.go +++ b/api/v1alpha1/webhook_suite_test.go @@ -92,8 +92,7 @@ var _ = BeforeSuite(func() { Expect(corev1.AddToScheme(scheme)).To(Succeed()) Expect(err).NotTo(HaveOccurred()) - err = admissionv1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) + Expect(admissionv1.AddToScheme(scheme)).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme @@ -115,8 +114,8 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&ServerClaim{}).SetupWebhookWithManager(mgr) - Expect(err).NotTo(HaveOccurred()) + Expect((&ServerClaim{}).SetupWebhookWithManager(mgr)).NotTo(HaveOccurred()) + Expect((&BMC{}).SetupWebhookWithManager(mgr)).NotTo(HaveOccurred()) //+kubebuilder:scaffold:webhook diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index bedf466..44eb57a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -8,11 +8,26 @@ package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Access) DeepCopyInto(out *Access) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Access. +func (in *Access) DeepCopy() *Access { + if in == nil { + return nil + } + out := new(Access) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BIOSSettings) DeepCopyInto(out *BIOSSettings) { *out = *in @@ -199,7 +214,16 @@ func (in *BMCSecretList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BMCSpec) DeepCopyInto(out *BMCSpec) { *out = *in - out.EndpointRef = in.EndpointRef + if in.EndpointRef != nil { + in, out := &in.EndpointRef, &out.EndpointRef + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.Access != nil { + in, out := &in.Access, &out.Access + *out = new(Access) + **out = **in + } out.BMCSecretRef = in.BMCSecretRef out.Protocol = in.Protocol if in.ConsoleProtocol != nil { @@ -225,7 +249,7 @@ func (in *BMCStatus) DeepCopyInto(out *BMCStatus) { in.IP.DeepCopyInto(&out.IP) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -485,7 +509,7 @@ func (in *ServerBootConfigurationSpec) DeepCopyInto(out *ServerBootConfiguration out.ServerRef = in.ServerRef if in.IgnitionSecretRef != nil { in, out := &in.IgnitionSecretRef, &out.IgnitionSecretRef - *out = new(corev1.LocalObjectReference) + *out = new(v1.LocalObjectReference) **out = **in } } @@ -579,17 +603,17 @@ func (in *ServerClaimSpec) DeepCopyInto(out *ServerClaimSpec) { *out = *in if in.ServerRef != nil { in, out := &in.ServerRef, &out.ServerRef - *out = new(corev1.LocalObjectReference) + *out = new(v1.LocalObjectReference) **out = **in } if in.ServerSelector != nil { in, out := &in.ServerSelector, &out.ServerSelector - *out = new(v1.LabelSelector) + *out = new(metav1.LabelSelector) (*in).DeepCopyInto(*out) } if in.IgnitionSecretRef != nil { in, out := &in.IgnitionSecretRef, &out.IgnitionSecretRef - *out = new(corev1.LocalObjectReference) + *out = new(v1.LocalObjectReference) **out = **in } } @@ -656,12 +680,12 @@ func (in *ServerSpec) DeepCopyInto(out *ServerSpec) { *out = *in if in.ServerClaimRef != nil { in, out := &in.ServerClaimRef, &out.ServerClaimRef - *out = new(corev1.ObjectReference) + *out = new(v1.ObjectReference) **out = **in } if in.BMCRef != nil { in, out := &in.BMCRef, &out.BMCRef - *out = new(corev1.LocalObjectReference) + *out = new(v1.LocalObjectReference) **out = **in } if in.BMC != nil { @@ -671,7 +695,7 @@ func (in *ServerSpec) DeepCopyInto(out *ServerSpec) { } if in.BootConfigurationRef != nil { in, out := &in.BootConfigurationRef, &out.BootConfigurationRef - *out = new(corev1.ObjectReference) + *out = new(v1.ObjectReference) **out = **in } if in.BootOrder != nil { @@ -718,7 +742,7 @@ func (in *ServerStatus) DeepCopyInto(out *ServerStatus) { in.BIOS.DeepCopyInto(&out.BIOS) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/bmc/bmc.go b/bmc/bmc.go index 67b478c..5367d35 100644 --- a/bmc/bmc.go +++ b/bmc/bmc.go @@ -179,4 +179,5 @@ type Manager struct { Model string PowerState string State string + MACAddress string } diff --git a/cmd/manager/main.go b/cmd/manager/main.go index ec539ec..8a05242 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -247,6 +247,12 @@ func main() { os.Exit(1) } } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&metalv1alpha1.BMC{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "BMC") + os.Exit(1) + } + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/metal.ironcore.dev_bmcs.yaml b/config/crd/bases/metal.ironcore.dev_bmcs.yaml index c6ac278..79d65d0 100644 --- a/config/crd/bases/metal.ironcore.dev_bmcs.yaml +++ b/config/crd/bases/metal.ironcore.dev_bmcs.yaml @@ -70,6 +70,18 @@ spec: spec: description: BMCSpec defines the desired state of BMC properties: + access: + description: |- + Access allows inline configuration of network access details for the BMC. + Use this field if access settings like address are to be configured directly within the BMC resource. + properties: + address: + description: Address is the IP or hostname used for accessing + the BMC. + type: string + required: + - address + type: object bmcSecretRef: description: |- BMCSecretRef is a reference to the Kubernetes Secret object that contains the credentials @@ -144,7 +156,6 @@ spec: type: object required: - bmcSecretRef - - endpointRef - protocol type: object status: diff --git a/config/crd/bases/metal.ironcore.dev_servers.yaml b/config/crd/bases/metal.ironcore.dev_servers.yaml index 50265a7..db862d7 100644 --- a/config/crd/bases/metal.ironcore.dev_servers.yaml +++ b/config/crd/bases/metal.ironcore.dev_servers.yaml @@ -94,6 +94,9 @@ spec: BMC contains the access details for the BMC. This field is optional and can be omitted if no BMC access is specified. properties: + address: + description: Address is the address of the BMC. + type: string bmcSecretRef: description: |- BMCSecretRef is a reference to the Kubernetes Secret object that contains the credentials @@ -110,9 +113,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - endpoint: - description: Endpoint is the address of the BMC endpoint. - type: string protocol: description: Protocol specifies the protocol to be used for communicating with the BMC. @@ -133,8 +133,8 @@ spec: - port type: object required: + - address - bmcSecretRef - - endpoint - protocol type: object bmcRef: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index d79d8c7..1a362d8 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -15,7 +15,7 @@ patches: # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_endpoints.yaml #- path: patches/webhook_in_bmcsecrets.yaml -#- path: patches/webhook_in_bmcs.yaml +- path: patches/webhook_in_bmcs.yaml #- path: patches/webhook_in_servers.yaml #- path: patches/webhook_in_serverbootconfigurations.yaml - path: patches/webhook_in_serverclaims.yaml diff --git a/config/crd/patches/cainjection_in_bmcs.yaml b/config/crd/patches/cainjection_in_bmcs.yaml new file mode 100644 index 0000000..0a920ae --- /dev/null +++ b/config/crd/patches/cainjection_in_bmcs.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: bmcs.metal.ironcore.dev diff --git a/config/crd/patches/webhook_in_bmcs.yaml b/config/crd/patches/webhook_in_bmcs.yaml new file mode 100644 index 0000000..522f506 --- /dev/null +++ b/config/crd/patches/webhook_in_bmcs.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bmcs.metal.ironcore.dev +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/network-policy/allow-webhook-traffic.yaml b/config/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 0000000..2cd13ae --- /dev/null +++ b/config/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,26 @@ +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: allow-webhook-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 5e3071b..b6d7f3e 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -4,6 +4,26 @@ kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-metal-ironcore-dev-v1alpha1-bmc + failurePolicy: Fail + name: vbmc.kb.io + rules: + - apiGroups: + - metal.ironcore.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - bmcs + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/internal/controller/bmc_controller.go b/internal/controller/bmc_controller.go index cba09a4..dcff2c8 100644 --- a/internal/controller/bmc_controller.go +++ b/internal/controller/bmc_controller.go @@ -6,6 +6,7 @@ package controller import ( "context" "fmt" + "net" "k8s.io/apimachinery/pkg/api/errors" @@ -86,18 +87,41 @@ func (r *BMCReconciler) reconcile(ctx context.Context, log logr.Logger, bmcObj * } func (r *BMCReconciler) updateBMCStatusDetails(ctx context.Context, log logr.Logger, bmcObj *metalv1alpha1.BMC) error { - endpoint := &metalv1alpha1.Endpoint{} - if err := r.Get(ctx, client.ObjectKey{Name: bmcObj.Spec.EndpointRef.Name}, endpoint); err != nil { - if errors.IsNotFound(err) { - return nil + var ( + ip metalv1alpha1.IP + macAddress string + ) + + if bmcObj.Spec.EndpointRef != nil { + endpoint := &metalv1alpha1.Endpoint{} + if err := r.Get(ctx, client.ObjectKey{Name: bmcObj.Spec.EndpointRef.Name}, endpoint); err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to get Endpoints for BMC: %w", err) + } + log.V(1).Info("Got Endpoints for BMC", "Endpoints", endpoint.Name) + } + + if bmcObj.Spec.Access != nil { + ips, err := net.LookupIP(bmcObj.Spec.Access.Address) + if err != nil { + return fmt.Errorf("failed to lookup IP for BMC address: %w", err) + } + if len(ips) > 0 { + // pick the the IPv4 address + // TODO: handle multiple IPs for a BMC (IPv4 and IPv6) + for _, ipaddress := range ips { + if ipaddress.To4() != nil { + ip = metalv1alpha1.MustParseIP(ipaddress.String()) + } + } } - return fmt.Errorf("failed to get Endpoints for BMC: %w", err) } - log.V(1).Info("Got Endpoints for BMC", "Endpoints", endpoint.Name) bmcBase := bmcObj.DeepCopy() - bmcObj.Status.IP = endpoint.Spec.IP - bmcObj.Status.MACAddress = endpoint.Spec.MACAddress + bmcObj.Status.IP = ip + bmcObj.Status.MACAddress = macAddress if err := r.Status().Patch(ctx, bmcObj, client.MergeFrom(bmcBase)); err != nil { return fmt.Errorf("failed to patch IP and MAC address status: %w", err) } diff --git a/internal/controller/bmc_controller_test.go b/internal/controller/bmc_controller_test.go index cb7fac2..b6b4343 100644 --- a/internal/controller/bmc_controller_test.go +++ b/internal/controller/bmc_controller_test.go @@ -7,6 +7,7 @@ import ( metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" @@ -100,4 +101,70 @@ var _ = Describe("BMC Controller", func() { HaveField("Spec.BMCRef.Name", endpoint.Name), )) }) + + It("Should successfully reconcile the a BMC resource with inline access information", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmcSecret) + + By("Creating a BMC resource") + bmc := &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: metalv1alpha1.BMCSpec{ + Access: &metalv1alpha1.Access{ + Address: "localhost", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: 8000, + }, + BMCSecretRef: v1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Status.IP", metalv1alpha1.MustParseIP("127.0.0.1")), + // TODO: in the inline access case we can't easily retrieve the MACAddress of a BMC + HaveField("Status.MACAddress", ""), + HaveField("Status.Model", "Joo Janta 200"), + HaveField("Status.State", metalv1alpha1.BMCStateEnabled), + HaveField("Status.PowerState", metalv1alpha1.OnPowerState), + HaveField("Status.FirmwareVersion", "1.45.455b66-rev4"), + )) + + By("Ensuring that the Server resource has been created") + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetServerNameFromBMCandIndex(0, bmc), + }, + } + Eventually(Object(server)).Should(SatisfyAll( + HaveField("OwnerReferences", ContainElement(metav1.OwnerReference{ + APIVersion: "metal.ironcore.dev/v1alpha1", + Kind: "BMC", + Name: bmc.Name, + UID: bmc.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + })), + HaveField("Spec.UUID", "38947555-7742-3448-3784-823347823834"), + HaveField("Spec.BMCRef.Name", bmc.Name), + )) + }) + }) diff --git a/internal/controller/bmcutils.go b/internal/controller/bmcutils.go index 49dcbf3..79cdac3 100644 --- a/internal/controller/bmcutils.go +++ b/internal/controller/bmcutils.go @@ -36,7 +36,7 @@ func GetBMCClientForServer(ctx context.Context, c client.Client, server *metalv1 c, insecure, server.Spec.BMC.Protocol.Name, - metalv1alpha1.MustParseIP(server.Spec.BMC.Endpoint), + server.Spec.BMC.Address, server.Spec.BMC.Protocol.Port, bmcSecret, ) @@ -46,9 +46,18 @@ func GetBMCClientForServer(ctx context.Context, c client.Client, server *metalv1 } func GetBMCClientFromBMC(ctx context.Context, c client.Client, bmcObj *metalv1alpha1.BMC, insecure bool) (bmc.BMC, error) { - endpoint := &metalv1alpha1.Endpoint{} - if err := c.Get(ctx, client.ObjectKey{Name: bmcObj.Spec.EndpointRef.Name}, endpoint); err != nil { - return nil, fmt.Errorf("failed to get Endpoints for BMC: %w", err) + var address string + + if bmcObj.Spec.EndpointRef != nil { + endpoint := &metalv1alpha1.Endpoint{} + if err := c.Get(ctx, client.ObjectKey{Name: bmcObj.Spec.EndpointRef.Name}, endpoint); err != nil { + return nil, fmt.Errorf("failed to get Endpoints for BMC: %w", err) + } + address = endpoint.Spec.IP.String() + } + + if bmcObj.Spec.Access != nil { + address = bmcObj.Spec.Access.Address } bmcSecret := &metalv1alpha1.BMCSecret{} @@ -56,10 +65,10 @@ func GetBMCClientFromBMC(ctx context.Context, c client.Client, bmcObj *metalv1al return nil, fmt.Errorf("failed to get BMC secret: %w", err) } - return CreateBMCClient(ctx, c, insecure, bmcObj.Spec.Protocol.Name, endpoint.Spec.IP, bmcObj.Spec.Protocol.Port, bmcSecret) + return CreateBMCClient(ctx, c, insecure, bmcObj.Spec.Protocol.Name, address, bmcObj.Spec.Protocol.Port, bmcSecret) } -func CreateBMCClient(ctx context.Context, c client.Client, insecure bool, bmcProtocol metalv1alpha1.ProtocolName, endpoint metalv1alpha1.IP, port int32, bmcSecret *metalv1alpha1.BMCSecret) (bmc.BMC, error) { +func CreateBMCClient(ctx context.Context, c client.Client, insecure bool, bmcProtocol metalv1alpha1.ProtocolName, address string, port int32, bmcSecret *metalv1alpha1.BMCSecret) (bmc.BMC, error) { protocol := "https" if insecure { protocol = "http" @@ -68,7 +77,7 @@ func CreateBMCClient(ctx context.Context, c client.Client, insecure bool, bmcPro var bmcClient bmc.BMC switch bmcProtocol { case metalv1alpha1.ProtocolRedfish: - bmcAddress := fmt.Sprintf("%s://%s:%d", protocol, endpoint, port) + bmcAddress := fmt.Sprintf("%s://%s:%d", protocol, address, port) username, password, err := GetBMCCredentialsFromSecret(bmcSecret) if err != nil { return nil, fmt.Errorf("failed to get credentials from BMC secret: %w", err) @@ -78,7 +87,7 @@ func CreateBMCClient(ctx context.Context, c client.Client, insecure bool, bmcPro return nil, fmt.Errorf("failed to create Redfish client: %w", err) } case metalv1alpha1.ProtocolRedfishLocal: - bmcAddress := fmt.Sprintf("%s://%s:%d", protocol, endpoint, port) + bmcAddress := fmt.Sprintf("%s://%s:%d", protocol, address, port) username, password, err := GetBMCCredentialsFromSecret(bmcSecret) if err != nil { return nil, fmt.Errorf("failed to get credentials from BMC secret: %w", err) @@ -88,7 +97,7 @@ func CreateBMCClient(ctx context.Context, c client.Client, insecure bool, bmcPro return nil, fmt.Errorf("failed to create Redfish client: %w", err) } case metalv1alpha1.ProtocolRedfishKube: - bmcAddress := fmt.Sprintf("%s://%s:%d", protocol, endpoint, port) + bmcAddress := fmt.Sprintf("%s://%s:%d", protocol, address, port) username, password, err := GetBMCCredentialsFromSecret(bmcSecret) if err != nil { return nil, fmt.Errorf("failed to get credentials from BMC secret: %w", err) diff --git a/internal/controller/endpoint_controller.go b/internal/controller/endpoint_controller.go index d58bdb9..a77dacf 100644 --- a/internal/controller/endpoint_controller.go +++ b/internal/controller/endpoint_controller.go @@ -168,7 +168,7 @@ func (r *EndpointReconciler) applyBMC(ctx context.Context, log logr.Logger, endp Name: endpoint.Name, }, Spec: metalv1alpha1.BMCSpec{ - EndpointRef: corev1.LocalObjectReference{ + EndpointRef: &corev1.LocalObjectReference{ Name: endpoint.Name, }, BMCSecretRef: corev1.LocalObjectReference{ @@ -208,8 +208,8 @@ func (r *EndpointReconciler) applyBMCSecret(ctx context.Context, log logr.Logger Name: endpoint.Name, }, Data: map[string][]byte{ - "username": []byte(m.DefaultCredentials[0].Username), - "password": []byte(m.DefaultCredentials[0].Password), + metalv1alpha1.BMCSecretUsernameKeyName: []byte(m.DefaultCredentials[0].Username), + metalv1alpha1.BMCSecretPasswordKeyName: []byte(m.DefaultCredentials[0].Password), }, } diff --git a/internal/controller/endpoint_controller_test.go b/internal/controller/endpoint_controller_test.go index a341d41..c1d111c 100644 --- a/internal/controller/endpoint_controller_test.go +++ b/internal/controller/endpoint_controller_test.go @@ -46,8 +46,8 @@ var _ = Describe("Endpoints Controller", func() { BlockOwnerDeletion: ptr.To(true), })), HaveField("Data", Equal(map[string][]byte{ - "username": []byte("foo"), - "password": []byte("bar"), + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), })))) DeferCleanup(k8sClient.Delete, bmcSecret) diff --git a/internal/controller/server_controller_test.go b/internal/controller/server_controller_test.go index 522d6c2..91d9e9b 100644 --- a/internal/controller/server_controller_test.go +++ b/internal/controller/server_controller_test.go @@ -202,7 +202,7 @@ var _ = Describe("Server Controller", func() { Name: metalv1alpha1.ProtocolRedfishLocal, Port: 8000, }, - Endpoint: "127.0.0.1", + Address: "127.0.0.1", BMCSecretRef: v1.LocalObjectReference{ Name: bmcSecret.Name, }, diff --git a/internal/controller/serverclaim_controller_test.go b/internal/controller/serverclaim_controller_test.go index bb5800e..c39a195 100644 --- a/internal/controller/serverclaim_controller_test.go +++ b/internal/controller/serverclaim_controller_test.go @@ -27,8 +27,8 @@ var _ = Describe("ServerClaim Controller", func() { GenerateName: "test-", }, Data: map[string][]byte{ - "username": []byte("foo"), - "password": []byte("bar"), + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), }, } Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) @@ -46,7 +46,7 @@ var _ = Describe("ServerClaim Controller", func() { Name: metalv1alpha1.ProtocolRedfishLocal, Port: 8000, }, - Endpoint: "127.0.0.1", + Address: "127.0.0.1", BMCSecretRef: v1.LocalObjectReference{ Name: bmcSecret.Name, },