Skip to content

Commit b4d08c3

Browse files
committed
Add OCI-based default UKI Configuration for HTTPBoot
1 parent 91c5cb7 commit b4d08c3

5 files changed

Lines changed: 156 additions & 90 deletions

File tree

cmd/main.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ func init() {
6363

6464
func main() {
6565
ctx := ctrl.LoggerInto(ctrl.SetupSignalHandler(), setupLog)
66-
defaultHttpUKIURL := NewDefaultHTTPBootData()
6766
skipControllerNameValidation := true
6867

6968
var metricsAddr string
@@ -79,12 +78,16 @@ func main() {
7978
var ipxeServicePort int
8079
var imageServerURL string
8180
var architecture string
81+
var defaultHTTPBootOCIImage string
82+
var defaultHTTPBootUKIURL string
8283

8384
flag.StringVar(&architecture, "architecture", "amd64", "Target system architecture (e.g., amd64, arm64)")
8485
flag.IntVar(&ipxeServicePort, "ipxe-service-port", 5000, "IPXE Service port to listen on.")
8586
flag.StringVar(&ipxeServiceProtocol, "ipxe-service-protocol", "http", "IPXE Service Protocol.")
8687
flag.StringVar(&ipxeServiceURL, "ipxe-service-url", "", "IPXE Service URL.")
8788
flag.StringVar(&imageServerURL, "image-server-url", "", "OS Image Server URL.")
89+
flag.StringVar(&defaultHTTPBootOCIImage, "default-httpboot-oci-image", "", "Default OCI image reference for http boot")
90+
flag.StringVar(&defaultHTTPBootUKIURL, "default-httpboot-uki-url", "", "Deprecated: use --default-httpboot-oci-image")
8891
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
8992
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
9093
flag.StringVar(&bootserverAddr, "boot-server-address", ":8082", "The address the boot-server binds to.")
@@ -122,6 +125,13 @@ func main() {
122125

123126
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
124127

128+
if defaultHTTPBootUKIURL != "" {
129+
setupLog.Info("Flag --default-httpboot-uki-url is deprecated; use --default-httpboot-oci-image instead")
130+
}
131+
if defaultHTTPBootOCIImage != "" && defaultHTTPBootUKIURL != "" {
132+
setupLog.Info("Ignoring --default-httpboot-uki-url because --default-httpboot-oci-image is set")
133+
}
134+
125135
// set the correct ipxe service URL by getting the address from the environment
126136
var ipxeServiceAddr string
127137
if ipxeServiceURL == "" {
@@ -303,7 +313,7 @@ func main() {
303313
}
304314

305315
setupLog.Info("starting boot-server")
306-
go bootserver.RunBootServer(bootserverAddr, ipxeServiceURL, mgr.GetClient(), serverLog.WithName("bootserver"), *defaultHttpUKIURL)
316+
go bootserver.RunBootServer(bootserverAddr, ipxeServiceURL, mgr.GetClient(), serverLog.WithName("bootserver"), defaultHTTPBootOCIImage, defaultHTTPBootUKIURL, imageServerURL, architecture)
307317

308318
setupLog.Info("starting image-proxy-server")
309319
go bootserver.RunImageProxyServer(imageProxyServerAddr, mgr.GetClient(), serverLog.WithName("imageproxyserver"))
@@ -361,10 +371,3 @@ func IndexHTTPBootConfigByNetworkIDs(ctx context.Context, mgr ctrl.Manager) erro
361371
},
362372
)
363373
}
364-
365-
func NewDefaultHTTPBootData() *string {
366-
var defaultUKIURL string
367-
flag.StringVar(&defaultUKIURL, "default-httpboot-uki-url", "", "Default UKI URL for http boot")
368-
369-
return &defaultUKIURL
370-
}

internal/controller/serverbootconfiguration_http_controller.go

Lines changed: 4 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,19 @@ package controller
55

66
import (
77
"context"
8-
"encoding/json"
98
"fmt"
109
"strings"
1110

1211
apimeta "k8s.io/apimachinery/pkg/api/meta"
1312

14-
"github.com/containerd/containerd/remotes/docker"
15-
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
16-
1713
corev1 "k8s.io/api/core/v1"
1814
"k8s.io/apimachinery/pkg/types"
1915
"sigs.k8s.io/controller-runtime/pkg/handler"
2016
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2117

2218
"github.com/go-logr/logr"
2319
bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
20+
"github.com/ironcore-dev/boot-operator/internal/uki"
2421
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2522
"k8s.io/apimachinery/pkg/runtime"
2623
ctrl "sigs.k8s.io/controller-runtime"
@@ -30,10 +27,6 @@ import (
3027
metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
3128
)
3229

33-
const (
34-
MediaTypeUKI = "application/vnd.ironcore.image.uki"
35-
)
36-
3730
type ServerBootConfigurationHTTPReconciler struct {
3831
client.Client
3932
Scheme *runtime.Scheme
@@ -187,74 +180,10 @@ func (r *ServerBootConfigurationHTTPReconciler) getSystemNetworkIDsFromServer(ct
187180
}
188181

189182
func (r *ServerBootConfigurationHTTPReconciler) constructUKIURL(ctx context.Context, image string) (string, error) {
190-
imageDetails := strings.Split(image, ":")
191-
if len(imageDetails) != 2 {
192-
return "", fmt.Errorf("invalid image format")
193-
}
194-
195-
ukiDigest, err := r.getUKIDigestFromNestedManifest(ctx, imageDetails[0], imageDetails[1])
196-
if err != nil {
197-
return "", fmt.Errorf("failed to fetch UKI layer digest: %w", err)
198-
}
199-
200-
ukiDigest = strings.TrimPrefix(ukiDigest, "sha256:")
201-
ukiURL := fmt.Sprintf("%s/%s/sha256-%s.efi", r.ImageServerURL, imageDetails[0], ukiDigest)
202-
return ukiURL, nil
203-
}
204-
205-
func (r *ServerBootConfigurationHTTPReconciler) getUKIDigestFromNestedManifest(ctx context.Context, imageName, imageVersion string) (string, error) {
206-
resolver := docker.NewResolver(docker.ResolverOptions{})
207-
imageRef := fmt.Sprintf("%s:%s", imageName, imageVersion)
208-
name, desc, err := resolver.Resolve(ctx, imageRef)
209-
if err != nil {
210-
return "", fmt.Errorf("failed to resolve image reference: %w", err)
211-
}
212-
213-
targetManifestDesc := desc
214-
manifestData, err := fetchContent(ctx, resolver, name, desc)
215-
if err != nil {
216-
return "", fmt.Errorf("failed to fetch manifest data: %w", err)
217-
}
218-
219-
var manifest ocispec.Manifest
220-
if desc.MediaType == ocispec.MediaTypeImageIndex {
221-
var indexManifest ocispec.Index
222-
if err := json.Unmarshal(manifestData, &indexManifest); err != nil {
223-
return "", fmt.Errorf("failed to unmarshal index manifest: %w", err)
224-
}
225-
226-
for _, manifest := range indexManifest.Manifests {
227-
platform := manifest.Platform
228-
if manifest.Platform != nil && platform.Architecture == r.Architecture {
229-
targetManifestDesc = manifest
230-
break
231-
}
232-
}
233-
if targetManifestDesc.Digest == "" {
234-
return "", fmt.Errorf("failed to find target manifest with architecture %s", r.Architecture)
235-
}
236-
237-
nestedData, err := fetchContent(ctx, resolver, name, targetManifestDesc)
238-
if err != nil {
239-
return "", fmt.Errorf("failed to fetch nested manifest: %w", err)
240-
}
241-
242-
if err := json.Unmarshal(nestedData, &manifest); err != nil {
243-
return "", fmt.Errorf("failed to unmarshal nested manifest: %w", err)
244-
}
245-
} else {
246-
if err := json.Unmarshal(manifestData, &manifest); err != nil {
247-
return "", fmt.Errorf("failed to unmarshal manifest: %w", err)
248-
}
183+
if strings.TrimSpace(r.ImageServerURL) == "" {
184+
return "", fmt.Errorf("image server URL is empty")
249185
}
250-
251-
for _, layer := range manifest.Layers {
252-
if layer.MediaType == MediaTypeUKI {
253-
return layer.Digest.String(), nil
254-
}
255-
}
256-
257-
return "", fmt.Errorf("UKI layer digest not found")
186+
return uki.ConstructUKIURLFromOCI(ctx, image, r.ImageServerURL, r.Architecture)
258187
}
259188

260189
func (r *ServerBootConfigurationHTTPReconciler) enqueueServerBootConfigReferencingIgnitionSecret(ctx context.Context, secret client.Object) []reconcile.Request {

internal/uki/oci.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package uki
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"fmt"
10+
"io"
11+
"strings"
12+
13+
"github.com/containerd/containerd/remotes"
14+
"github.com/containerd/containerd/remotes/docker"
15+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
16+
)
17+
18+
const MediaTypeUKI = "application/vnd.ironcore.image.uki"
19+
20+
func ConstructUKIURLFromOCI(ctx context.Context, image string, imageServerURL string, architecture string) (string, error) {
21+
imageDetails := strings.Split(image, ":")
22+
if len(imageDetails) != 2 {
23+
return "", fmt.Errorf("invalid image format")
24+
}
25+
26+
ukiDigest, err := getUKIDigestFromNestedManifest(ctx, imageDetails[0], imageDetails[1], architecture)
27+
if err != nil {
28+
return "", fmt.Errorf("failed to fetch UKI layer digest: %w", err)
29+
}
30+
31+
ukiDigest = strings.TrimPrefix(ukiDigest, "sha256:")
32+
ukiURL := fmt.Sprintf("%s/%s/sha256-%s.efi", imageServerURL, imageDetails[0], ukiDigest)
33+
return ukiURL, nil
34+
}
35+
36+
func getUKIDigestFromNestedManifest(ctx context.Context, imageName, imageVersion, architecture string) (string, error) {
37+
resolver := docker.NewResolver(docker.ResolverOptions{})
38+
imageRef := fmt.Sprintf("%s:%s", imageName, imageVersion)
39+
name, desc, err := resolver.Resolve(ctx, imageRef)
40+
if err != nil {
41+
return "", fmt.Errorf("failed to resolve image reference: %w", err)
42+
}
43+
44+
targetManifestDesc := desc
45+
manifestData, err := fetchContent(ctx, resolver, name, desc)
46+
if err != nil {
47+
return "", fmt.Errorf("failed to fetch manifest data: %w", err)
48+
}
49+
50+
var manifest ocispec.Manifest
51+
if desc.MediaType == ocispec.MediaTypeImageIndex {
52+
var indexManifest ocispec.Index
53+
if err := json.Unmarshal(manifestData, &indexManifest); err != nil {
54+
return "", fmt.Errorf("failed to unmarshal index manifest: %w", err)
55+
}
56+
57+
for _, manifest := range indexManifest.Manifests {
58+
platform := manifest.Platform
59+
if manifest.Platform != nil && platform.Architecture == architecture {
60+
targetManifestDesc = manifest
61+
break
62+
}
63+
}
64+
if targetManifestDesc.Digest == "" {
65+
return "", fmt.Errorf("failed to find target manifest with architecture %s", architecture)
66+
}
67+
68+
nestedData, err := fetchContent(ctx, resolver, name, targetManifestDesc)
69+
if err != nil {
70+
return "", fmt.Errorf("failed to fetch nested manifest: %w", err)
71+
}
72+
73+
if err := json.Unmarshal(nestedData, &manifest); err != nil {
74+
return "", fmt.Errorf("failed to unmarshal nested manifest: %w", err)
75+
}
76+
} else {
77+
if err := json.Unmarshal(manifestData, &manifest); err != nil {
78+
return "", fmt.Errorf("failed to unmarshal manifest: %w", err)
79+
}
80+
}
81+
82+
for _, layer := range manifest.Layers {
83+
if layer.MediaType == MediaTypeUKI {
84+
return layer.Digest.String(), nil
85+
}
86+
}
87+
88+
return "", fmt.Errorf("UKI layer digest not found")
89+
}
90+
91+
func fetchContent(ctx context.Context, resolver remotes.Resolver, ref string, desc ocispec.Descriptor) ([]byte, error) {
92+
fetcher, err := resolver.Fetcher(ctx, ref)
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to get fetcher: %w", err)
95+
}
96+
97+
reader, err := fetcher.Fetch(ctx, desc)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to fetch content: %w", err)
100+
}
101+
102+
defer func() {
103+
if cerr := reader.Close(); cerr != nil {
104+
fmt.Printf("failed to close reader: %v\n", cerr)
105+
}
106+
}()
107+
108+
data, err := io.ReadAll(reader)
109+
if err != nil {
110+
return nil, fmt.Errorf("failed to read content: %w", err)
111+
}
112+
113+
if int64(len(data)) != desc.Size {
114+
return nil, fmt.Errorf("size mismatch: expected %d, got %d", desc.Size, len(data))
115+
}
116+
117+
return data, nil
118+
}

server/bootserver.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
butanecommon "github.com/coreos/butane/config/common"
2424
"github.com/go-logr/logr"
2525
bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
26+
"github.com/ironcore-dev/boot-operator/internal/uki"
2627
)
2728

2829
type IPXETemplateData struct {
@@ -48,13 +49,13 @@ var predefinedConditions = map[string]v1.Condition{
4849
},
4950
}
5051

51-
func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient client.Client, log logr.Logger, defaultUKIURL string) {
52+
func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient client.Client, log logr.Logger, defaultOCIImage string, defaultUKIURL string, imageServerURL string, architecture string) {
5253
http.HandleFunc("/ipxe/", func(w http.ResponseWriter, r *http.Request) {
5354
handleIPXE(w, r, k8sClient, log, ipxeServiceURL)
5455
})
5556

5657
http.HandleFunc("/httpboot", func(w http.ResponseWriter, r *http.Request) {
57-
handleHTTPBoot(w, r, k8sClient, log, defaultUKIURL)
58+
handleHTTPBoot(w, r, k8sClient, log, defaultOCIImage, defaultUKIURL, imageServerURL, architecture)
5859
})
5960

6061
http.HandleFunc("/ignition/", func(w http.ResponseWriter, r *http.Request) {
@@ -369,7 +370,7 @@ func renderIgnition(yamlData []byte) ([]byte, error) {
369370
return jsonData, nil
370371
}
371372

372-
func handleHTTPBoot(w http.ResponseWriter, r *http.Request, k8sClient client.Client, log logr.Logger, defaultUKIURL string) {
373+
func handleHTTPBoot(w http.ResponseWriter, r *http.Request, k8sClient client.Client, log logr.Logger, defaultOCIImage string, defaultUKIURL string, imageServerURL string, architecture string) {
373374
log.Info("Processing HTTPBoot request", "method", r.Method, "path", r.URL.Path, "clientIP", r.RemoteAddr)
374375
ctx := r.Context()
375376

@@ -407,9 +408,24 @@ func handleHTTPBoot(w http.ResponseWriter, r *http.Request, k8sClient client.Cli
407408
var httpBootResponseData map[string]string
408409
if len(httpBootConfigs.Items) == 0 {
409410
log.Info("No HTTPBootConfig found for client IP, delivering default httpboot data", "clientIPs", clientIPs)
411+
ukiURL := defaultUKIURL
412+
if defaultOCIImage != "" {
413+
if imageServerURL == "" {
414+
log.Error(fmt.Errorf("image server URL is empty"), "Default OCI image provided but image server URL is not set")
415+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
416+
return
417+
}
418+
constructedURL, err := uki.ConstructUKIURLFromOCI(ctx, defaultOCIImage, imageServerURL, architecture)
419+
if err != nil {
420+
log.Error(err, "Failed to construct default UKI URL from OCI image", "image", defaultOCIImage)
421+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
422+
return
423+
}
424+
ukiURL = constructedURL
425+
}
410426
httpBootResponseData = map[string]string{
411427
"ClientIPs": strings.Join(clientIPs, ","),
412-
"UKIURL": defaultUKIURL,
428+
"UKIURL": ukiURL,
413429
}
414430
} else {
415431
// TODO: Pick the first HttpBootConfig if multiple CRs are found.

server/imageproxyserver.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414

1515
"github.com/go-logr/logr"
16+
"github.com/ironcore-dev/boot-operator/internal/uki"
1617
"sigs.k8s.io/controller-runtime/pkg/client"
1718
)
1819

@@ -22,7 +23,6 @@ const (
2223
imageKey = "imageName"
2324
layerDigestKey = "layerDigest"
2425
versionKey = "version"
25-
MediaTypeUKI = "application/vnd.ironcore.image.uki"
2626
)
2727

2828
type TokenResponse struct {
@@ -208,7 +208,7 @@ func modifyProxyResponse(bearerToken string) func(*http.Response) error {
208208
}
209209

210210
// Rewrite media type if it's a UKI
211-
if ct := resp.Header.Get("Content-Type"); ct == MediaTypeUKI {
211+
if ct := resp.Header.Get("Content-Type"); ct == uki.MediaTypeUKI {
212212
resp.Header.Set("Content-Type", "application/efi")
213213
}
214214

0 commit comments

Comments
 (0)