Skip to content

Commit b446367

Browse files
committed
Fixes after updates from main
1 parent 08b3bcb commit b446367

6 files changed

Lines changed: 284 additions & 20 deletions

internal/controller/serverbootconfiguration_http_controller.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,18 +200,21 @@ func (r *ServerBootConfigurationHTTPReconciler) getSystemNetworkIDsFromServer(ct
200200
}
201201

202202
func (r *ServerBootConfigurationHTTPReconciler) constructUKIURL(ctx context.Context, image string) (string, error) {
203-
imageDetails := strings.Split(image, ":")
204-
if len(imageDetails) != 2 {
205-
return "", fmt.Errorf("invalid image format")
203+
// Parse image reference - split on last ':' to handle registry:port/image:tag format
204+
lastColon := strings.LastIndex(image, ":")
205+
if lastColon == -1 {
206+
return "", fmt.Errorf("invalid image format: missing tag")
206207
}
208+
imageName := image[:lastColon]
209+
imageVersion := image[lastColon+1:]
207210

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

213216
ukiDigest = strings.TrimPrefix(ukiDigest, "sha256:")
214-
ukiURL := fmt.Sprintf("%s/%s/sha256-%s.efi", r.ImageServerURL, imageDetails[0], ukiDigest)
217+
ukiURL := fmt.Sprintf("%s/%s/sha256-%s.efi", r.ImageServerURL, imageName, ukiDigest)
215218
return ukiURL, nil
216219
}
217220

internal/controller/serverbootconfiguration_http_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ var _ = Describe("ServerBootConfiguration Controller", func() {
6565
ServerRef: corev1.LocalObjectReference{
6666
Name: server.Name,
6767
},
68-
Image: "ghcr.io/ironcore-dev/os-images/test-image:100.1",
68+
Image: MockImageRef("ironcore-dev/os-images/test-image", "100.1"),
6969
IgnitionSecretRef: &corev1.LocalObjectReference{Name: "foo"},
7070
},
7171
}

internal/controller/serverbootconfiguration_pxe_controller.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,19 +223,22 @@ func (r *ServerBootConfigurationPXEReconciler) getSystemIPFromBootConfig(ctx con
223223
}
224224

225225
func (r *ServerBootConfigurationPXEReconciler) getImageDetailsFromConfig(ctx context.Context, config *metalv1alpha1.ServerBootConfiguration) (string, string, string, error) {
226-
imageDetails := strings.Split(config.Spec.Image, ":")
227-
if len(imageDetails) != 2 {
228-
return "", "", "", fmt.Errorf("invalid image format")
226+
// Parse image reference - split on last ':' to handle registry:port/image:tag format
227+
lastColon := strings.LastIndex(config.Spec.Image, ":")
228+
if lastColon == -1 {
229+
return "", "", "", fmt.Errorf("invalid image format: missing tag")
229230
}
231+
imageName := config.Spec.Image[:lastColon]
232+
imageVersion := config.Spec.Image[lastColon+1:]
230233

231-
kernelDigest, initrdDigest, squashFSDigest, err := r.getLayerDigestsFromNestedManifest(ctx, imageDetails[0], imageDetails[1])
234+
kernelDigest, initrdDigest, squashFSDigest, err := r.getLayerDigestsFromNestedManifest(ctx, imageName, imageVersion)
232235
if err != nil {
233236
return "", "", "", fmt.Errorf("failed to fetch layer digests: %w", err)
234237
}
235238

236-
kernelURL := fmt.Sprintf("%s/image?imageName=%s&version=%s&layerDigest=%s", r.IPXEServiceURL, imageDetails[0], imageDetails[1], kernelDigest)
237-
initrdURL := fmt.Sprintf("%s/image?imageName=%s&version=%s&layerDigest=%s", r.IPXEServiceURL, imageDetails[0], imageDetails[1], initrdDigest)
238-
squashFSURL := fmt.Sprintf("%s/image?imageName=%s&version=%s&layerDigest=%s", r.IPXEServiceURL, imageDetails[0], imageDetails[1], squashFSDigest)
239+
kernelURL := fmt.Sprintf("%s/image?imageName=%s&version=%s&layerDigest=%s", r.IPXEServiceURL, imageName, imageVersion, kernelDigest)
240+
initrdURL := fmt.Sprintf("%s/image?imageName=%s&version=%s&layerDigest=%s", r.IPXEServiceURL, imageName, imageVersion, initrdDigest)
241+
squashFSURL := fmt.Sprintf("%s/image?imageName=%s&version=%s&layerDigest=%s", r.IPXEServiceURL, imageName, imageVersion, squashFSDigest)
239242

240243
return kernelURL, initrdURL, squashFSURL, nil
241244
}

internal/controller/serverbootconfiguration_pxe_controller_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ var _ = Describe("ServerBootConfiguration Controller", func() {
6464
ServerRef: corev1.LocalObjectReference{
6565
Name: server.Name,
6666
},
67-
Image: "ghcr.io/ironcore-dev/os-images/gardenlinux:1877.0",
67+
Image: MockImageRef("ironcore-dev/os-images/gardenlinux", "1877.0"),
6868
IgnitionSecretRef: &corev1.LocalObjectReference{Name: "foo"},
6969
},
7070
}
@@ -125,7 +125,7 @@ var _ = Describe("ServerBootConfiguration Controller", func() {
125125
ServerRef: corev1.LocalObjectReference{
126126
Name: server.Name,
127127
},
128-
Image: "ghcr.io/gardenlinux/gardenlinux:1772.0",
128+
Image: MockImageRef("gardenlinux/gardenlinux", "1772.0"),
129129
IgnitionSecretRef: &corev1.LocalObjectReference{Name: "foo"},
130130
},
131131
}

internal/controller/suite_test.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636

3737
bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
3838
"github.com/ironcore-dev/boot-operator/internal/registry"
39+
testregistry "github.com/ironcore-dev/boot-operator/test/registry"
3940
//+kubebuilder:scaffold:imports
4041
)
4142

@@ -46,9 +47,10 @@ const (
4647
)
4748

4849
var (
49-
cfg *rest.Config
50-
k8sClient client.Client
51-
testEnv *envtest.Environment
50+
cfg *rest.Config
51+
k8sClient client.Client
52+
testEnv *envtest.Environment
53+
mockRegistry *testregistry.MockRegistry
5254
)
5355

5456
func TestControllers(t *testing.T) {
@@ -64,8 +66,17 @@ func TestControllers(t *testing.T) {
6466
var _ = BeforeSuite(func() {
6567
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
6668

67-
// Set ALLOWED_REGISTRIES for tests to use ghcr.io images
68-
Expect(os.Setenv("ALLOWED_REGISTRIES", "ghcr.io")).To(Succeed())
69+
By("starting mock OCI registry")
70+
mockRegistry = testregistry.NewMockRegistry()
71+
DeferCleanup(mockRegistry.Close)
72+
73+
// Push test images to mock registry (using simple paths without localhost prefix)
74+
Expect(mockRegistry.PushPXEImage("ironcore-dev/os-images/gardenlinux", "1877.0", runtime.GOARCH)).To(Succeed())
75+
Expect(mockRegistry.PushPXEImageOldFormat("gardenlinux/gardenlinux", "1772.0", runtime.GOARCH)).To(Succeed())
76+
Expect(mockRegistry.PushHTTPImage("ironcore-dev/os-images/test-image", "100.1")).To(Succeed())
77+
78+
// Set ALLOWED_REGISTRIES to use mock registry
79+
Expect(os.Setenv("ALLOWED_REGISTRIES", mockRegistry.RegistryAddress())).To(Succeed())
6980
DeferCleanup(func() {
7081
Expect(os.Unsetenv("ALLOWED_REGISTRIES")).To(Succeed())
7182
})
@@ -165,3 +176,8 @@ func SetupTest() *corev1.Namespace {
165176

166177
return ns
167178
}
179+
180+
// MockImageRef returns a fully qualified image reference for the mock registry
181+
func MockImageRef(name, tag string) string {
182+
return fmt.Sprintf("%s/%s:%s", mockRegistry.RegistryAddress(), name, tag)
183+
}

test/registry/registry.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package registry
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"net/http/httptest"
11+
"strings"
12+
"sync"
13+
14+
"github.com/opencontainers/go-digest"
15+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
16+
)
17+
18+
const (
19+
MediaTypeKernel = "application/vnd.ironcore.image.kernel"
20+
MediaTypeInitrd = "application/vnd.ironcore.image.initramfs"
21+
MediaTypeSquashfs = "application/vnd.ironcore.image.squashfs"
22+
MediaTypeUKI = "application/vnd.ironcore.image.uki"
23+
MediaTypeKernelOld = "application/io.gardenlinux.kernel"
24+
MediaTypeInitrdOld = "application/io.gardenlinux.initrd"
25+
)
26+
27+
// MockRegistry provides an in-memory OCI registry for testing
28+
type MockRegistry struct {
29+
mu sync.RWMutex
30+
manifests map[string]ocispec.Manifest // Key: "name:tag" or "name@digest"
31+
manifestsByDigest map[digest.Digest]ocispec.Manifest // For digest lookups
32+
indexes map[string]ocispec.Index
33+
blobs map[digest.Digest][]byte
34+
server *httptest.Server
35+
}
36+
37+
// NewMockRegistry creates a new mock OCI registry server
38+
func NewMockRegistry() *MockRegistry {
39+
r := &MockRegistry{
40+
manifests: make(map[string]ocispec.Manifest),
41+
manifestsByDigest: make(map[digest.Digest]ocispec.Manifest),
42+
indexes: make(map[string]ocispec.Index),
43+
blobs: make(map[digest.Digest][]byte),
44+
}
45+
46+
mux := http.NewServeMux()
47+
48+
// OCI Distribution API endpoints
49+
mux.HandleFunc("/v2/", func(w http.ResponseWriter, req *http.Request) {
50+
if req.URL.Path == "/v2/" {
51+
// Version check endpoint
52+
w.Header().Set("Content-Type", "application/json")
53+
w.WriteHeader(http.StatusOK)
54+
_ = json.NewEncoder(w).Encode(map[string]string{"version": "2.0"})
55+
return
56+
}
57+
58+
// Manifest endpoint
59+
if strings.Contains(req.URL.Path, "/manifests/") {
60+
r.handleManifest(w, req)
61+
return
62+
}
63+
64+
http.NotFound(w, req)
65+
})
66+
67+
r.server = httptest.NewServer(mux)
68+
return r
69+
}
70+
71+
// Close shuts down the mock registry server
72+
func (r *MockRegistry) Close() {
73+
r.server.Close()
74+
}
75+
76+
// URL returns the base URL of the mock registry
77+
func (r *MockRegistry) URL() string {
78+
return r.server.URL
79+
}
80+
81+
// RegistryAddress returns the registry address without http:// prefix
82+
func (r *MockRegistry) RegistryAddress() string {
83+
return strings.TrimPrefix(r.URL(), "http://")
84+
}
85+
86+
// pushPXEManifest is a helper to store PXE manifests with given media types
87+
func (r *MockRegistry) pushPXEManifest(name, tag string, kernelMedia, initrdMedia string) {
88+
kernelDigest := digest.FromString(fmt.Sprintf("kernel-%s-%s", name, tag))
89+
initrdDigest := digest.FromString(fmt.Sprintf("initrd-%s-%s", name, tag))
90+
squashfsDigest := digest.FromString(fmt.Sprintf("squashfs-%s-%s", name, tag))
91+
configDigest := digest.FromString(fmt.Sprintf("config-%s-%s", name, tag))
92+
93+
manifest := ocispec.Manifest{
94+
MediaType: ocispec.MediaTypeImageManifest,
95+
Config: ocispec.Descriptor{
96+
MediaType: "application/vnd.oci.image.config.v1+json",
97+
Digest: configDigest,
98+
Size: 2,
99+
},
100+
Layers: []ocispec.Descriptor{
101+
{
102+
MediaType: kernelMedia,
103+
Digest: kernelDigest,
104+
Size: 1024,
105+
},
106+
{
107+
MediaType: initrdMedia,
108+
Digest: initrdDigest,
109+
Size: 2048,
110+
},
111+
{
112+
MediaType: MediaTypeSquashfs,
113+
Digest: squashfsDigest,
114+
Size: 4096,
115+
},
116+
},
117+
}
118+
119+
ref := fmt.Sprintf("%s:%s", name, tag)
120+
r.manifests[ref] = manifest
121+
122+
// Calculate and store manifest digest
123+
manifestBytes, _ := json.Marshal(manifest)
124+
manifestDigest := digest.FromBytes(manifestBytes)
125+
r.manifestsByDigest[manifestDigest] = manifest
126+
127+
r.blobs[manifest.Config.Digest] = []byte("{}")
128+
r.blobs[kernelDigest] = []byte("kernel-data")
129+
r.blobs[initrdDigest] = []byte("initrd-data")
130+
r.blobs[squashfsDigest] = []byte("squashfs-data")
131+
}
132+
133+
// PushPXEImage adds a PXE boot image with kernel, initrd, and squashfs layers
134+
func (r *MockRegistry) PushPXEImage(name, tag, architecture string) error {
135+
r.mu.Lock()
136+
defer r.mu.Unlock()
137+
r.pushPXEManifest(name, tag, MediaTypeKernel, MediaTypeInitrd)
138+
return nil
139+
}
140+
141+
// PushPXEImageOldFormat adds a PXE boot image using old Gardenlinux media types
142+
func (r *MockRegistry) PushPXEImageOldFormat(name, tag, architecture string) error {
143+
r.mu.Lock()
144+
defer r.mu.Unlock()
145+
r.pushPXEManifest(name, tag, MediaTypeKernelOld, MediaTypeInitrdOld)
146+
return nil
147+
}
148+
149+
// PushHTTPImage adds an HTTP boot image with UKI layer
150+
func (r *MockRegistry) PushHTTPImage(name, tag string) error {
151+
r.mu.Lock()
152+
defer r.mu.Unlock()
153+
154+
ukiDigest := digest.FromString(fmt.Sprintf("uki-%s-%s", name, tag))
155+
configDigest := digest.FromString(fmt.Sprintf("config-%s-%s", name, tag))
156+
157+
manifest := ocispec.Manifest{
158+
MediaType: ocispec.MediaTypeImageManifest,
159+
Config: ocispec.Descriptor{
160+
MediaType: "application/vnd.oci.image.config.v1+json",
161+
Digest: configDigest,
162+
Size: 2,
163+
},
164+
Layers: []ocispec.Descriptor{
165+
{
166+
MediaType: MediaTypeUKI,
167+
Digest: ukiDigest,
168+
Size: 8192,
169+
},
170+
},
171+
}
172+
173+
ref := fmt.Sprintf("%s:%s", name, tag)
174+
r.manifests[ref] = manifest
175+
176+
// Calculate and store manifest digest
177+
manifestBytes, _ := json.Marshal(manifest)
178+
manifestDigest := digest.FromBytes(manifestBytes)
179+
r.manifestsByDigest[manifestDigest] = manifest
180+
181+
r.blobs[manifest.Config.Digest] = []byte("{}")
182+
r.blobs[ukiDigest] = []byte("uki-data")
183+
184+
return nil
185+
}
186+
187+
func (r *MockRegistry) handleManifest(w http.ResponseWriter, req *http.Request) {
188+
// Match pattern: /v2/{name}/manifests/{reference}
189+
parts := strings.Split(strings.TrimPrefix(req.URL.Path, "/v2/"), "/manifests/")
190+
if len(parts) != 2 {
191+
http.NotFound(w, req)
192+
return
193+
}
194+
195+
name := parts[0]
196+
reference := parts[1]
197+
198+
r.mu.RLock()
199+
defer r.mu.RUnlock()
200+
201+
var manifest ocispec.Manifest
202+
var exists bool
203+
var manifestDigest string
204+
205+
// Check if reference is a digest (sha256:...)
206+
if strings.HasPrefix(reference, "sha256:") {
207+
// Look up by digest
208+
dgst, err := digest.Parse(reference)
209+
if err != nil {
210+
http.Error(w, "invalid digest", http.StatusBadRequest)
211+
return
212+
}
213+
manifest, exists = r.manifestsByDigest[dgst]
214+
manifestDigest = reference
215+
} else {
216+
// Look up by tag
217+
imageRef := fmt.Sprintf("%s:%s", name, reference)
218+
manifest, exists = r.manifests[imageRef]
219+
// Calculate digest for Content-Digest header
220+
manifestBytes, _ := json.Marshal(manifest)
221+
dgst := digest.FromBytes(manifestBytes)
222+
manifestDigest = dgst.String()
223+
}
224+
225+
if !exists {
226+
http.Error(w, "manifest not found", http.StatusNotFound)
227+
return
228+
}
229+
230+
if req.Method == http.MethodHead {
231+
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
232+
w.Header().Set("Docker-Content-Digest", manifestDigest)
233+
w.WriteHeader(http.StatusOK)
234+
return
235+
}
236+
237+
manifestData, _ := json.Marshal(manifest)
238+
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
239+
w.Header().Set("Docker-Content-Digest", manifestDigest)
240+
w.WriteHeader(http.StatusOK)
241+
_, _ = w.Write(manifestData)
242+
}

0 commit comments

Comments
 (0)