Skip to content
Merged
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.
)
9 changes: 7 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,12 @@ func main() {
}

setupLog.Info("starting boot-server")
go bootserver.RunBootServer(bootserverAddr, ipxeServiceURL, mgr.GetClient(), serverLog.WithName("bootserver"), *defaultHttpUKIURL)
go func() {
if err := bootserver.RunBootServer(bootserverAddr, ipxeServiceURL, mgr.GetClient(), serverLog.WithName("bootserver"), *defaultHttpUKIURL); err != nil {
setupLog.Error(err, "boot-server exited")
panic(err)
}
}()

setupLog.Info("starting image-proxy-server")
go bootserver.RunImageProxyServer(imageProxyServerAddr, mgr.GetClient(), serverLog.WithName("imageproxyserver"))
Expand Down Expand Up @@ -354,7 +359,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
8 changes: 5 additions & 3 deletions server/bootserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ var predefinedConditions = map[string]v1.Condition{
},
}

func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient client.Client, log logr.Logger, defaultUKIURL string) {
func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient client.Client, log logr.Logger, defaultUKIURL string) error {
http.HandleFunc("/ipxe/", func(w http.ResponseWriter, r *http.Request) {
handleIPXE(w, r, k8sClient, log, ipxeServiceURL)
})
Expand Down Expand Up @@ -90,8 +90,10 @@ func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient clien
log.Info("Starting boot server", "address", ipxeServerAddr)
if err := http.ListenAndServe(ipxeServerAddr, nil); err != nil {
log.Error(err, "failed to start boot server")
panic(err)
return err
}

return nil
}

func handleIPXE(w http.ResponseWriter, r *http.Request, k8sClient client.Client, log logr.Logger, ipxeServiceURL string) {
Expand Down Expand Up @@ -393,7 +395,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
68 changes: 68 additions & 0 deletions server/bootserver_suit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package server

import (
"fmt"
"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()

errCh := make(chan error, 1)
testLog := logr.Discard()
go func() {
defer GinkgoRecover()
errCh <- RunBootServer(testServerAddr, ipxeServiceURL, k8sClient, testLog, defaultUKIURL)
}()

Eventually(func() error {
select {
case err := <-errCh:
if err != nil {
return err
}
return fmt.Errorf("boot server exited unexpectedly without error")
default:
}

resp, err := http.Get(testServerURL + "/httpboot")
if resp != nil {
_ = resp.Body.Close()
}
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",
},
}
_ = 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"))
})
})
})
Loading