From 716d059aedfe690996dd7ae57152878b120d4d28 Mon Sep 17 00:00:00 2001 From: Hardik Dodiya Date: Wed, 10 Dec 2025 16:15:17 +0100 Subject: [PATCH] Improve tests for the bootserver --- api/v1alpha1/constants.go | 13 ++-- cmd/main.go | 2 +- server/bootserver.go | 2 +- server/bootserver_suit_test.go | 53 +++++++++++++++ server/bootserver_test.go | 119 +++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 server/bootserver_suit_test.go create mode 100644 server/bootserver_test.go diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index 54b9048c..f7acf6db 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -4,10 +4,11 @@ package v1alpha1 const ( - DefaultIgnitionKey = "ignition" // Key for accessing Ignition configuration data within a Kubernetes Secret object. - DefaultIPXEScriptKey = "ipxe-script" // Key for accessing iPXE script data within the iPXE-specific Secret object. - SystemUUIDIndexKey = "spec.systemUUID" // Field to index resources by their system UUID. - SystemIPIndexKey = "spec.systemIPs" // Field to index resources by their system IP addresses. - DefaultFormatKey = "format" // Key for determining the format of the data stored in a Secret, such as fcos or plain-ignition. - FCOSFormat = "fcos" // Specifies the format value used for Fedora CoreOS specific configurations. + DefaultIgnitionKey = "ignition" // Key for accessing Ignition configuration data within a Kubernetes Secret object. + DefaultIPXEScriptKey = "ipxe-script" // Key for accessing iPXE script data within the iPXE-specific Secret object. + SystemUUIDIndexKey = "spec.systemUUID" // Field to index resources by their system UUID. + SystemIPIndexKey = "spec.systemIPs" // Field to index resources by their system IP addresses. + NetworkIdentifierIndexKey = "spec.networkIdentifiers" // Field to index resources by their network identifiers (IP addresses and MAC addresses). + DefaultFormatKey = "format" // Key for determining the format of the data stored in a Secret, such as fcos or plain-ignition. + FCOSFormat = "fcos" // Specifies the format value used for Fedora CoreOS specific configurations. ) diff --git a/cmd/main.go b/cmd/main.go index 006439f7..d457730d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -354,7 +354,7 @@ func IndexHTTPBootConfigByNetworkIDs(ctx context.Context, mgr ctrl.Manager) erro return mgr.GetFieldIndexer().IndexField( ctx, &bootv1alpha1.HTTPBootConfig{}, - bootv1alpha1.SystemIPIndexKey, + bootv1alpha1.NetworkIdentifierIndexKey, func(Obj client.Object) []string { HTTPBootConfig := Obj.(*bootv1alpha1.HTTPBootConfig) return HTTPBootConfig.Spec.NetworkIdentifiers diff --git a/server/bootserver.go b/server/bootserver.go index 0581792d..29279b5b 100644 --- a/server/bootserver.go +++ b/server/bootserver.go @@ -393,7 +393,7 @@ func handleHTTPBoot(w http.ResponseWriter, r *http.Request, k8sClient client.Cli var httpBootConfigs bootv1alpha1.HTTPBootConfigList for _, ip := range clientIPs { - if err := k8sClient.List(ctx, &httpBootConfigs, client.MatchingFields{bootv1alpha1.SystemIPIndexKey: ip}); err != nil { + if err := k8sClient.List(ctx, &httpBootConfigs, client.MatchingFields{bootv1alpha1.NetworkIdentifierIndexKey: ip}); err != nil { log.Info("Failed to list HTTPBootConfig for IP", "IP", ip, "error", err) continue } diff --git a/server/bootserver_suit_test.go b/server/bootserver_suit_test.go new file mode 100644 index 00000000..def7a16f --- /dev/null +++ b/server/bootserver_suit_test.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "net/http" + "testing" + + "github.com/go-logr/logr" + bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var ( + testServerAddr = ":30003" + testServerURL = "http://localhost:30003" + + defaultUKIURL = "https://example.com/default.efi" + ipxeServiceURL = "http://localhost:30004" + + k8sClient client.Client +) + +func TestBootServer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Boot Server Suite") +} + +var _ = BeforeSuite(func() { + scheme := runtime.NewScheme() + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(bootv1alpha1.AddToScheme(scheme)).To(Succeed()) + + k8sClient = fake.NewClientBuilder(). + WithScheme(scheme). + Build() + + go func() { + defer GinkgoRecover() + RunBootServer(testServerAddr, ipxeServiceURL, k8sClient, logr.Discard(), defaultUKIURL) + }() + + Eventually(func() error { + _, err := http.Get(testServerURL + "/httpboot") + return err + }, "5s", "200ms").Should(Succeed()) +}) diff --git a/server/bootserver_test.go b/server/bootserver_test.go new file mode 100644 index 00000000..bfa34636 --- /dev/null +++ b/server/bootserver_test.go @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/go-logr/logr" + bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type httpBootResponse struct { + ClientIPs string `json:"ClientIPs"` + UKIURL string `json:"UKIURL"` + SystemUUID string `json:"SystemUUID,omitempty"` +} + +var _ = Describe("BootServer", func() { + Context("/httpboot endpoint", func() { + It("delivers default httpboot data when no HTTPBootConfig matches the client IP", func() { + resp, err := http.Get(testServerURL + "/httpboot") + Expect(err).NotTo(HaveOccurred()) + defer func() { + _ = resp.Body.Close() + }() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(resp.Header.Get("Content-Type")).To(Equal("application/json")) + + var body httpBootResponse + Expect(json.NewDecoder(resp.Body).Decode(&body)).To(Succeed()) + + By("returning the default UKI URL") + Expect(body.UKIURL).To(Equal(defaultUKIURL)) + + By("including the recorded client IPs") + Expect(body.ClientIPs).NotTo(BeEmpty()) + + By("not setting a SystemUUID in the default case") + Expect(body.SystemUUID).To(SatisfyAny(BeEmpty(), Equal(""))) + }) + }) + + It("converts valid Butane YAML to JSON", func() { + butaneYAML := []byte(` +variant: fcos +version: 1.5.0 +systemd: + units: + - name: test.service + enabled: true +`) + + jsonData, err := renderIgnition(butaneYAML) + Expect(err).ToNot(HaveOccurred()) + Expect(jsonData).ToNot(BeEmpty()) + Expect(string(jsonData)).To(ContainSubstring(`"systemd"`)) + }) + + It("returns an error for invalid YAML", func() { + bad := []byte("this ::: is not yaml") + _, err := renderIgnition(bad) + Expect(err).To(HaveOccurred()) + }) + + Context("Verify the SetStatusCondition method", func() { + + var testLog = logr.Discard() + + It("returns an error for unknown condition type", func() { + cfg := &bootv1alpha1.IPXEBootConfig{ + ObjectMeta: v1.ObjectMeta{ + Name: "unknown-cond", + Namespace: "default", + }, + } + Expect(k8sClient.Create(context.Background(), cfg)).To(Succeed()) + + err := SetStatusCondition( + context.Background(), + k8sClient, + testLog, + cfg, + "DoesNotExist", + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("condition type DoesNotExist not found")) + }) + + It("returns an error for unsupported resource types", func() { + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "bad-type", + Namespace: "default", + }, + } + _ = k8sClient.Create(context.Background(), secret) + + err := SetStatusCondition( + context.Background(), + k8sClient, + testLog, + secret, + "IgnitionDataFetched", + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported resource type")) + }) + }) +})