Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions api/v1alpha1/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
)
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion server/bootserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
53 changes: 53 additions & 0 deletions server/bootserver_suit_test.go
Original file line number Diff line number Diff line change
@@ -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)
}()
Comment on lines +40 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fake client lacks field indexer for NetworkIdentifierIndexKey.

The fake client built with fake.NewClientBuilder() doesn't have field indexers configured. When handleHTTPBoot calls k8sClient.List(ctx, &httpBootConfigs, client.MatchingFields{bootv1alpha1.NetworkIdentifierIndexKey: ip}), the index lookup will not work as expected.

You need to configure the fake client with the index using WithIndex:

🔧 Proposed fix
 	k8sClient = fake.NewClientBuilder().
 		WithScheme(scheme).
+		WithIndex(&bootv1alpha1.HTTPBootConfig{}, bootv1alpha1.NetworkIdentifierIndexKey, func(obj client.Object) []string {
+			config := obj.(*bootv1alpha1.HTTPBootConfig)
+			return config.Spec.NetworkIdentifiers
+		}).
 		Build()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
k8sClient = fake.NewClientBuilder().
WithScheme(scheme).
Build()
go func() {
defer GinkgoRecover()
RunBootServer(testServerAddr, ipxeServiceURL, k8sClient, logr.Discard(), defaultUKIURL)
}()
k8sClient = fake.NewClientBuilder().
WithScheme(scheme).
WithIndex(&bootv1alpha1.HTTPBootConfig{}, bootv1alpha1.NetworkIdentifierIndexKey, func(obj client.Object) []string {
config := obj.(*bootv1alpha1.HTTPBootConfig)
return config.Spec.NetworkIdentifiers
}).
Build()
🤖 Prompt for AI Agents
In `@server/bootserver_suit_test.go` around lines 40 - 47, The fake k8s client
created with fake.NewClientBuilder() is missing the field indexer for
NetworkIdentifierIndexKey, so List calls using
client.MatchingFields{bootv1alpha1.NetworkIdentifierIndexKey: ip} in
handleHTTPBoot won't match; fix by configuring the fake builder with
WithIndex(bootv1alpha1.NetworkIdentifierIndexKey, func(obj client.Object)
[]string { /* return the field value used by the real indexer, e.g., extract
NetworkIdentifier from BootConfig/httpBootConfig */ }) before Build(), so the
k8sClient used by RunBootServer has the same field index behavior as the real
client.


Eventually(func() error {
_, err := http.Get(testServerURL + "/httpboot")
return err
}, "5s", "200ms").Should(Succeed())
})
119 changes: 119 additions & 0 deletions server/bootserver_test.go
Original file line number Diff line number Diff line change
@@ -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",
},
}
Expect(k8sClient.Create(context.Background(), secret)).To(Succeed())

err := SetStatusCondition(
context.Background(),
k8sClient,
testLog,
secret,
"IgnitionDataFetched",
)

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("unsupported resource type"))
})
})
})
Loading