Skip to content
Merged
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
1 change: 1 addition & 0 deletions lib/images/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions lib/images/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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
}

Expand Down Expand Up @@ -456,5 +461,6 @@ type containerMetadata struct {
Entrypoint []string
Cmd []string
Env map[string]string
Labels map[string]string
WorkingDir string
}
9 changes: 9 additions & 0 deletions lib/images/oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
7 changes: 7 additions & 0 deletions lib/images/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions lib/images/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 33 additions & 4 deletions lib/instances/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 48 additions & 0 deletions lib/instances/create_kernel_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 7 additions & 7 deletions lib/system/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions lib/system/versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import "runtime"
type KernelVersion string

const (
// ImageKernelVersionLabel lets images request a specific guest kernel artifact.
ImageKernelVersionLabel = "io.kernel.kernel-version"

// Kernel_202601152 is the previous kernel version with vGPU support
Kernel_202601152 KernelVersion = "ch-6.12.8-kernel-1.3-202601152"

Expand Down Expand Up @@ -84,3 +87,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
}
16 changes: 16 additions & 0 deletions lib/system/versions_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading