Skip to content

Add OCI-based default UKI Configuration for HTTPBoot#266

Open
hardikdr wants to merge 1 commit intomainfrom
enh/default-oci-httpboot
Open

Add OCI-based default UKI Configuration for HTTPBoot#266
hardikdr wants to merge 1 commit intomainfrom
enh/default-oci-httpboot

Conversation

@hardikdr
Copy link
Member

@hardikdr hardikdr commented Feb 5, 2026

Proposed Changes

Fixes #

Summary by CodeRabbit

  • New Features

    • New flag to configure the HTTP boot server from an OCI image; server can derive UKI URLs from OCI images (architecture-aware) and accepts an image server URL and architecture.
  • Deprecations

    • The direct default UKI URL flag is deprecated in favor of the OCI-image flag; supplying both emits a runtime warning and the OCI image takes precedence.
  • Bug Fixes / Reliability

    • Improved error handling and validation when constructing default UKI URLs from OCI images.

@github-actions github-actions bot added enhancement New feature or request size/L labels Feb 5, 2026
@hardikdr hardikdr added ok-to-publish Run github action to publish the image ok-to-image and removed enhancement New feature or request size/L labels Feb 5, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

Walkthrough

Centralizes UKI URL construction in a new internal/uki package that resolves OCI images to UKI layer digests. Adds CLI flags to pass a default OCI image and image server URL through server startup, updates bootserver/controller flows to use the new helper, and removes local UKI media-type/constants and legacy default data init.

Changes

Cohort / File(s) Summary
New UKI package
internal/uki/oci.go
Adds MediaTypeUKI and ConstructUKIURLFromOCI(ctx, image, imageServerURL, architecture) plus helpers to resolve OCI references, traverse indexes/manifests, find UKI layer digest, and build a UKI URL.
CLI & server startup
cmd/main.go, server/bootserver.go
Adds --default-httpboot-oci-image and --default-httpboot-uki-url flags; updates RunBootServer and handleHTTPBoot signatures to accept OCI image, UKI URL, image server URL, and architecture; integrates UKI URL construction and runtime deprecation/precedence logs.
Controller & proxy integration
internal/controller/serverbootconfiguration_http_controller.go, server/imageproxyserver.go
Replaces inline OCI/manifest parsing with uki.ConstructUKIURLFromOCI(); switches media-type comparisons to uki.MediaTypeUKI; removes local exported MediaTypeUKI and legacy URL construction code.
Build context
Dockerfile
Expands copied build context from internal/controller/ to the whole internal/ directory so the new internal/uki package is included.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BootServer
    participant UKIHelper as internal/uki
    participant OCIRegistry as OCI Registry / Image Server

    Client->>BootServer: HTTPBoot request (no explicit UKI)
    BootServer->>UKIHelper: ConstructUKIURLFromOCI(image, imageServerURL, arch)
    UKIHelper->>OCIRegistry: Resolve image reference / fetch manifest(s)
    OCIRegistry-->>UKIHelper: Manifest / index JSON
    UKIHelper->>OCIRegistry: Fetch nested manifest/layer blob
    OCIRegistry-->>UKIHelper: Layer blob (UKI) with digest
    UKIHelper-->>BootServer: UKI URL (imageServerURL/image:sha256:digest)
    BootServer-->>Client: HTTPBoot response with constructed UKI URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

