Skip to content

Commit

Permalink
Add inline Endpoint to BMC type (#151)
Browse files Browse the repository at this point in the history
* Add `Access` field to `BMC` type

Allow the definition of a `BMC` resource without depending on an
`Endpoint` resource.

* Generate API reference docs

* Fix test

* Rename `.spec.access` to `.spec.endpoint`
  • Loading branch information
afritzler authored Oct 30, 2024
1 parent 1850f8e commit 350bd4b
Show file tree
Hide file tree
Showing 24 changed files with 587 additions and 53 deletions.
18 changes: 17 additions & 1 deletion api/v1alpha1/bmc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

// Endpoint 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
Endpoint *InlineEndpoint `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.
Expand All @@ -35,6 +41,16 @@ type BMCSpec struct {
ConsoleProtocol *ConsoleProtocol `json:"consoleProtocol,omitempty"`
}

// InlineEndpoint defines inline network access configuration for the BMC.
type InlineEndpoint struct {
// MACAddress is the MAC address of the endpoint.
MACAddress string `json:"macAddress"`
// IP is the IP address of the BMC.
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Schemaless
IP IP `json:"ip"`
}

// ConsoleProtocol defines the protocol and port used for console access to the BMC.
type ConsoleProtocol struct {
// Name specifies the name of the console protocol.
Expand Down
84 changes: 84 additions & 0 deletions api/v1alpha1/bmc_webhook.go
Original file line number Diff line number Diff line change
@@ -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.Endpoint != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("endpointRef"), spec.EndpointRef, "only one of 'endpointRef' or 'endpoint' should be specified"))
allErrs = append(allErrs, field.Invalid(fldPath.Child("endpoint"), spec.Endpoint, "only one of 'endpointRef' or 'endpoint' should be specified"))
}
if spec.EndpointRef == nil && spec.Endpoint == nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("endpointRef"), spec.EndpointRef, "either 'endpointRef' or 'endpoint' must be specified"))
allErrs = append(allErrs, field.Invalid(fldPath.Child("endpoint"), spec.Endpoint, "either 'endpointRef' or 'endpoint' 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
}
162 changes: 162 additions & 0 deletions api/v1alpha1/bmc_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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 InlineEndpoint spec fields", func() {
bmc := &BMC{
ObjectMeta: metav1.ObjectMeta{
Name: "test-invalid",
},
Spec: BMCSpec{
EndpointRef: &v1.LocalObjectReference{Name: "foo"},
Endpoint: &InlineEndpoint{
IP: MustParseIP("127.0.0.1"),
MACAddress: "aa:bb:cc:dd:ee:ff",
},
},
}
Expect(k8sClient.Create(ctx, bmc)).To(HaveOccurred())
Eventually(Get(bmc)).Should(Satisfy(errors.IsNotFound))
})

It("Should deny if the BMC has no EndpointRef and InlineEndpoint 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 InlineEndpoint 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 InlineEndpoint 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.Endpoint = &InlineEndpoint{
IP: MustParseIP("127.0.0.1"),
MACAddress: "aa:bb:cc:dd:ee:ff",
}
})).Should(Succeed())
})

It("Should admit if the BMC has no EndpointRef but an InlineEndpoint spec field", func() {
bmc := &BMC{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-",
},
Spec: BMCSpec{
Endpoint: &InlineEndpoint{
IP: MustParseIP("127.0.0.1"),
MACAddress: "aa:bb:cc:dd:ee:ff",
},
},
}
Expect(k8sClient.Create(ctx, bmc)).To(Succeed())
DeferCleanup(k8sClient.Delete, bmc)
})

It("Should deny if the BMC InlineEndpoint spec field has been removed", func() {
bmc := &BMC{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-",
},
Spec: BMCSpec{
Endpoint: &InlineEndpoint{
IP: MustParseIP("127.0.0.1"),
MACAddress: "aa:bb:cc:dd:ee:ff",
},
},
}
Expect(k8sClient.Create(ctx, bmc)).To(Succeed())
DeferCleanup(k8sClient.Delete, bmc)

Eventually(Update(bmc, func() {
bmc.Spec.Endpoint = nil
})).Should(Not(Succeed()))

Eventually(Object(bmc)).Should(SatisfyAll(
HaveField("Spec.Endpoint.IP", MustParseIP("127.0.0.1")),
HaveField("Spec.Endpoint.MACAddress", "aa:bb:cc:dd:ee:ff"),
))
})

It("Should admit if the BMC has is changing to an EndpointRef from an InlineEndpoint spec field", func() {
bmc := &BMC{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-",
},
Spec: BMCSpec{
Endpoint: &InlineEndpoint{
IP: MustParseIP("127.0.0.1"),
MACAddress: "aa:bb:cc:dd:ee:ff",
},
},
}
Expect(k8sClient.Create(ctx, bmc)).To(Succeed())
DeferCleanup(k8sClient.Delete, bmc)

Eventually(Update(bmc, func() {
bmc.Spec.EndpointRef = &v1.LocalObjectReference{Name: "foo"}
bmc.Spec.Endpoint = nil
})).Should(Succeed())
})

})
7 changes: 7 additions & 0 deletions api/v1alpha1/bmcsecret_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions api/v1alpha1/server_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 3 additions & 4 deletions api/v1alpha1/webhook_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading

0 comments on commit 350bd4b

Please sign in to comment.