diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 175fb6fc..8f6bca3d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,21 @@ jobs: run: | go version + - name: Install libkrun dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential git autoconf automake libtool pkg-config + + - name: Build and install libkrun + run: | + git clone --depth 1 --branch v1.17.0 https://github.com/containers/libkrun.git + cd libkrun + ./autogen.sh + ./configure --prefix=/usr/local + make + sudo make install + cd .. + - name: Get revision SHA and branch (safe) id: get-rev env: diff --git a/deployment/urunc-deploy/Dockerfile b/deployment/urunc-deploy/Dockerfile index 1b38f73d..7fa41c14 100644 --- a/deployment/urunc-deploy/Dockerfile +++ b/deployment/urunc-deploy/Dockerfile @@ -15,6 +15,18 @@ FROM debian:bullseye@sha256:25c0cab214b810db1b3c8adef5a12a92596979abddf86bb364e8d9c9d111df9f AS solo5-builder # Remove libc-bin files to avoid segmentation fault. +FROM debian:bullseye@sha256:25c0cab214b810db1b3c8adef5a12a92596979abddf86bb364e8d9c9d111df9f AS krun-builder +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential git autoconf automake libtool pkg-config && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /krun +# Build and install libkrun from source +RUN git clone --depth 1 --branch v2.4.0 https://github.com/containers/libkrun.git . && \ + ./autogen.sh && \ + ./configure --prefix=/usr/local && \ + make && \ + make install # See more: https://stackoverflow.com/questions/78105004/docker-build-fails-because-unable-to-install-libc-bins RUN rm -f /var/lib/dpkg/info/libc-bin.* && \ apt-get clean && \ @@ -41,6 +53,10 @@ RUN cp /app/tenders/hvt/solo5-hvt /artifacts/ && \ FROM golang:1.24.6-alpine3.21@sha256:50f8a10a46c0c26b5b816a80314f1999196c44c3e3571f41026b061339c29db6 AS urunc-builder RUN apk update && \ apk add --no-cache git make build-base linux-headers +# Copy libkrun headers and libraries from krun-builder +COPY --from=krun-builder /usr/local/include/libkrun.h /usr/local/include/ +COPY --from=krun-builder /usr/local/lib/libkrun* /usr/local/lib/ +COPY --from=krun-builder /usr/local/lib64/libkrun* /usr/local/lib64/ || true WORKDIR /app ARG REPO=urunc-dev/urunc ARG BRANCH=main diff --git a/pkg/unikontainers/hypervisors/krun.go b/pkg/unikontainers/hypervisors/krun.go new file mode 100644 index 00000000..ecb2622e --- /dev/null +++ b/pkg/unikontainers/hypervisors/krun.go @@ -0,0 +1,218 @@ +// Copyright (c) 2023-2025, Nubificus LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hypervisors + +/* +#cgo LDFLAGS: -L/usr/local/lib64 -lkrun +#cgo CFLAGS: -I/usr/local/include +#include +#include +*/ +import "C" +import ( + "fmt" + "os" + "strings" + "unsafe" + + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +const ( + KrunVmm VmmType = "libkrun" + KrunBinary string = "libkrun" +) + +type Krun struct { + binaryPath string + binary string +} + +// Stop kills the libkrun VM process +func (k *Krun) Stop(pid int) error { + return killProcess(pid) +} + +// UsesKVM returns true as libkrun uses KVM +func (k *Krun) UsesKVM() bool { + return true +} + +// SupportsSharedfs returns false as libkrun has limited device support +func (k *Krun) SupportsSharedfs(_ string) bool { + return false +} + +// Path returns the path to libkrun +func (k *Krun) Path() string { + return k.binaryPath +} + +// Ok checks if libkrun is available +func (k *Krun) Ok() error { + // Check if libkrun.so is loadable by attempting to create a context + ctxID := C.krun_create_ctx() + if ctxID < 0 { + return ErrVMMNotInstalled + } + C.krun_free_ctx(C.uint(ctxID)) + return nil +} + +func (k *Krun) Execve(args types.ExecArgs, ukernel types.Unikernel) error { + vmmLog.Debug("Starting libkrun VM configuration") + + // Create libkrun context + ctxID := C.krun_create_ctx() + if ctxID < 0 { + return fmt.Errorf("krun_create_ctx failed with error code: %d", ctxID) + } + defer C.krun_free_ctx(C.uint(ctxID)) + + // Set VM config (memory and vCPUs) + ramMiB := C.uint(args.MemSizeB / (1024 * 1024)) + if ramMiB == 0 { + ramMiB = C.uint(DefaultMemory) + } + numVCPUs := C.uchar(args.VCPUs) + if numVCPUs == 0 { + numVCPUs = 1 + } + + ret := C.krun_set_vm_config(C.uint(ctxID), numVCPUs, ramMiB) + if ret < 0 { + return fmt.Errorf("krun_set_vm_config failed with error code: %d", ret) + } + vmmLog.Debugf("Set VM config: %d vCPUs, %d MiB RAM", numVCPUs, ramMiB) + + // Set root filesystem from sharedfs (if available) + if args.Sharedfs.Path != "" { + cRoot := C.CString(args.Sharedfs.Path) + defer C.free(unsafe.Pointer(cRoot)) + ret = C.krun_set_root(C.uint(ctxID), cRoot) + if ret < 0 { + return fmt.Errorf("krun_set_root failed with error code: %d", ret) + } + vmmLog.Debugf("Set root: %s", args.Sharedfs.Path) + } + + // Add kernel if provided + if args.UnikernelPath != "" { + cKernel := C.CString(args.UnikernelPath) + defer C.free(unsafe.Pointer(cKernel)) + + // Set kernel with optional initrd and command line + var cInitrd *C.char + if args.InitrdPath != "" { + cInitrd = C.CString(args.InitrdPath) + defer C.free(unsafe.Pointer(cInitrd)) + } + + // Build command line from args.Command (which is a string) + var cCmdline *C.char + if args.Command != "" { + cCmdline = C.CString(args.Command) + defer C.free(unsafe.Pointer(cCmdline)) + } + + // kernel_format: 0 for default/auto-detect + ret = C.krun_set_kernel(C.uint(ctxID), cKernel, 0, cInitrd, cCmdline) + if ret < 0 { + return fmt.Errorf("krun_set_kernel failed with error code: %d", ret) + } + if args.InitrdPath != "" { + vmmLog.Debugf("Set kernel: %s, initrd: %s", args.UnikernelPath, args.InitrdPath) + } else { + vmmLog.Debugf("Set kernel: %s", args.UnikernelPath) + } + } + + // Add block devices + blockArgs := ukernel.MonitorBlockCli() + for _, blockArg := range blockArgs { + cBlockID := C.CString(blockArg.ID) + cBlockPath := C.CString(blockArg.Path) + defer C.free(unsafe.Pointer(cBlockID)) + defer C.free(unsafe.Pointer(cBlockPath)) + + ret = C.krun_add_disk(C.uint(ctxID), cBlockID, cBlockPath, C.bool(false)) + if ret < 0 { + return fmt.Errorf("krun_add_disk failed for %s with error code: %d", blockArg.ID, ret) + } + vmmLog.Debugf("Added block device: %s -> %s", blockArg.ID, blockArg.Path) + } + + // Configure networking if tap device provided + if args.Net.TapDev != "" { + cTapDev := C.CString(args.Net.TapDev) + defer C.free(unsafe.Pointer(cTapDev)) + + // krun_add_net_tap takes (ctx_id, tap_name, mac, features, flags) + // Pass nil for mac to use default, 0 for features/flags + ret = C.krun_add_net_tap(C.uint(ctxID), cTapDev, nil, 0, 0) + if ret < 0 { + return fmt.Errorf("krun_add_net_tap failed with error code: %d", ret) + } + vmmLog.Debugf("Set network tap device: %s", args.Net.TapDev) + + // Set MAC address if provided + if args.Net.MAC != "" { + // Parse MAC address to byte array + macStr := strings.ReplaceAll(args.Net.MAC, ":", "") + if len(macStr) == 12 { + var macBytes [6]C.uint8_t + for i := 0; i < 6; i++ { + var b byte + fmt.Sscanf(macStr[i*2:i*2+2], "%02x", &b) + macBytes[i] = C.uint8_t(b) + } + ret = C.krun_set_net_mac(C.uint(ctxID), &macBytes[0]) + if ret < 0 { + return fmt.Errorf("krun_set_net_mac failed with error code: %d", ret) + } + vmmLog.Debugf("Set network MAC: %s", args.Net.MAC) + } + } + } + + // Set environment variables + // krun_set_env expects a null-terminated array of C strings + if len(args.Environment) > 0 { + // Create null-terminated array of environment variables + cEnv := make([]*C.char, len(args.Environment)+1) + for i, env := range args.Environment { + cEnv[i] = C.CString(env) + defer C.free(unsafe.Pointer(cEnv[i])) + } + cEnv[len(args.Environment)] = nil // null-terminate + + ret = C.krun_set_env(C.uint(ctxID), &cEnv[0]) + if ret < 0 { + vmmLog.Warnf("krun_set_env failed with error code: %d", ret) + } + } + + // Start the VM + vmmLog.Debug("Starting libkrun VM") + ret = C.krun_start_enter(C.uint(ctxID)) + if ret < 0 { + return fmt.Errorf("krun_start_enter failed with error code: %d", ret) + } + + // krun_start_enter blocks until VM exits + vmmLog.Debug("libkrun VM exited") + os.Exit(0) + return nil +} diff --git a/pkg/unikontainers/hypervisors/vmm.go b/pkg/unikontainers/hypervisors/vmm.go index 850f7eca..78797023 100644 --- a/pkg/unikontainers/hypervisors/vmm.go +++ b/pkg/unikontainers/hypervisors/vmm.go @@ -52,6 +52,10 @@ var vmmFactories = map[VmmType]VMMFactory{ binary: FirecrackerBinary, createFunc: func(binary, binaryPath string) types.VMM { return &Firecracker{binary: binary, binaryPath: binaryPath} }, }, + KrunVmm: { + binary: KrunBinary, + createFunc: func(binary, binaryPath string) types.VMM { return &Krun{binary: binary, binaryPath: binaryPath} }, + }, } func NewVMM(vmmType VmmType, monitors map[string]types.MonitorConfig) (vmm types.VMM, err error) { diff --git a/pkg/unikontainers/urunc_config.go b/pkg/unikontainers/urunc_config.go index 8b8686de..20990141 100644 --- a/pkg/unikontainers/urunc_config.go +++ b/pkg/unikontainers/urunc_config.go @@ -84,6 +84,7 @@ func defaultMonitorsConfig() map[string]types.MonitorConfig { "hvt": {DefaultMemoryMB: 256, DefaultVCPUs: 1}, "spt": {DefaultMemoryMB: 256, DefaultVCPUs: 1}, "firecracker": {DefaultMemoryMB: 256, DefaultVCPUs: 1}, + "libkrun": {DefaultMemoryMB: 256, DefaultVCPUs: 1}, } } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 14b4db18..8760b181 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -69,3 +69,21 @@ func TestDocker(t *testing.T) { }) } } + +func TestKrun(t *testing.T) { + kvmGroup, err := getKVMGroupID() + if err != nil { + t.Errorf("Failed to get KVM group id") + } + tests := krunTestCases() + + for i := range tests { + tests[i].Groups = []int64{kvmGroup} + } + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + nerdctlTool := newNerdctlTool(tc) + runTest(nerdctlTool, t) + }) + } +} diff --git a/tests/e2e/test_cases.go b/tests/e2e/test_cases.go index 1a367fea..5f6243e7 100644 --- a/tests/e2e/test_cases.go +++ b/tests/e2e/test_cases.go @@ -1157,3 +1157,57 @@ func dockerTestCases(kvmGroup ...int64) []containerTestArgs { }, } } + +func krunTestCases() []containerTestArgs { + return []containerTestArgs{ + { + Image: "harbor.nbfc.io/nubificus/urunc/hello-krun-unikraft:latest", + Name: "Krun-unikraft-hello", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "256M", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, // Skip if libkrun images not available + ExpectOut: "Hello world", + TestFunc: matchTest, + }, + { + Image: "harbor.nbfc.io/nubificus/urunc/redis-krun-unikraft:latest", + Name: "Krun-unikraft-redis", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "512M", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, // Skip if libkrun images not available + TestFunc: pingTest, + }, + { + Image: "harbor.nbfc.io/nubificus/urunc/nginx-krun-unikraft:latest", + Name: "Krun-unikraft-nginx", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "512M", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, // Skip if libkrun images not available + TestFunc: httpGetTest, + }, + } +}