api-change, area/metal-automation

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete, containing only placeholder template lines with no actual content, objectives, or issue references provided by the author. Fill in the description with proposed changes (bullet points explaining the implementation), rationale, and any issue references (Fixes #XXXX) that this PR addresses.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding OCI-based default UKI configuration for HTTPBoot. It is specific, concise, and clearly reflects the primary objective across the multiple file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enh/default-oci-httpboot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hardikdr hardikdr force-pushed the enh/default-oci-httpboot branch from b4d08c3 to ac3a292 Compare February 5, 2026 14:30
@github-actions github-actions bot added size/L enhancement New feature or request labels Feb 5, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@internal/uki/oci.go`:
- Around line 57-66: The loop is shadowing the outer manifest variable and may
leave targetManifestDesc set to the initial desc when no architecture match is
found; update the code in the block that iterates indexManifest.Manifests so you
do not shadow the outer manifest (rename the loop variable, e.g., to m or
entry), ensure targetManifestDesc is initialized to a zero/empty descriptor (or
use a found flag) before the loop, set targetManifestDesc only when m.Platform
!= nil && m.Platform.Architecture == architecture, and after the loop check that
targetManifestDesc is actually populated (or the found flag is true) before
using its Digest; reference: indexManifest, targetManifestDesc, manifest
(outer), architecture, desc.
- Around line 20-24: The image parsing in ConstructUKIURLFromOCI incorrectly
splits on all ':' which breaks registry host:port cases (e.g.,
myregistry:5000/repo/image:v1.0); update the function to parse the OCI reference
properly by using a robust parser like github.com/distribution/reference or
github.com/google/go-containerregistry/pkg/name to extract the repository/name
and tag/digest, or at minimum use strings.LastIndex to separate the tag (and
handle image@digest with '@') instead of strings.Split, then build the UKI URL
from the parsed reference parts (retain imageServerURL and architecture logic).

In `@server/bootserver.go`:
- Around line 409-429: The code may return an empty UKI URL when both
defaultOCIImage and defaultUKIURL are empty, causing clients to receive
"UKIURL": ""; update the logic where ukiURL is set (the block handling
defaultOCIImage/defaultUKIURL that populates httpBootResponseData) to detect the
case both defaultOCIImage == "" and defaultUKIURL == "" and either log a
warning/error via log.Warn/ log.Error and return an HTTP error (http.Error) (or
set a clear fallback value) instead of silently returning an empty UKIURL;
ensure the change references ukiURL, defaultOCIImage, defaultUKIURL,
httpBootResponseData and preserves the existing OCI construction path using
uki.ConstructUKIURLFromOCI.
🧹 Nitpick comments (4)
internal/uki/oci.go (1)

102-106: Avoid fmt.Printf for error logging in library code.

Using fmt.Printf for logging is not suitable for production code. The calling code may have structured logging configured, and this bypasses it. Consider returning the close error or accepting a logger parameter.

♻️ Proposed fix to silently ignore close error or propagate it
 	defer func() {
-		if cerr := reader.Close(); cerr != nil {
-			fmt.Printf("failed to close reader: %v\n", cerr)
-		}
+		_ = reader.Close()
 	}()
internal/controller/serverbootconfiguration_http_controller.go (1)

182-187: Consider validating Architecture field is non-empty.

The method validates ImageServerURL but not Architecture. If Architecture is empty, it will be passed to ConstructUKIURLFromOCI, which may fail to find a matching manifest or produce unexpected behavior.

♻️ Proposed fix to validate Architecture
 func (r *ServerBootConfigurationHTTPReconciler) constructUKIURL(ctx context.Context, image string) (string, error) {
 	if strings.TrimSpace(r.ImageServerURL) == "" {
 		return "", fmt.Errorf("image server URL is empty")
 	}
+	if strings.TrimSpace(r.Architecture) == "" {
+		return "", fmt.Errorf("architecture is empty")
+	}
 	return uki.ConstructUKIURLFromOCI(ctx, image, r.ImageServerURL, r.Architecture)
 }
server/bootserver.go (1)

52-52: Consider using a configuration struct for cleaner function signatures.

RunBootServer now has 8 parameters, which impacts readability and maintainability. A configuration struct would make the API cleaner and easier to extend.

♻️ Example configuration struct approach
type BootServerConfig struct {
    Addr                  string
    IPXEServiceURL        string
    K8sClient             client.Client
    Log                   logr.Logger
    DefaultOCIImage       string
    DefaultUKIURL         string
    ImageServerURL        string
    Architecture          string
}

func RunBootServer(cfg BootServerConfig) {
    // ...
}
cmd/main.go (1)

316-316: Both deprecated and new flag values are passed despite "ignoring" warning.

At line 132-133, when both flags are provided, a warning states --default-httpboot-uki-url is being ignored. However, both values are still passed to RunBootServer. While the server logic handles precedence correctly, consider clearing defaultHTTPBootUKIURL when it's being ignored for consistency with the warning message.

♻️ Proposed fix to clear ignored value
 	if defaultHTTPBootOCIImage != "" && defaultHTTPBootUKIURL != "" {
 		setupLog.Info("Ignoring --default-httpboot-uki-url because --default-httpboot-oci-image is set")
+		defaultHTTPBootUKIURL = ""
 	}

Comment on lines +20 to +24
func ConstructUKIURLFromOCI(ctx context.Context, image string, imageServerURL string, architecture string) (string, error) {
imageDetails := strings.Split(image, ":")
if len(imageDetails) != 2 {
return "", fmt.Errorf("invalid image format")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual implementation
find . -name "oci.go" -path "*/uki/*" | head -5

Repository: ironcore-dev/boot-operator

Length of output: 91


🏁 Script executed:

# Check the structure of the file and the specific function
git ls-files | grep -E "internal/uki"

Repository: ironcore-dev/boot-operator

Length of output: 89


🏁 Script executed:

# Let's look at the actual code around lines 20-24
if [ -f internal/uki/oci.go ]; then
  echo "=== File exists, reading content ==="
  wc -l internal/uki/oci.go
  echo ""
  echo "=== Lines 1-50 ==="
  head -50 internal/uki/oci.go
fi

Repository: ironcore-dev/boot-operator

Length of output: 1773


🌐 Web query:

OCI image reference format specification

💡 Result:

In OCI, the registry API (OCI Distribution Spec) treats a “reference” as either a tag or a digest in routes like /v2/<name>/manifests/<reference>—not a full “image ref string”. The Distribution Spec also standardizes the allowed repository name and tag formats via regexes. [2]

Separately, the OCI Image Spec explicitly points to the Distribution (a.k.a. “distribution/distribution”) reference format for “image reference” strings (e.g., annotations like org.opencontainers.image.base.name). [3] In practice, most tooling follows the Distribution Project’s reference grammar:

Common OCI/Docker image reference string grammar (Distribution Project)

reference := name [ ":" tag ] [ "@" digest ]
name      := [domain '/'] remote-name
domain    := host [':' port-number]
tag       := /[\w][\w.-]{0,127}/
digest    := digest-algorithm ":" digest-hex

[1]

OCI Distribution Spec regex constraints (registry API)

  • Repository <name> must match:
    [a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*
    
  • Tag <reference> (when it’s a tag) must be ≤ 128 chars and match:
    [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}
    

[2]

Examples

  • registry.example.com/myorg/myimage:1.2.3
  • registry.example.com/myorg/myimage@sha256:<hex>
  • myorg/myimage:latest (domain omitted; defaults are tool-specific) [1][2]

🌐 Web query:

Go library for parsing OCI image references RFC standards

💡 Result:

For Go, the two most common (and well-tested) libraries for parsing OCI/Docker image references are:

  1. github.com/distribution/reference — the “authoritative” reference parser used across the registry ecosystem. Its grammar explicitly aligns the host portion with RFC 3986 (Appendix A) (IPv4/IPv6-in-brackets, domain, optional port), and it provides helpers like Parse, ParseNamed, ParseNormalizedNamed, and ParseDockerRef. [1]

  2. github.com/google/go-containerregistry/pkg/name — higher-level structured parsing with options for strict vs weak validation and Docker Hub defaulting behavior; entry point is name.ParseReference(...). [2]

If your priority is “OCI image reference parsing + RFC-style host validation,” start with github.com/distribution/reference. [1]

Sources: [1] [2]


Image format validation is fragile for registry URLs with ports.

The current split-by-: approach will fail for image references that include a registry port, such as myregistry:5000/repo/image:v1.0. This would produce more than 2 segments and incorrectly reject the reference as invalid.

Consider using a proper OCI reference parser. Go provides two well-maintained options: github.com/distribution/reference (the authoritative implementation) or github.com/google/go-containerregistry/pkg/name. Alternatively, if a simple fix is preferred, use strings.LastIndex to separate the tag from the image name, though this approach won't handle the full OCI reference syntax including digest anchors (e.g., image@sha256:<hash>).

🐛 Proposed minimal fix using last colon for tag separation
 func ConstructUKIURLFromOCI(ctx context.Context, image string, imageServerURL string, architecture string) (string, error) {
-	imageDetails := strings.Split(image, ":")
-	if len(imageDetails) != 2 {
+	lastColon := strings.LastIndex(image, ":")
+	if lastColon == -1 || lastColon == len(image)-1 {
 		return "", fmt.Errorf("invalid image format")
 	}
+	imageName := image[:lastColon]
+	imageVersion := image[lastColon+1:]

-	ukiDigest, err := getUKIDigestFromNestedManifest(ctx, imageDetails[0], imageDetails[1], architecture)
+	ukiDigest, err := getUKIDigestFromNestedManifest(ctx, imageName, imageVersion, architecture)
 	if err != nil {
 		return "", fmt.Errorf("failed to fetch UKI layer digest: %w", err)
 	}

 	ukiDigest = strings.TrimPrefix(ukiDigest, "sha256:")
-	ukiURL := fmt.Sprintf("%s/%s/sha256-%s.efi", imageServerURL, imageDetails[0], ukiDigest)
+	ukiURL := fmt.Sprintf("%s/%s/sha256-%s.efi", imageServerURL, imageName, ukiDigest)
 	return ukiURL, nil
 }
🤖 Prompt for AI Agents
In `@internal/uki/oci.go` around lines 20 - 24, The image parsing in
ConstructUKIURLFromOCI incorrectly splits on all ':' which breaks registry
host:port cases (e.g., myregistry:5000/repo/image:v1.0); update the function to
parse the OCI reference properly by using a robust parser like
github.com/distribution/reference or
github.com/google/go-containerregistry/pkg/name to extract the repository/name
and tag/digest, or at minimum use strings.LastIndex to separate the tag (and
handle image@digest with '@') instead of strings.Split, then build the UKI URL
from the parsed reference parts (retain imageServerURL and architecture logic).

Comment on lines +57 to +66
for _, manifest := range indexManifest.Manifests {
platform := manifest.Platform
if manifest.Platform != nil && platform.Architecture == architecture {
targetManifestDesc = manifest
break
}
}
if targetManifestDesc.Digest == "" {
return "", fmt.Errorf("failed to find target manifest with architecture %s", architecture)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Variable shadowing and potential logic error in architecture matching.

The loop variable manifest shadows the outer manifest variable declared at line 50. Additionally, if no manifest matches the requested architecture, targetManifestDesc retains its initial value from desc, and the digest check on line 64 may incorrectly pass since desc already has a valid digest from the initial resolution.

🐛 Proposed fix to avoid shadowing and properly detect missing architecture
+		var found bool
-		for _, manifest := range indexManifest.Manifests {
-			platform := manifest.Platform
-			if manifest.Platform != nil && platform.Architecture == architecture {
-				targetManifestDesc = manifest
+		for _, m := range indexManifest.Manifests {
+			if m.Platform != nil && m.Platform.Architecture == architecture {
+				targetManifestDesc = m
+				found = true
 				break
 			}
 		}
-		if targetManifestDesc.Digest == "" {
+		if !found {
 			return "", fmt.Errorf("failed to find target manifest with architecture %s", architecture)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, manifest := range indexManifest.Manifests {
platform := manifest.Platform
if manifest.Platform != nil && platform.Architecture == architecture {
targetManifestDesc = manifest
break
}
}
if targetManifestDesc.Digest == "" {
return "", fmt.Errorf("failed to find target manifest with architecture %s", architecture)
}
var found bool
for _, m := range indexManifest.Manifests {
if m.Platform != nil && m.Platform.Architecture == architecture {
targetManifestDesc = m
found = true
break
}
}
if !found {
return "", fmt.Errorf("failed to find target manifest with architecture %s", architecture)
}
🤖 Prompt for AI Agents
In `@internal/uki/oci.go` around lines 57 - 66, The loop is shadowing the outer
manifest variable and may leave targetManifestDesc set to the initial desc when
no architecture match is found; update the code in the block that iterates
indexManifest.Manifests so you do not shadow the outer manifest (rename the loop
variable, e.g., to m or entry), ensure targetManifestDesc is initialized to a
zero/empty descriptor (or use a found flag) before the loop, set
targetManifestDesc only when m.Platform != nil && m.Platform.Architecture ==
architecture, and after the loop check that targetManifestDesc is actually
populated (or the found flag is true) before using its Digest; reference:
indexManifest, targetManifestDesc, manifest (outer), architecture, desc.

Comment on lines 409 to 429
if len(httpBootConfigs.Items) == 0 {
log.Info("No HTTPBootConfig found for client IP, delivering default httpboot data", "clientIPs", clientIPs)
ukiURL := defaultUKIURL
if defaultOCIImage != "" {
if imageServerURL == "" {
log.Error(fmt.Errorf("image server URL is empty"), "Default OCI image provided but image server URL is not set")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
constructedURL, err := uki.ConstructUKIURLFromOCI(ctx, defaultOCIImage, imageServerURL, architecture)
if err != nil {
log.Error(err, "Failed to construct default UKI URL from OCI image", "image", defaultOCIImage)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
ukiURL = constructedURL
}
httpBootResponseData = map[string]string{
"ClientIPs": strings.Join(clientIPs, ","),
"UKIURL": defaultUKIURL,
"UKIURL": ukiURL,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential silent failure when neither OCI image nor UKI URL is configured.

If both defaultOCIImage and defaultUKIURL are empty, ukiURL will be an empty string, and the response will contain "UKIURL": "". This may cause issues for clients expecting a valid URL. Consider logging a warning or returning an error.

🛡️ Proposed fix to warn when no default UKI configuration is available
 	if len(httpBootConfigs.Items) == 0 {
 		log.Info("No HTTPBootConfig found for client IP, delivering default httpboot data", "clientIPs", clientIPs)
 		ukiURL := defaultUKIURL
 		if defaultOCIImage != "" {
 			if imageServerURL == "" {
 				log.Error(fmt.Errorf("image server URL is empty"), "Default OCI image provided but image server URL is not set")
 				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 				return
 			}
 			constructedURL, err := uki.ConstructUKIURLFromOCI(ctx, defaultOCIImage, imageServerURL, architecture)
 			if err != nil {
 				log.Error(err, "Failed to construct default UKI URL from OCI image", "image", defaultOCIImage)
 				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 				return
 			}
 			ukiURL = constructedURL
 		}
+		if ukiURL == "" {
+			log.Info("Warning: No default UKI URL configured, response will have empty UKIURL")
+		}
 		httpBootResponseData = map[string]string{
🤖 Prompt for AI Agents
In `@server/bootserver.go` around lines 409 - 429, The code may return an empty
UKI URL when both defaultOCIImage and defaultUKIURL are empty, causing clients
to receive "UKIURL": ""; update the logic where ukiURL is set (the block
handling defaultOCIImage/defaultUKIURL that populates httpBootResponseData) to
detect the case both defaultOCIImage == "" and defaultUKIURL == "" and either
log a warning/error via log.Warn/ log.Error and return an HTTP error
(http.Error) (or set a clear fallback value) instead of silently returning an
empty UKIURL; ensure the change references ukiURL, defaultOCIImage,
defaultUKIURL, httpBootResponseData and preserves the existing OCI construction
path using uki.ConstructUKIURLFromOCI.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Dockerfile (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Using a release candidate Go version in production builds is risky.

golang:1.26rc2 is a release candidate, not a stable release. RC versions may have bugs or behavior changes before the final release. Consider using a stable Go version (e.g., golang:1.24 or golang:1.25) for production Docker images.

🤖 Fix all issues with AI agents
In `@internal/uki/oci.go`:
- Around line 36-42: getUKIDigestFromNestedManifest currently constructs an
unauthenticated resolver (docker.NewResolver(docker.ResolverOptions{})), which
fails for private OCI registries; update this function to build ResolverOptions
with Hosts populated and an authorizer created via docker.NewDockerAuthorizer so
the resolver can authenticate. Specifically, modify
getUKIDigestFromNestedManifest to load credentials (either static from
environment/secret or from the Docker config file ~/.docker/config.json), create
an authorizer with docker.NewDockerAuthorizer(...) using those credentials, set
ResolverOptions.Hosts to include registry host info, then pass that options
struct into docker.NewResolver so Resolve() can access private registries.
Ensure the changes reference the getUKIDigestFromNestedManifest function and the
ResolverOptions/ docker.NewDockerAuthorizer usage.
🧹 Nitpick comments (2)
internal/uki/oci.go (1)

102-106: Use structured logging instead of fmt.Printf.

The error from reader.Close() is logged using fmt.Printf, which bypasses the structured logging used elsewhere in the codebase. Consider accepting a logger parameter or returning the error.

♻️ Proposed fix
-func fetchContent(ctx context.Context, resolver remotes.Resolver, ref string, desc ocispec.Descriptor) ([]byte, error) {
+func fetchContent(ctx context.Context, resolver remotes.Resolver, ref string, desc ocispec.Descriptor) (data []byte, err 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)
-		}
-	}()
+	defer func() {
+		if cerr := reader.Close(); cerr != nil && err == nil {
+			err = fmt.Errorf("failed to close reader: %w", cerr)
+		}
+	}()

-	data, err := io.ReadAll(reader)
+	data, err = io.ReadAll(reader)
cmd/main.go (1)

128-133: Consider clearing defaultHTTPBootUKIURL when both flags are provided.

The log message states the UKI URL is being ignored, but both values are still passed to RunBootServer. While the boot server handles precedence correctly, explicitly clearing the deprecated value here would make the behavior more obvious and prevent confusion.

♻️ Proposed fix
 	if defaultHTTPBootOCIImage != "" && defaultHTTPBootUKIURL != "" {
 		setupLog.Info("Ignoring --default-httpboot-uki-url because --default-httpboot-oci-image is set")
+		defaultHTTPBootUKIURL = ""
 	}

Comment on lines +36 to +42
func getUKIDigestFromNestedManifest(ctx context.Context, imageName, imageVersion, architecture string) (string, error) {
resolver := docker.NewResolver(docker.ResolverOptions{})
imageRef := fmt.Sprintf("%s:%s", imageName, imageVersion)
name, desc, err := resolver.Resolve(ctx, imageRef)
if err != nil {
return "", fmt.Errorf("failed to resolve image reference: %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

containerd docker resolver authentication options Go

💡 Result:

In github.com/containerd/containerd/remotes/docker, the “Docker resolver” auth knobs are primarily:

  • Per-host auth (recommended): set ResolverOptions.Hosts (a RegistryHosts func) that returns one or more RegistryHost entries, each of which can carry an Authorizer (RegistryHost.Authorizer). The Authorizer is typically created with docker.NewDockerAuthorizer(...). [1]
  • Authorizer configuration options: docker.NewDockerAuthorizer(opts...) supports:
    • docker.WithAuthCreds(func(host string) (user, secret string, err error))
    • docker.WithAuthHeader(http.Header) (extra headers)
    • docker.WithAuthClient(*http.Client) (custom HTTP client)
    • (also available) docker.WithFetchRefreshToken(...) [1]
  • Deprecated (older) resolver-wide auth fields: ResolverOptions.Authorizer / ResolverOptions.Credentials / ResolverOptions.Host exist but are deprecated in favor of Hosts. [1]

Minimal pattern (static creds) looks like:

import "github.com/containerd/containerd/remotes/docker"

auth := docker.NewDockerAuthorizer(
  docker.WithAuthCreds(func(host string) (string, string, error) {
    return "USERNAME", "PASSWORD_OR_TOKEN", nil
  }),
)

resolver := docker.NewResolver(docker.ResolverOptions{
  Hosts: docker.ConfigureDefaultRegistries(
    docker.WithAuthorizer(auth),
  ),
})

If you want Docker-CLI-style auth resolution (reads $DOCKER_CONFIG/config.json, default ~/.docker/config.json), nerdctl provides a ready-made helper resolver: github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver.New(...), plus an AuthCreds helper compatible with docker.WithAuthCreds. [2]

Sources: containerd remotes/docker API docs (resolver + authorizer + options) [1]; nerdctl dockerconfigresolver helper (Docker config-based creds/resolver) [2].


🏁 Script executed:

# Search for the function and surrounding context in the file
find . -name "oci.go" -path "*/uki/*" | head -5

Repository: ironcore-dev/boot-operator

Length of output: 91


🏁 Script executed:

# Read the oci.go file to understand the full context
cat -n ./internal/uki/oci.go

Repository: ironcore-dev/boot-operator

Length of output: 4490


🏁 Script executed:

# Check how getUKIDigestFromNestedManifest is called and if there's any auth setup elsewhere
rg "getUKIDigestFromNestedManifest|ConstructUKIURLFromOCI" -A 3 -B 3

# Check if there's any auth configuration in the codebase
rg "NewDockerAuthorizer|Authorizer|WithAuthCreds" --type go

Repository: ironcore-dev/boot-operator

Length of output: 2619


🏁 Script executed:

# Check if there's a default OCI image configured and how it's used
rg "defaultOCIImage|ConstructUKIURLFromOCI" -B 5 -A 5

# Look for any configuration files that might specify registry settings
fd "config|config\.yaml|config\.yml|.env" --type f | head -20

# Check the main.go or initialization to understand how the service is configured
fd "main\.go" --type f

Repository: ironcore-dev/boot-operator

Length of output: 5872


🏁 Script executed:

# Check the main.go to understand how RunBootServer is called
cat -n cmd/main.go | head -150

# Also check bootserver.go to see the RunBootServer function signature and how it initializes the resolver
cat -n server/bootserver.go | head -100

Repository: ironcore-dev/boot-operator

Length of output: 11261


🏁 Script executed:

# Check if there are any tests that might clarify the intended usage
rg "getUKIDigestFromNestedManifest|ConstructUKIURLFromOCI" --type go -l

# Look for any TODO or FIXME comments related to authentication
rg "TODO|FIXME|todo|fixme" internal/uki/oci.go

Repository: ironcore-dev/boot-operator

Length of output: 173


Add authentication support for private OCI registries.

The docker.NewResolver(docker.ResolverOptions{}) call creates a resolver without authentication credentials, which will fail when pulling UKI images from private registries. To support private registries, configure the resolver with authentication via ResolverOptions.Hosts and docker.NewDockerAuthorizer(...), either with static credentials or by reading from Docker config (~/.docker/config.json).

🤖 Prompt for AI Agents
In `@internal/uki/oci.go` around lines 36 - 42, getUKIDigestFromNestedManifest
currently constructs an unauthenticated resolver
(docker.NewResolver(docker.ResolverOptions{})), which fails for private OCI
registries; update this function to build ResolverOptions with Hosts populated
and an authorizer created via docker.NewDockerAuthorizer so the resolver can
authenticate. Specifically, modify getUKIDigestFromNestedManifest to load
credentials (either static from environment/secret or from the Docker config
file ~/.docker/config.json), create an authorizer with
docker.NewDockerAuthorizer(...) using those credentials, set
ResolverOptions.Hosts to include registry host info, then pass that options
struct into docker.NewResolver so Resolve() can access private registries.
Ensure the changes reference the getUKIDigestFromNestedManifest function and the
ResolverOptions/ docker.NewDockerAuthorizer usage.

@hardikdr hardikdr added area/metal-automation and removed ok-to-publish Run github action to publish the image ok-to-image labels Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant