From 63fa3baa9344c7aa913310293cf9bf7771ebb7d4 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Tue, 31 Mar 2026 16:17:58 -0400 Subject: [PATCH 1/2] Select guest kernel from image label --- lib/images/manager.go | 1 + lib/images/oci.go | 6 ++++ lib/images/oci_test.go | 9 ++++++ lib/images/storage.go | 7 +++++ lib/images/types.go | 1 + lib/instances/create.go | 37 +++++++++++++++++++--- lib/instances/create_kernel_test.go | 48 +++++++++++++++++++++++++++++ lib/system/versions.go | 17 ++++++++++ lib/system/versions_test.go | 16 ++++++++++ 9 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 lib/instances/create_kernel_test.go create mode 100644 lib/system/versions_test.go diff --git a/lib/images/manager.go b/lib/images/manager.go index 27dd7ff5..4987b4f6 100644 --- a/lib/images/manager.go +++ b/lib/images/manager.go @@ -300,6 +300,7 @@ func (m *manager) buildImage(ctx context.Context, ref *ResolvedRef) { meta.Entrypoint = result.Metadata.Entrypoint meta.Cmd = result.Metadata.Cmd meta.Env = result.Metadata.Env + meta.Labels = result.Metadata.Labels meta.WorkingDir = result.Metadata.WorkingDir if err := writeMetadata(m.paths, ref.Repository(), ref.DigestHex(), meta); err != nil { diff --git a/lib/images/oci.go b/lib/images/oci.go index a5c09fbe..0e2e7742 100644 --- a/lib/images/oci.go +++ b/lib/images/oci.go @@ -277,6 +277,7 @@ func (c *ociClient) extractOCIMetadata(layoutTag string) (*containerMetadata, er Entrypoint: configFile.Config.Entrypoint, Cmd: configFile.Config.Cmd, Env: make(map[string]string), + Labels: make(map[string]string), WorkingDir: configFile.Config.WorkingDir, } @@ -292,6 +293,10 @@ func (c *ociClient) extractOCIMetadata(layoutTag string) (*containerMetadata, er } } + for key, value := range configFile.Config.Labels { + meta.Labels[key] = value + } + return meta, nil } @@ -456,5 +461,6 @@ type containerMetadata struct { Entrypoint []string Cmd []string Env map[string]string + Labels map[string]string WorkingDir string } diff --git a/lib/images/oci_test.go b/lib/images/oci_test.go index 4887d106..421a0a68 100644 --- a/lib/images/oci_test.go +++ b/lib/images/oci_test.go @@ -23,6 +23,8 @@ import ( "github.com/stretchr/testify/require" ) +const testImageKernelVersion = "ch-6.12.8-kernel-1.6-202603301" + // BuildKit cache config mediatype - this is what BuildKit uses when exporting // cache with image-manifest=true const buildKitCacheConfigMediaType = "application/vnd.buildkit.cacheconfig.v0" @@ -264,6 +266,10 @@ func createTestDockerImage(t *testing.T) v1.Image { img, err = mutate.Config(img, v1.Config{ Entrypoint: []string{"/usr/local/bin/guest-agent"}, Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + Labels: map[string]string{ + "io.kernel.kernel-version": testImageKernelVersion, + "io.kernel.kernel-release": "6.12.8+", + }, WorkingDir: "/app", }) require.NoError(t, err) @@ -324,6 +330,8 @@ func TestDockerSaveTarballToOCILayoutRoundtrip(t *testing.T) { assert.Equal(t, []string{"/usr/local/bin/guest-agent"}, meta.Entrypoint) assert.Equal(t, "/app", meta.WorkingDir) assert.Contains(t, meta.Env, "PATH") + assert.Equal(t, testImageKernelVersion, meta.Labels["io.kernel.kernel-version"]) + assert.Equal(t, "6.12.8+", meta.Labels["io.kernel.kernel-release"]) // Step 7: Verify unpackLayers produces correct rootfs // umoci's UnpackRootfs extracts directly into the target directory @@ -393,6 +401,7 @@ func TestDockerSaveToOCILayoutCacheHit(t *testing.T) { assert.Equal(t, []string{"/usr/local/bin/guest-agent"}, result.Metadata.Entrypoint) assert.Equal(t, "/app", result.Metadata.WorkingDir) assert.Equal(t, digestStr, result.Digest) + assert.Equal(t, testImageKernelVersion, result.Metadata.Labels["io.kernel.kernel-version"]) // Verify rootfs was unpacked (umoci extracts directly into exportDir) agentPath := filepath.Join(exportDir, "usr", "local", "bin", "guest-agent") diff --git a/lib/images/storage.go b/lib/images/storage.go index accfa9cf..064a8951 100644 --- a/lib/images/storage.go +++ b/lib/images/storage.go @@ -23,6 +23,7 @@ type imageMetadata struct { Entrypoint []string `json:"entrypoint,omitempty"` Cmd []string `json:"cmd,omitempty"` Env map[string]string `json:"env,omitempty"` + Labels map[string]string `json:"labels,omitempty"` Tags tags.Tags `json:"tags,omitempty"` WorkingDir string `json:"working_dir,omitempty"` CreatedAt time.Time `json:"created_at"` @@ -51,6 +52,12 @@ func (m *imageMetadata) toImage() *Image { if len(m.Env) > 0 { img.Env = m.Env } + if len(m.Labels) > 0 { + img.Labels = make(map[string]string, len(m.Labels)) + for key, value := range m.Labels { + img.Labels[key] = value + } + } if len(m.Tags) > 0 { img.Tags = tags.Clone(m.Tags) } diff --git a/lib/images/types.go b/lib/images/types.go index 5d02c7ce..891cb3ff 100644 --- a/lib/images/types.go +++ b/lib/images/types.go @@ -17,6 +17,7 @@ type Image struct { Entrypoint []string Cmd []string Env map[string]string + Labels map[string]string Tags tags.Tags WorkingDir string CreatedAt time.Time diff --git a/lib/instances/create.go b/lib/instances/create.go index ca4be904..1abd4c21 100644 --- a/lib/instances/create.go +++ b/lib/instances/create.go @@ -135,6 +135,19 @@ func (m *manager) createInstance( } m.recordImageUsage(ctx, imageInfo) + defaultKernel := m.systemManager.GetDefaultKernelVersion() + kernelVer, err := resolveCreateKernelVersion(imageInfo, defaultKernel) + if err != nil { + log.ErrorContext(ctx, "invalid image kernel label", "image", req.Image, "error", err) + return nil, err + } + if kernelVer != defaultKernel { + log.InfoContext(ctx, "using image-declared kernel version", + "image", req.Image, + "kernel", kernelVer, + "label", system.ImageKernelVersionLabel) + } + // 3. Generate instance ID (CUID2 for secure, collision-resistant IDs) id := cuid2.Generate() ctx = enrichInstancesTrace(ctx, attribute.String("instance_id", id)) @@ -204,10 +217,7 @@ func (m *manager) createInstance( networkName = "default" } - // 8. Get default kernel version - kernelVer := m.systemManager.GetDefaultKernelVersion() - - // 9. Get process manager for hypervisor type (needed for socket name) + // 8. Get process manager for hypervisor type (needed for socket name) hvType := req.Hypervisor if hvType == "" { hvType = m.defaultHypervisor @@ -830,6 +840,25 @@ func (m *manager) buildHypervisorConfig(ctx context.Context, inst *Instance, ima }, nil } +func resolveCreateKernelVersion(imageInfo *images.Image, defaultKernel system.KernelVersion) (system.KernelVersion, error) { + if imageInfo == nil || len(imageInfo.Labels) == 0 { + return defaultKernel, nil + } + + requested := strings.TrimSpace(imageInfo.Labels[system.ImageKernelVersionLabel]) + if requested == "" { + return defaultKernel, nil + } + + kernelVer, ok := system.ParseKernelVersion(requested) + if !ok { + return "", fmt.Errorf("%w: image %s requests unsupported kernel version %q via label %s", + ErrInvalidRequest, imageInfo.Name, requested, system.ImageKernelVersionLabel) + } + + return kernelVer, nil +} + // kernelArgs returns the kernel command line arguments for the given hypervisor type. // vz uses hvc0 (virtio console), all others use ttyS0 (serial port). func (m *manager) kernelArgs(hvType hypervisor.Type) string { diff --git a/lib/instances/create_kernel_test.go b/lib/instances/create_kernel_test.go new file mode 100644 index 00000000..5b50dd58 --- /dev/null +++ b/lib/instances/create_kernel_test.go @@ -0,0 +1,48 @@ +package instances + +import ( + "errors" + "testing" + + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/system" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveCreateKernelVersionUsesDefaultWithoutLabel(t *testing.T) { + defaultKernel := system.Kernel_202603091 + + got, err := resolveCreateKernelVersion(&images.Image{Name: "docker.io/library/alpine:latest"}, defaultKernel) + require.NoError(t, err) + assert.Equal(t, defaultKernel, got) +} + +func TestResolveCreateKernelVersionUsesImageLabel(t *testing.T) { + defaultKernel := system.Kernel_202603091 + imageInfo := &images.Image{ + Name: "docker.io/onkernel/chromium-headful-vgpu:test", + Labels: map[string]string{ + system.ImageKernelVersionLabel: string(system.Kernel_202603301), + }, + } + + got, err := resolveCreateKernelVersion(imageInfo, defaultKernel) + require.NoError(t, err) + assert.Equal(t, system.Kernel_202603301, got) +} + +func TestResolveCreateKernelVersionRejectsUnknownLabel(t *testing.T) { + defaultKernel := system.Kernel_202603091 + imageInfo := &images.Image{ + Name: "docker.io/onkernel/chromium-headful-vgpu:test", + Labels: map[string]string{ + system.ImageKernelVersionLabel: "ch-6.12.8-kernel-9.9-20990101", + }, + } + + _, err := resolveCreateKernelVersion(imageInfo, defaultKernel) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrInvalidRequest)) + assert.Contains(t, err.Error(), system.ImageKernelVersionLabel) +} diff --git a/lib/system/versions.go b/lib/system/versions.go index 89d12588..af0f5c11 100644 --- a/lib/system/versions.go +++ b/lib/system/versions.go @@ -6,6 +6,12 @@ import "runtime" type KernelVersion string const ( + // ImageKernelVersionLabel lets images request a specific guest kernel artifact. + ImageKernelVersionLabel = "io.kernel.kernel-version" + + // ImageKernelReleaseLabel records the uname -r release the image was built against. + ImageKernelReleaseLabel = "io.kernel.kernel-release" + // Kernel_202601152 is the previous kernel version with vGPU support Kernel_202601152 KernelVersion = "ch-6.12.8-kernel-1.3-202601152" @@ -84,3 +90,14 @@ func GetArch() string { } return arch } + +// ParseKernelVersion returns a supported kernel version by exact artifact name. +func ParseKernelVersion(version string) (KernelVersion, bool) { + for _, supported := range SupportedKernelVersions { + if string(supported) == version { + return supported, true + } + } + + return "", false +} diff --git a/lib/system/versions_test.go b/lib/system/versions_test.go new file mode 100644 index 00000000..9a41a355 --- /dev/null +++ b/lib/system/versions_test.go @@ -0,0 +1,16 @@ +package system + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseKernelVersion(t *testing.T) { + version, ok := ParseKernelVersion(string(Kernel_202603301)) + assert.True(t, ok) + assert.Equal(t, Kernel_202603301, version) + + _, ok = ParseKernelVersion("ch-6.12.8-kernel-9.9-20990101") + assert.False(t, ok) +} From 32d4e43a210cf0fae6d99686a688534c3fbad489 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Tue, 31 Mar 2026 16:43:10 -0400 Subject: [PATCH 2/2] Prepare all kernels on startup --- lib/system/manager.go | 14 +++++++------- lib/system/versions.go | 3 --- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/system/manager.go b/lib/system/manager.go index 6ea7ffc2..c1a7e9f8 100644 --- a/lib/system/manager.go +++ b/lib/system/manager.go @@ -14,7 +14,7 @@ var initrdEnsureLocks sync.Map // Manager handles system files (kernel, initrd) type Manager interface { - // EnsureSystemFiles ensures default kernel and initrd exist + // EnsureSystemFiles ensures all supported kernels and the current initrd exist. EnsureSystemFiles(ctx context.Context) error // GetKernelPath returns path to kernel file @@ -51,13 +51,13 @@ func getInitrdEnsureLock(initrdDir string) *sync.Mutex { return lock.(*sync.Mutex) } -// EnsureSystemFiles ensures default kernel and initrd exist, downloading/building if needed +// EnsureSystemFiles ensures all supported kernels and the current initrd exist, +// downloading/building them if needed. func (m *manager) EnsureSystemFiles(ctx context.Context) error { - kernelVer := m.GetDefaultKernelVersion() - - // Ensure kernel exists - if _, err := m.ensureKernel(kernelVer); err != nil { - return fmt.Errorf("ensure kernel %s: %w", kernelVer, err) + for _, kernelVer := range SupportedKernelVersions { + if _, err := m.ensureKernel(kernelVer); err != nil { + return fmt.Errorf("ensure kernel %s: %w", kernelVer, err) + } } // Ensure initrd exists (builds if missing or stale) diff --git a/lib/system/versions.go b/lib/system/versions.go index af0f5c11..aa07108d 100644 --- a/lib/system/versions.go +++ b/lib/system/versions.go @@ -9,9 +9,6 @@ const ( // ImageKernelVersionLabel lets images request a specific guest kernel artifact. ImageKernelVersionLabel = "io.kernel.kernel-version" - // ImageKernelReleaseLabel records the uname -r release the image was built against. - ImageKernelReleaseLabel = "io.kernel.kernel-release" - // Kernel_202601152 is the previous kernel version with vGPU support Kernel_202601152 KernelVersion = "ch-6.12.8-kernel-1.3-202601152"