diff --git a/api/v1alpha1/httpbootconfig_types.go b/api/v1alpha1/httpbootconfig_types.go index d07702e6..f68b0536 100644 --- a/api/v1alpha1/httpbootconfig_types.go +++ b/api/v1alpha1/httpbootconfig_types.go @@ -12,6 +12,8 @@ import ( type HTTPBootConfigSpec struct { SystemUUID string `json:"systemUUID,omitempty"` IgnitionSecretRef *corev1.LocalObjectReference `json:"ignitionSecretRef,omitempty"` + SystemIP string `json:"systemIP,omitempty"` + UKIURL string `json:"ukiURL,omitempty"` } // HTTPBootConfigStatus defines the observed state of HTTPBootConfig diff --git a/cmd/main.go b/cmd/main.go index 51823579..f1797a50 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -62,6 +62,7 @@ func init() { func main() { ctx := ctrl.LoggerInto(ctrl.SetupSignalHandler(), setupLog) defaultIpxeTemplateData := NewDefaultIPXETemplateData() + defaultHttpUKIURL := NewDefaultHTTPBootData() var metricsAddr string var enableLeaderElection bool @@ -227,8 +228,13 @@ func main() { os.Exit(1) } + if err := IndexHTTPBootConfigBySystemIP(ctx, mgr); err != nil { + setupLog.Error(err, "unable to set up indexer for HTTPBootConfig SystemIP") + os.Exit(1) + } + setupLog.Info("starting boot-server") - go bootserver.RunBootServer(ipxeServerAddr, ipxeServiceURL, mgr.GetClient(), serverLog.WithName("bootserver"), *defaultIpxeTemplateData) + go bootserver.RunBootServer(ipxeServerAddr, ipxeServiceURL, mgr.GetClient(), serverLog.WithName("bootserver"), *defaultIpxeTemplateData, *defaultHttpUKIURL) setupLog.Info("starting image-proxy-server") go bootserver.RunImageProxyServer(imageProxyServerAddr, mgr.GetClient(), serverLog.WithName("imageproxyserver")) @@ -275,6 +281,18 @@ func IndexHTTPBootConfigBySystemUUID(ctx context.Context, mgr ctrl.Manager) erro ) } +func IndexHTTPBootConfigBySystemIP(ctx context.Context, mgr ctrl.Manager) error { + return mgr.GetFieldIndexer().IndexField( + ctx, + &bootv1alpha1.HTTPBootConfig{}, + systemIPIndexKey, + func(Obj client.Object) []string { + HTTPBootConfig := Obj.(*bootv1alpha1.HTTPBootConfig) + return []string{HTTPBootConfig.Spec.SystemIP} + }, + ) +} + func NewDefaultIPXETemplateData() *bootserver.IPXETemplateData { var cfg bootserver.IPXETemplateData flag.StringVar(&cfg.KernelURL, "default-kernel-url", "", "Default URL for the kernel") @@ -284,3 +302,10 @@ func NewDefaultIPXETemplateData() *bootserver.IPXETemplateData { return &cfg } + +func NewDefaultHTTPBootData() *string { + var defaultUKIURL string + flag.StringVar(&defaultUKIURL, "default-httpboot-uki-url", "", "Default UKI URL for http boot") + + return &defaultUKIURL +} diff --git a/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml b/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml index 4ed47cb5..2adfc4b7 100644 --- a/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml +++ b/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml @@ -64,8 +64,12 @@ spec: type: string type: object x-kubernetes-map-type: atomic + systemIP: + type: string systemUUID: type: string + ukiURL: + type: string type: object status: description: HTTPBootConfigStatus defines the observed state of HTTPBootConfig diff --git a/server/bootserver.go b/server/bootserver.go index b07a8cc5..0fb5c70f 100644 --- a/server/bootserver.go +++ b/server/bootserver.go @@ -4,6 +4,7 @@ package server import ( + "encoding/json" "fmt" "net" "net/http" @@ -31,11 +32,15 @@ type IPXETemplateData struct { IPXEServerURL string } -func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient client.Client, log logr.Logger, defaultIpxeTemplateData IPXETemplateData) { +func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient client.Client, log logr.Logger, defaultIpxeTemplateData IPXETemplateData, defaultUKIURL string) { http.HandleFunc("/ipxe", func(w http.ResponseWriter, r *http.Request) { handleIPXE(w, r, k8sClient, log, ipxeServiceURL, defaultIpxeTemplateData) }) + http.HandleFunc("/httpboot", func(w http.ResponseWriter, r *http.Request) { + handleHTTPBoot(w, r, k8sClient, log, defaultUKIURL) + }) + http.HandleFunc("/ignition/", func(w http.ResponseWriter, r *http.Request) { uuid := path.Base(r.URL.Path) if uuid == "" { @@ -51,7 +56,7 @@ func RunBootServer(ipxeServerAddr string, ipxeServiceURL string, k8sClient clien } if len(ipxeBootConfigList.Items) == 0 { - log.Info("No IPXEBootConfig found with given UUID") + log.Info("No IPXEBootConfig found with given UUID. Trying HTTPBootConfig") handleIgnitionHTTPBoot(w, r, k8sClient, log, uuid) } else { handleIgnitionIPXEBoot(w, r, k8sClient, log, uuid) @@ -267,3 +272,76 @@ func renderIgnition(yamlData []byte) ([]byte, error) { return jsonData, nil } + +func handleHTTPBoot(w http.ResponseWriter, r *http.Request, k8sClient client.Client, log logr.Logger, defaultUKIURL string) { + log.Info("Processing HTTPBoot request", "method", r.Method, "path", r.URL.Path, "clientIP", r.RemoteAddr) + ctx := r.Context() + + clientIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + log.Error(err, "Failed to parse client IP address", "clientIP", r.RemoteAddr) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + clientIPs := []string{clientIP} + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + for _, ip := range strings.Split(xff, ",") { + trimmedIP := strings.TrimSpace(ip) + if trimmedIP != "" { + clientIPs = append(clientIPs, trimmedIP) + } + } + } + + var httpBootConfigs bootv1alpha1.HTTPBootConfigList + for _, ip := range clientIPs { + if err := k8sClient.List(ctx, &httpBootConfigs, client.MatchingFields{"spec.systemIP": ip}); err != nil { + log.Info("Failed to list HTTPBootConfig for IP", "IP", ip, "error", err) + continue + } + + if len(httpBootConfigs.Items) > 0 { + log.Info("Found HTTPBootConfig", "IP", ip) + break + } + } + + var httpBootResponseData map[string]string + if len(httpBootConfigs.Items) == 0 { + log.Info("No HTTPBootConfig found for client IP, delivering default httpboot data", "clientIP", clientIP) + httpBootResponseData = map[string]string{ + "ClientIPs": strings.Join(clientIPs, ","), + "UKIURL": defaultUKIURL, + } + } else { + // TODO: Pick the first HttpBootConfig if multiple CRs are found. + // Implement better validation in the future. + httpBootConfig := httpBootConfigs.Items[0] + + httpBootResponseData = map[string]string{ + "ClientIPs": strings.Join(clientIPs, ","), + "UKIURL": "", + "SystemUUID": "", + } + if httpBootConfig.Spec.UKIURL != "" { + httpBootResponseData["UKIURL"] = httpBootConfig.Spec.UKIURL + } + if httpBootConfig.Spec.SystemUUID != "" { + httpBootResponseData["SystemUUID"] = httpBootConfig.Spec.SystemUUID + } + } + + response, err := json.Marshal(httpBootResponseData) + if err != nil { + log.Error(err, "Failed to marshal response data") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(response); err != nil { + log.Error(err, "Failed to write response") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +}