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
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ RUN go mod download
# Copy the go source
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
COPY internal/registry/ internal/registry/
COPY internal/ internal/
COPY server/ server/
COPY templates/ templates/

Expand Down
31 changes: 22 additions & 9 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ func init() {

func main() {
ctx := ctrl.LoggerInto(ctrl.SetupSignalHandler(), setupLog)
defaultHttpUKIURL := NewDefaultHTTPBootData()
skipControllerNameValidation := true

var metricsAddr string
Expand All @@ -81,12 +80,16 @@ func main() {
var imageServerURL string
var architecture string
var allowedRegistries string
var defaultHTTPBootOCIImage string
var defaultHTTPBootUKIURL string

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

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

if defaultHTTPBootUKIURL != "" {
setupLog.Info("Flag --default-httpboot-uki-url is deprecated; use --default-httpboot-oci-image instead")
}
if defaultHTTPBootOCIImage != "" && defaultHTTPBootUKIURL != "" {
setupLog.Info("Ignoring --default-httpboot-uki-url because --default-httpboot-oci-image is set")
}

// set the correct ipxe service URL by getting the address from the environment
var ipxeServiceAddr string
if ipxeServiceURL == "" {
Expand Down Expand Up @@ -317,7 +327,17 @@ func main() {

setupLog.Info("starting boot-server")
go func() {
if err := bootserver.RunBootServer(bootserverAddr, ipxeServiceURL, mgr.GetClient(), serverLog.WithName("bootserver"), *defaultHttpUKIURL); err != nil {
if err := bootserver.RunBootServer(
bootserverAddr,
ipxeServiceURL,
mgr.GetClient(),
serverLog.WithName("bootserver"),
registryValidator,
defaultHTTPBootOCIImage,
defaultHTTPBootUKIURL,
imageServerURL,
architecture,
); err != nil {
setupLog.Error(err, "boot-server exited")
panic(err)
}
Expand Down Expand Up @@ -379,10 +399,3 @@ func IndexHTTPBootConfigByNetworkIDs(ctx context.Context, mgr ctrl.Manager) erro
},
)
}

func NewDefaultHTTPBootData() *string {
var defaultUKIURL string
flag.StringVar(&defaultUKIURL, "default-httpboot-uki-url", "", "Default UKI URL for http boot")

return &defaultUKIURL
}
102 changes: 0 additions & 102 deletions internal/controller/serverbootconfig_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,11 @@ package controller

import (
"context"
"encoding/json"
"fmt"
"io"
"strings"

"github.com/containerd/containerd/remotes"
"github.com/distribution/reference"
metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -68,73 +64,6 @@ func BuildImageReference(imageName, imageVersion string) string {
return fmt.Sprintf("%s:%s", imageName, imageVersion)
}

// FindManifestByArchitecture navigates an OCI image index to find the manifest for a specific architecture.
// If enableCNAMECompat is true, it first tries to find manifests using the legacy CNAME annotation approach.
// Returns the architecture-specific manifest, or an error if not found.
func FindManifestByArchitecture(ctx context.Context, resolver remotes.Resolver, name string, desc ocispec.Descriptor, architecture string, enableCNAMECompat bool) (ocispec.Manifest, error) {
manifestData, err := fetchContent(ctx, resolver, name, desc)
if err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to fetch manifest data: %w", err)
}

var manifest ocispec.Manifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to unmarshal manifest: %w", err)
}

// If not an index, return the manifest directly
if desc.MediaType != ocispec.MediaTypeImageIndex {
return manifest, nil
}

// Parse as index and find architecture-specific manifest
var indexManifest ocispec.Index
if err := json.Unmarshal(manifestData, &indexManifest); err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to unmarshal index manifest: %w", err)
}

var targetManifestDesc ocispec.Descriptor

// Backward compatibility for CNAME prefix based OCI (PXE only)
if enableCNAMECompat {
for _, m := range indexManifest.Manifests {
if strings.HasPrefix(m.Annotations["cname"], CNAMEPrefixMetalPXE) {
if m.Annotations["architecture"] == architecture {
targetManifestDesc = m
break
}
}
}
}

// Standard platform-based architecture lookup
if targetManifestDesc.Digest == "" {
for _, m := range indexManifest.Manifests {
if m.Platform != nil && m.Platform.Architecture == architecture {
targetManifestDesc = m
break
}
}
}

if targetManifestDesc.Digest == "" {
return ocispec.Manifest{}, fmt.Errorf("failed to find target manifest with architecture %s", architecture)
}

// Fetch the nested manifest
nestedData, err := fetchContent(ctx, resolver, name, targetManifestDesc)
if err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to fetch nested manifest: %w", err)
}

var nestedManifest ocispec.Manifest
if err := json.Unmarshal(nestedData, &nestedManifest); err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to unmarshal nested manifest: %w", err)
}

return nestedManifest, nil
}

// ExtractServerNetworkIDs extracts IP addresses (and optionally MAC addresses) from a Server's network interfaces.
// Returns a slice of IP addresses as strings. If includeMACAddresses is true, MAC addresses are also included.
func ExtractServerNetworkIDs(server *metalv1alpha1.Server, includeMACAddresses bool) []string {
Expand Down Expand Up @@ -189,37 +118,6 @@ func EnqueueServerBootConfigsReferencingSecret(ctx context.Context, c client.Cli
return requests
}

// fetchContent fetches the content of an OCI descriptor using the provided resolver.
// It validates the content size matches the descriptor and returns the raw bytes.
func fetchContent(ctx context.Context, resolver remotes.Resolver, ref string, desc ocispec.Descriptor) ([]byte, error) {
fetcher, err := resolver.Fetcher(ctx, ref)
if err != nil {
return nil, fmt.Errorf("failed to get fetcher: %w", err)
}

reader, err := fetcher.Fetch(ctx, desc)
if err != nil {
return nil, fmt.Errorf("failed to fetch content: %w", err)
}

defer func() {
if cerr := reader.Close(); cerr != nil {
fmt.Printf("failed to close reader: %v\n", cerr)
}
}()

data, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read content: %w", err)
}

if int64(len(data)) != desc.Size {
return nil, fmt.Errorf("size mismatch: expected %d, got %d", desc.Size, len(data))
}

return data, nil
}

// PatchServerBootConfigWithError updates the ServerBootConfiguration state to Error
// and sets an ImageValidation condition with the error details.
func PatchServerBootConfigWithError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta"

"github.com/containerd/containerd/remotes/docker"
"github.com/ironcore-dev/boot-operator/internal/oci"
"github.com/ironcore-dev/boot-operator/internal/registry"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -214,7 +215,7 @@ func (r *ServerBootConfigurationHTTPReconciler) getUKIDigestFromNestedManifest(c
return "", fmt.Errorf("failed to resolve image reference: %w", err)
}

manifest, err := FindManifestByArchitecture(ctx, resolver, name, desc, r.Architecture, false)
manifest, err := oci.FindManifestByArchitecture(ctx, resolver, name, desc, r.Architecture, oci.FindManifestOptions{})
if err != nil {
return "", err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/ironcore-dev/boot-operator/api/v1alpha1"
"github.com/ironcore-dev/boot-operator/internal/oci"
"github.com/ironcore-dev/boot-operator/internal/registry"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -229,7 +230,10 @@ func (r *ServerBootConfigurationPXEReconciler) getLayerDigestsFromNestedManifest
return "", "", "", fmt.Errorf("failed to resolve image reference: %w", err)
}

manifest, err := FindManifestByArchitecture(ctx, resolver, name, desc, r.Architecture, true)
manifest, err := oci.FindManifestByArchitecture(ctx, resolver, name, desc, r.Architecture, oci.FindManifestOptions{
EnableCNAMECompat: true,
CNAMEPrefix: CNAMEPrefixMetalPXE,
})
if err != nil {
return "", "", "", err
}
Expand Down
130 changes: 130 additions & 0 deletions internal/oci/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package oci

import (
"context"
"encoding/json"
"fmt"
"io"
"strings"

"github.com/containerd/containerd/remotes"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

type FindManifestOptions struct {
// EnableCNAMECompat enables a legacy manifest selection mode based on annotations.
// This is currently only needed for backward compatibility with older PXE images.
EnableCNAMECompat bool
// CNAMEPrefix is the required prefix for the "cname" annotation when EnableCNAMECompat is set.
CNAMEPrefix string
}

const (
annotationCNAME = "cname"
annotationArchitecture = "architecture"
)

// FindManifestByArchitecture navigates an OCI image index to find the manifest for a specific architecture.
// If opts.EnableCNAMECompat is true, it first tries to find a manifest using the legacy CNAME annotation approach.
func FindManifestByArchitecture(
ctx context.Context,
resolver remotes.Resolver,
name string,
desc ocispec.Descriptor,
architecture string,
opts FindManifestOptions,
) (ocispec.Manifest, error) {
manifestData, err := FetchContent(ctx, resolver, name, desc)
if err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to fetch manifest data: %w", err)
}

var manifest ocispec.Manifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to unmarshal manifest: %w", err)
}

// If not an index, return the manifest directly.
if desc.MediaType != ocispec.MediaTypeImageIndex {
return manifest, nil
}

// Parse as index and find architecture-specific manifest.
var indexManifest ocispec.Index
if err := json.Unmarshal(manifestData, &indexManifest); err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to unmarshal index manifest: %w", err)
}

var targetManifestDesc ocispec.Descriptor

// Backward compatibility for CNAME-prefix based OCI.
if opts.EnableCNAMECompat && strings.TrimSpace(opts.CNAMEPrefix) != "" {
for _, m := range indexManifest.Manifests {
if strings.HasPrefix(m.Annotations[annotationCNAME], opts.CNAMEPrefix) && m.Annotations[annotationArchitecture] == architecture {
targetManifestDesc = m
break
}
}
}

// Standard platform-based architecture lookup.
if targetManifestDesc.Digest == "" {
for _, m := range indexManifest.Manifests {
if m.Platform != nil && m.Platform.Architecture == architecture {
targetManifestDesc = m
break
}
}
}

if targetManifestDesc.Digest == "" {
return ocispec.Manifest{}, fmt.Errorf("failed to find target manifest with architecture %s", architecture)
}

// Fetch the nested manifest.
nestedData, err := FetchContent(ctx, resolver, name, targetManifestDesc)
if err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to fetch nested manifest: %w", err)
}

var nestedManifest ocispec.Manifest
if err := json.Unmarshal(nestedData, &nestedManifest); err != nil {
return ocispec.Manifest{}, fmt.Errorf("failed to unmarshal nested manifest: %w", err)
}

return nestedManifest, nil
}

// FetchContent fetches the content of an OCI descriptor using the provided resolver.
// It validates the content size matches the descriptor and returns the raw bytes.
func FetchContent(ctx context.Context, resolver remotes.Resolver, ref string, desc ocispec.Descriptor) ([]byte, error) {
fetcher, err := resolver.Fetcher(ctx, ref)
if err != nil {
return nil, fmt.Errorf("failed to get fetcher: %w", err)
}

reader, err := fetcher.Fetch(ctx, desc)
if err != nil {
return nil, fmt.Errorf("failed to fetch content: %w", err)
}

defer func() {
if cerr := reader.Close(); cerr != nil {
fmt.Printf("failed to close reader: %v\n", cerr)
}
}()

data, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read content: %w", err)
}

if int64(len(data)) != desc.Size {
return nil, fmt.Errorf("size mismatch: expected %d, got %d", desc.Size, len(data))
}

return data, nil
}
Loading
Loading