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
8 changes: 6 additions & 2 deletions api/v1alpha1/ipxebootconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ type IPXEBootConfigSpec struct {
Image string `json:"image,omitempty"`

// KernelURL is the URL where the kernel of the OS is hosted, eg. the URL to the Kernel layer of the OS OCI image.
KernelURL string `json:"kernelURL,omitempty"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
KernelURL string `json:"kernelURL"`

// InitrdURL is the URL where the Initrd (initial RAM disk) of the OS is hosted, eg. the URL to the Initrd layer of the OS OCI image.
InitrdURL string `json:"initrdURL,omitempty"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
InitrdURL string `json:"initrdURL"`
Comment on lines +27 to +34

@coderabbitai coderabbitai Bot Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -a 'ipxebootconfig_types.go|zz_generated|crd|generated|ipxe.*yaml|IPXE' . | sed 's#^\./##' | head -200

echo
echo "== read type file =="
if [ -f api/v1alpha1/ipxebootconfig_types.go ]; then
  nl -ba api/v1alpha1/ipxebootconfig_types.go | sed -n '1,180p'
fi

echo
echo "== search KernelURL/Image/initrd/url/defaulting/webhook files =="
rg -n "KernelURL|InitrdURL|Image|IPXEBootConfig|Default|Conversion|webhook|Validate|defaulting" -S --glob '!**/vendor/**' --glob '!**/.git/**' . | head -300

Repository: ironcore-dev/boot-operator

Length of output: 936


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CRD manifests mentioning IPXEBootConfig fields =="
rg -n "IPXEBootConfig|kernelURL|initrdURL|required|deprecated" -S --glob '*crd*.yaml' --glob '*.yaml' --glob '*.yml' api config manifests 2>/dev/null | sed -n '1,240p'

echo
echo "== list file under api/v1alpha1 =="
git ls-files api/v1alpha1 | sed -n '1,200p'

echo
echo "== inspect api/v1alpha1 files sizes/head =="
for f in $(git ls-files api/v1alpha1 | head -50); do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  sed -n '1,220p' "$f"
done

Repository: ironcore-dev/boot-operator

Length of output: 2835


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== type file =="
cat -n api/v1alpha1/ipxebootconfig_types.go | sed -n '1,180p'

echo
echo "== relevant generated CRD sections =="
cat -n config/crd/bases/boot.ironcore.dev_ipxebootconfigs.yaml | sed -n '1,130p'
cat -n dist/chart/templates/crd/boot.ironcore.dev_ipxebootconfigs.yaml | sed -n '1,130人p'

echo
echo "== webhook/defaulting/conversion existence =="
git ls-files | grep -Ei '(webhook|default|convert|ipxebootconfig)' | sed -n '1,200p'

echo
echo "== relevant source references =="
rg -n "KernelURL|InitrdURL|Image |IPXEBootConfig|Deprecated|Required|webhook|defaulter|Defaulting|Conversion" api/v1alpha1 config manager pkg internal --glob '!**/vendor/**' 2>/dev/null | sed -n '1,240p'

Repository: ironcore-dev/boot-operator

Length of output: 11651


Plan the rollout for the newly required fields.

KernelURL and InitrdURL are now typed as required, and the CRD schema includes them in spec.required, so existing IPXEBootConfig objects that omit these fields will fail admission after rollout. Add a migration/defaulting or conversion path, or explicitly document that breaking legacy v1alpha1 resources is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/v1alpha1/ipxebootconfig_types.go` around lines 27 - 34, Plan the rollout
for the newly required KernelURL and InitrdURL fields in the IPXEBootConfig API:
add a migration, defaulting, or conversion path that preserves existing v1alpha1
resources missing these fields, or explicitly document and implement acceptance
of the breaking admission change. Ensure the chosen approach is reflected in the
API conversion/defaulting or release documentation rather than only updating
validation markers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@afritzler do we want those fields to be required? it would at least fail faster

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would say yes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


// SquashfsURL is the URL where the Squashfs of the OS is hosted, eg. the URL to the Squashfs layer of the OS OCI image.
SquashfsURL string `json:"squashfsURL,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions cmdutils/move_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ var _ = Describe("bootctl move", func() {

sourceIPXESecret := create(ctx, clients.Source, namedObj(&corev1.Secret{}, "ipxe-secret"))
sourceIPXEBootConfig := namedObj(&bootv1alphav1.IPXEBootConfig{}, "test-ipxe-boot-config")
sourceIPXEBootConfig.Spec.KernelURL = "http://example.com/kernel"
sourceIPXEBootConfig.Spec.InitrdURL = "http://example.com/initrd"
sourceIPXEBootConfig.Spec.IgnitionSecretRef = &corev1.LocalObjectReference{Name: sourceIPXESecret.Name}
sourceIPXEBootConfig = create(ctx, clients.Source, sourceIPXEBootConfig)

Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/boot.ironcore.dev_ipxebootconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ spec:
description: InitrdURL is the URL where the Initrd (initial RAM disk)
of the OS is hosted, eg. the URL to the Initrd layer of the OS OCI
image.
minLength: 1
type: string
ipxeScriptSecretRef:
description: IPXEScriptSecretRef is a reference to the secret containing
Expand All @@ -90,6 +91,7 @@ spec:
kernelURL:
description: KernelURL is the URL where the kernel of the OS is hosted,
eg. the URL to the Kernel layer of the OS OCI image.
minLength: 1
type: string
squashfsURL:
description: SquashfsURL is the URL where the Squashfs of the OS is
Expand All @@ -103,6 +105,9 @@ spec:
systemUUID:
description: SystemUUID is the unique identifier (UUID) of the server.
type: string
required:
- initrdURL
- kernelURL
type: object
status:
description: IPXEBootConfigStatus defines the observed state of IPXEBootConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ spec:
description: InitrdURL is the URL where the Initrd (initial RAM disk)
of the OS is hosted, eg. the URL to the Initrd layer of the OS OCI
image.
minLength: 1
type: string
ipxeScriptSecretRef:
description: IPXEScriptSecretRef is a reference to the secret containing
Expand All @@ -96,6 +97,7 @@ spec:
kernelURL:
description: KernelURL is the URL where the kernel of the OS is hosted,
eg. the URL to the Kernel layer of the OS OCI image.
minLength: 1
type: string
squashfsURL:
description: SquashfsURL is the URL where the Squashfs of the OS is
Expand All @@ -109,6 +111,9 @@ spec:
systemUUID:
description: SystemUUID is the unique identifier (UUID) of the server.
type: string
required:
- initrdURL
- kernelURL
type: object
status:
description: IPXEBootConfigStatus defines the observed state of IPXEBootConfig
Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ _Appears in:_
| `systemUUID` _string_ | SystemUUID is the unique identifier (UUID) of the server. | | |
| `systemIPs` _string array_ | SystemIPs is a list of IP addresses assigned to the server. | | |
| `image` _string_ | Image is deprecated and will be removed. | | |
| `kernelURL` _string_ | KernelURL is the URL where the kernel of the OS is hosted, eg. the URL to the Kernel layer of the OS OCI image. | | |
| `initrdURL` _string_ | InitrdURL is the URL where the Initrd (initial RAM disk) of the OS is hosted, eg. the URL to the Initrd layer of the OS OCI image. | | |
| `kernelURL` _string_ | KernelURL is the URL where the kernel of the OS is hosted, eg. the URL to the Kernel layer of the OS OCI image. | | MinLength: 1 <br />Required: \{\} <br /> |
| `initrdURL` _string_ | InitrdURL is the URL where the Initrd (initial RAM disk) of the OS is hosted, eg. the URL to the Initrd layer of the OS OCI image. | | MinLength: 1 <br />Required: \{\} <br /> |
| `squashfsURL` _string_ | SquashfsURL is the URL where the Squashfs of the OS is hosted, eg. the URL to the Squashfs layer of the OS OCI image. | | |
| `ipxeServerURL` _string_ | IPXEServerURL is deprecated and will be removed. | | |
| `ignitionSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core)_ | IgnitionSecretRef is a reference to the secret containing the Ignition configuration. | | |
Expand Down
5 changes: 4 additions & 1 deletion internal/controller/ipxebootconfig_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ var _ = Describe("IPXEBootConfig Controller", func() {
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
Spec: bootv1alpha1.IPXEBootConfigSpec{
KernelURL: "http://example.com/kernel",
InitrdURL: "http://example.com/initrd",
},
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
Expand Down
17 changes: 10 additions & 7 deletions internal/controller/serverbootconfiguration_pxe_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,16 @@ func (r *ServerBootConfigurationPXEReconciler) getImageDetailsFromConfig(ctx con
return "", "", "", fmt.Errorf("failed to fetch layer digests: %w", err)
}

kernelURL := buildImageURL(r.IPXEServiceURL, imageName, imageVersion, kernelDigest)
initrdURL := buildImageURL(r.IPXEServiceURL, imageName, imageVersion, initrdDigest)
squashFSURL := buildImageURL(r.IPXEServiceURL, imageName, imageVersion, squashFSDigest)
var kernelURL, initrdURL, squashFSURL string
if kernelDigest != "" {
kernelURL = buildImageURL(r.IPXEServiceURL, imageName, imageVersion, kernelDigest)
}
if initrdDigest != "" {
initrdURL = buildImageURL(r.IPXEServiceURL, imageName, imageVersion, initrdDigest)
}
if squashFSDigest != "" {
squashFSURL = buildImageURL(r.IPXEServiceURL, imageName, imageVersion, squashFSDigest)
}
log.V(1).Info("Built image URLs", "kernelURL", kernelURL, "initrdURL", initrdURL, "squashfsURL", squashFSURL)

return kernelURL, initrdURL, squashFSURL, nil
Expand Down Expand Up @@ -258,10 +265,6 @@ func (r *ServerBootConfigurationPXEReconciler) getLayerDigestsFromNestedManifest
}
}

if kernelDigest == "" || initrdDigest == "" || squashFSDigest == "" {
return "", "", "", fmt.Errorf("failed to find all required layer digests")
}

return kernelDigest, initrdDigest, squashFSDigest, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,59 @@ var _ = Describe("ServerBootConfiguration Controller", func() {
HaveField("Spec.IgnitionSecretRef.Name", "foo"),
))
})

It("should map a ServerBootConfiguration using an initramfs-only oci image (no squashfs)", func(ctx SpecContext) {
By("creating a new Server object")
server := &metalv1alpha1.Server{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "server-",
},
Spec: metalv1alpha1.ServerSpec{
SystemUUID: "12345",
},
}
Expect(k8sClient.Create(ctx, server)).To(Succeed())

By("patching the Server NICs in Server status")
Eventually(UpdateStatus(server, func() {
server.Status.NetworkInterfaces = []metalv1alpha1.NetworkInterface{
{
Name: "foo",
IPs: []metalv1alpha1.IP{metalv1alpha1.MustParseIP("1.1.1.1")},
MACAddress: "abcd",
},
}
})).Should(Succeed())

By("creating a new ServerBootConfiguration")
config := &metalv1alpha1.ServerBootConfiguration{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
GenerateName: "test-",
},
Spec: metalv1alpha1.ServerBootConfigurationSpec{
ServerRef: corev1.LocalObjectReference{
Name: server.Name,
},
Image: MockImageRef("ironcore-dev/os-images/sanitizer", "1.0.0"),
IgnitionSecretRef: &corev1.LocalObjectReference{Name: "foo"},
},
}
Expect(k8sClient.Create(ctx, config)).To(Succeed())

By("ensuring that the ipxe boot configuration has no squashfs URL")
bootConfig := &v1alpha1.IPXEBootConfig{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Name: config.Name,
},
}
Eventually(Object(bootConfig)).Should(SatisfyAll(
HaveField("Spec.SystemUUID", server.Spec.SystemUUID),
HaveField("Spec.SystemIPs", ContainElement("1.1.1.1")),
HaveField("Spec.KernelURL", Not(BeEmpty())),
HaveField("Spec.InitrdURL", Not(BeEmpty())),
HaveField("Spec.SquashfsURL", BeEmpty()),
))
})
})
1 change: 1 addition & 0 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ var _ = BeforeSuite(func() {
// Push test images to mock registry (using simple paths without localhost prefix)
Expect(mockRegistry.PushPXEImage("ironcore-dev/os-images/gardenlinux", "1877.0", runtime.GOARCH)).To(Succeed())
Expect(mockRegistry.PushPXEImageOldFormat("gardenlinux/gardenlinux", "1772.0", runtime.GOARCH)).To(Succeed())
Expect(mockRegistry.PushPXEImageNoSquashFS("ironcore-dev/os-images/sanitizer", "1.0.0", runtime.GOARCH)).To(Succeed())
Expect(mockRegistry.PushHTTPImage("ironcore-dev/os-images/test-image", "100.1")).To(Succeed())

// Set allowed registries to use mock registry
Expand Down
38 changes: 38 additions & 0 deletions server/bootserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
package server

import (
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
"text/template"

"github.com/go-logr/logr"
bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
Expand Down Expand Up @@ -406,6 +409,41 @@ var _ = Describe("ConfigSelector", func() {
})
})

Context("iPXE script template", func() {
renderIPXEScript := func(data IPXETemplateData) string {
tmpl, err := template.ParseFiles("../templates/ipxe-script.tpl")
Expect(err).NotTo(HaveOccurred())
var buf bytes.Buffer
Expect(tmpl.Execute(&buf, data)).To(Succeed())
return buf.String()
}

It("includes squashfs kernel params when SquashfsURL is set", func() {
script := renderIPXEScript(IPXETemplateData{
KernelURL: "http://example.com/kernel",
InitrdURL: "http://example.com/initrd",
SquashfsURL: "http://example.com/squashfs",
IPXEServerURL: "http://example.com",
})
Expect(script).To(ContainSubstring("set squashfs-url http://example.com/squashfs"))
Expect(script).To(ContainSubstring("gl.ovl=/:tmpfs"))
Expect(script).To(ContainSubstring("gl.url=${squashfs-url}"))
Expect(script).To(ContainSubstring("gl.live=1"))
})

It("omits squashfs kernel params when SquashfsURL is empty", func() {
script := renderIPXEScript(IPXETemplateData{
KernelURL: "http://example.com/kernel",
InitrdURL: "http://example.com/initrd",
SquashfsURL: "",
IPXEServerURL: "http://example.com",
})
Expect(strings.Contains(script, "squashfs-url")).To(BeFalse())
Expect(strings.Contains(script, "gl.ovl")).To(BeFalse())
Expect(strings.Contains(script, "gl.live")).To(BeFalse())
})
})

Context("resolveServer", func() {
It("skips deleted SBCs and resolves via the next one", func() {
server := &metalv1alpha1.Server{
Expand Down
8 changes: 4 additions & 4 deletions templates/ipxe-script.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
set ipxe-svc {{.IPXEServerURL}}
set kernel-url {{.KernelURL}}
set initrd-url {{.InitrdURL}}
set squashfs-url {{.SquashfsURL}}

{{if .SquashfsURL}}set squashfs-url {{.SquashfsURL}}
{{end}}
echo Loading kernel...
kernel ${kernel-url} initrd=initrd gl.ovl=/:tmpfs gl.url=${squashfs-url} gl.live=1 ip=any ignition.firstboot=1 ignition.config.url=${ipxe-svc}/ignition/${uuid} ignition.platform.id=metal console=ttyS0,115200 console=tty0 console=ttyAMA0 earlyprintk=ttyS0,115200 consoleblank=0
kernel ${kernel-url} initrd=initrd{{if .SquashfsURL}} gl.ovl=/:tmpfs gl.url=${squashfs-url} gl.live=1{{end}} ip=any ignition.firstboot=1 ignition.config.url=${ipxe-svc}/ignition/${uuid} ignition.platform.id=metal console=ttyS0,115200 console=tty0 console=ttyAMA0 earlyprintk=ttyS0,115200 consoleblank=0
echo Loading initrd...
initrd ${initrd-url}
echo Booting...
boot
boot
54 changes: 52 additions & 2 deletions test/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
MediaTypeKernelOld = "application/io.gardenlinux.kernel"
// MediaTypeInitrdOld represents the legacy initrd media type
MediaTypeInitrdOld = "application/io.gardenlinux.initrd"
// MediaTypeOCIConfig represents the OCI image config media type
MediaTypeOCIConfig = "application/vnd.oci.image.config.v1+json"
)

// MockRegistry provides an in-memory OCI registry for testing
Expand Down Expand Up @@ -100,7 +102,7 @@ func (r *MockRegistry) pushPXEManifest(name, tag string, kernelMedia, initrdMedi
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: ocispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
MediaType: MediaTypeOCIConfig,
Digest: configDigest,
Size: 2,
},
Expand Down Expand Up @@ -137,6 +139,45 @@ func (r *MockRegistry) pushPXEManifest(name, tag string, kernelMedia, initrdMedi
r.blobs[squashfsDigest] = []byte("squashfs-data")
}

// pushPXEManifestNoSquashFS stores a PXE manifest with only kernel and initrd layers.
func (r *MockRegistry) pushPXEManifestNoSquashFS(name, tag string, kernelMedia, initrdMedia string) {
kernelDigest := digest.FromString(fmt.Sprintf("kernel-%s-%s", name, tag))
initrdDigest := digest.FromString(fmt.Sprintf("initrd-%s-%s", name, tag))
configDigest := digest.FromString(fmt.Sprintf("config-%s-%s", name, tag))

manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: ocispec.Descriptor{
MediaType: MediaTypeOCIConfig,
Digest: configDigest,
Size: 2,
},
Layers: []ocispec.Descriptor{
{
MediaType: kernelMedia,
Digest: kernelDigest,
Size: 1024,
},
{
MediaType: initrdMedia,
Digest: initrdDigest,
Size: 2048,
},
},
}

ref := fmt.Sprintf("%s:%s", name, tag)
r.manifests[ref] = manifest

manifestBytes, _ := json.Marshal(manifest)
manifestDigest := digest.FromBytes(manifestBytes)
r.manifestsByDigest[manifestDigest] = manifest

r.blobs[manifest.Config.Digest] = []byte("{}")
r.blobs[kernelDigest] = []byte("kernel-data")
r.blobs[initrdDigest] = []byte("initrd-data")
}

// PushPXEImage adds a PXE boot image with kernel, initrd, and squashfs layers
func (r *MockRegistry) PushPXEImage(name, tag, architecture string) error {
r.mu.Lock()
Expand All @@ -145,6 +186,15 @@ func (r *MockRegistry) PushPXEImage(name, tag, architecture string) error {
return nil
}

// PushPXEImageNoSquashFS adds a PXE boot image with only kernel and initrd layers (no squashfs).
// Used for initramfs-only images such as the sanitizer.
func (r *MockRegistry) PushPXEImageNoSquashFS(name, tag, architecture string) error {
r.mu.Lock()
defer r.mu.Unlock()
r.pushPXEManifestNoSquashFS(name, tag, MediaTypeKernel, MediaTypeInitrd)
return nil
}

// PushPXEImageOldFormat adds a PXE boot image using old Gardenlinux media types
func (r *MockRegistry) PushPXEImageOldFormat(name, tag, architecture string) error {
r.mu.Lock()
Expand All @@ -164,7 +214,7 @@ func (r *MockRegistry) PushHTTPImage(name, tag string) error {
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: ocispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
MediaType: MediaTypeOCIConfig,
Digest: configDigest,
Size: 2,
},
Expand Down
Loading