diff --git a/README.md b/README.md index 370220877..b1e843208 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ supported VM/Sandbox monitors and unikernels: | Mewz | QEMU | x86 | In-memory | | Linux | QEMU, Firecracker, Cloud-HYpervisor | x86,aarch64 | Initrd, Block/Devmapper, 9pfs, Virtiofs | | Hermit | QEMU | x86 | Initrd | +| IncludeOS | QEMU, Solo5-hvt, Solo5-spt | x86,aarch64 | Block/Devmapper | We plan to add support for more unikernel frameworks and other platforms too. Feel free to [contact](#Contact) us for a specific unikernel framework or similar diff --git a/docs/hypervisor-support.md b/docs/hypervisor-support.md index 94e50b121..d2f0070c4 100644 --- a/docs/hypervisor-support.md +++ b/docs/hypervisor-support.md @@ -66,6 +66,7 @@ Supported unikernel frameworks with `urunc`: - [Mewz](../unikernel-support#mewz) - [Linux](../unikernel-support#linux) - [Hermit](../unikernel-support#hermit) +- [IncludeOS](../unikernel-support#includeos) An example unikernel: @@ -245,6 +246,7 @@ Supported unikernel frameworks with `urunc`: - [Rumprun](../unikernel-support#rumprun) - [MirageOS](../unikernel-support#mirage) +- [IncludeOS](../unikernel-support#includeos) An example unikernel with a block image inside the container's rootfs: @@ -385,6 +387,7 @@ Supported unikernel frameworks with `urunc`: - [Rumprun](../unikernel-support#rumprun) - [MirageOS](../unikernel-support#mirage) +- [IncludeOS](../unikernel-support#includeos) An example unikernel which utilizes devmapper for block storage: diff --git a/docs/index.md b/docs/index.md index ce91b6371..76f6a3f03 100644 --- a/docs/index.md +++ b/docs/index.md @@ -61,6 +61,7 @@ Sandbox monitors, along with the unikernels that can run on top of them. | [Mewz](./unikernel-support#mewz)| [Qemu](./hypervisor-support#qemu) | x86 | In-memory | | [Linux](./unikernel-support#linux)| [Qemu](./hypervisor-support#qemu), [Firecracker](./hypervisor-support#aws-firecracker), [Cloud-Hypervisor](./hypervisor-support#cloud-hypervisor) | x86, aarch64 | Initrd, Block/Devmapper, 9pfs, Virtiofs | | [Hermit](./unikernel-support#hermit)| [Qemu](./hypervisor-support#qemu) | x86 | Initrd | +| [IncludeOS](./unikernel-support#includeos)| [Qemu](./hypervisor-support#qemu), [Solo5-hvt](./hypervisor-support#solo5-hvt), [Solo5-spt](./hypervisor-support#solo5-spt) | x86, aarch64 | Block/Devmapper | diff --git a/docs/unikernel-support.md b/docs/unikernel-support.md index 7dfb17d83..646e5f1ed 100644 --- a/docs/unikernel-support.md +++ b/docs/unikernel-support.md @@ -392,6 +392,36 @@ An example of running a Hermit unikernel with `urunc`: sudo nerdctl run --rm -ti --runtime io.containerd.urunc.v2 harbor.nbfc.io/nubificus/urunc/hello-world-qemu-hermit-initrd:latest ``` +## IncludeOS + +[IncludeOS](https://github.com/includeos/IncludeOS) is an open-source, modular unikernel written in C++ designed for building cloud services and network applications. IncludeOS can execute on top of [Solo5](https://github.com/Solo5/solo5) and [Qemu](https://www.qemu.org/), making it compatible with `urunc` across these monitors. + +### VMMs and other sandbox monitors + +IncludeOS runs on top of [QEMU](https://www.qemu.org/) as well as [Solo5-hvt](https://github.com/Solo5/solo5) and [Solo5-spt](https://github.com/Solo5/solo5). It accesses the network via standard interfaces: +- **QEMU**: virtio-net +- **Solo5**: Solo5 network interface (`--net:service=...`) + +For storage, IncludeOS supports block devices: +- **QEMU**: virtio-blk +- **Solo5**: Solo5 block device interface (`--block:rootfs=...`) + +### IncludeOS and `urunc` + +In the case of IncludeOS, `urunc` provides support for booting on QEMU, Solo5-hvt, and Solo5-spt. When the container is configured with network access, `urunc` provides the appropriate monitor networking configuration. + +An example of running an IncludeOS unikernel with `urunc` on top of QEMU: + +```bash +sudo nerdctl run --rm -ti --runtime io.containerd.urunc.v2 harbor.nbfc.io/nubificus/urunc/hello-qemu-includeos:latest +``` + +An example of running an IncludeOS unikernel with `urunc` on top of Solo5-hvt: + +```bash +sudo nerdctl run --rm -ti --runtime io.containerd.urunc.v2 harbor.nbfc.io/nubificus/urunc/hello-hvt-includeos:latest +``` + ## Future unikernels and frameworks: In the near future, we plan to add support for the following frameworks: diff --git a/docs/variables/versions.yml b/docs/variables/versions.yml index eebc7b551..54740da2f 100644 --- a/docs/variables/versions.yml +++ b/docs/variables/versions.yml @@ -14,3 +14,5 @@ versions: kubectl: "1.30.0" calico: "3.28.0" kubernetes: "1.30" + includeos: "6fd33d3" + nix: "2.35.2" diff --git a/pkg/unikontainers/unikernels/includeos.go b/pkg/unikontainers/unikernels/includeos.go new file mode 100644 index 000000000..6a8f04b64 --- /dev/null +++ b/pkg/unikontainers/unikernels/includeos.go @@ -0,0 +1,130 @@ +// Copyright (c) 2023-2026, 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 unikernels + +import ( + "strings" + + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +const IncludeosUnikernel string = "includeos" + +type IncludeOS struct { + Command string + Monitor string + Net IncludeOSNet + Block []IncludeOSBlock +} + +type IncludeOSNet struct { + Address string + Gateway string + Mask string +} + +type IncludeOSBlock struct { + ID string + HostPath string +} + +func (i *IncludeOS) CommandString() (string, error) { + var parts []string + if i.Net.Address != "" { + parts = append(parts, i.Net.Address, i.Net.Gateway, i.Net.Mask) + } + if i.Command != "" { + parts = append(parts, i.Command) + } + return strings.Join(parts, " "), nil +} + +func (i *IncludeOS) SupportsBlock() bool { + return true +} + +// SupportsFS returns whether IncludeOS supports shared filesystem mounts (such as 9pfs or virtiofs). +// IncludeOS does not support 9pfs or virtiofs; block storage is handled via SupportsBlock. +func (i *IncludeOS) SupportsFS(_ string) bool { + return false +} + +func (i *IncludeOS) MonitorNetCli(ifName string, mac string) string { + switch i.Monitor { + case "hvt", "spt": + netOption := "--net:service=" + ifName + netOption += " --net-mac:service=" + mac + return netOption + case "qemu": + return "" + default: + return "" + } +} + +func (i *IncludeOS) MonitorBlockCli() []types.MonitorBlockArgs { + if len(i.Block) == 0 { + return nil + } + switch i.Monitor { + case "hvt", "spt": + return []types.MonitorBlockArgs{ + { + ID: "rootfs", + Path: i.Block[0].HostPath, + }, + } + case "qemu": + return []types.MonitorBlockArgs{ + { + ID: "rootfs", + Path: i.Block[0].HostPath, + }, + } + default: + return nil + } +} + +func (i *IncludeOS) MonitorCli() types.MonitorCliArgs { + return types.MonitorCliArgs{} +} + +func (i *IncludeOS) Init(data types.UnikernelParams) error { + if data.Net.Mask != "" { + i.Net.Address = data.Net.IP + i.Net.Gateway = data.Net.Gateway + i.Net.Mask = data.Net.Mask + } + + i.Block = make([]IncludeOSBlock, 0, len(data.Block)) + for _, blk := range data.Block { + newBlk := IncludeOSBlock{ + ID: blk.ID, + HostPath: blk.Source, + } + i.Block = append(i.Block, newBlk) + } + + i.Command = strings.Join(data.CmdLine, " ") + i.Monitor = data.Monitor + + return nil +} + +func newIncludeos() *IncludeOS { + includeosStruct := new(IncludeOS) + return includeosStruct +} diff --git a/pkg/unikontainers/unikernels/includeos_test.go b/pkg/unikontainers/unikernels/includeos_test.go new file mode 100644 index 000000000..83c0b2f11 --- /dev/null +++ b/pkg/unikontainers/unikernels/includeos_test.go @@ -0,0 +1,207 @@ +// Copyright (c) 2023-2026, 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 unikernels + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +func TestIncludeOSCommandString(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + includeos *IncludeOS + expected string + }{ + { + name: "empty configuration", + includeos: &IncludeOS{}, + expected: "", + }, + { + name: "with network and command", + includeos: &IncludeOS{ + Net: IncludeOSNet{ + Address: "192.168.1.10", + Gateway: "192.168.1.1", + Mask: "255.255.255.0", + }, + Command: "my-app --arg1", + }, + expected: "192.168.1.10 192.168.1.1 255.255.255.0 my-app --arg1", + }, + { + name: "with network only", + includeos: &IncludeOS{ + Net: IncludeOSNet{ + Address: "10.0.0.2", + Gateway: "10.0.0.1", + Mask: "255.0.0.0", + }, + }, + expected: "10.0.0.2 10.0.0.1 255.0.0.0", + }, + { + name: "with command only", + includeos: &IncludeOS{ + Command: "run server", + }, + expected: "run server", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result, err := tc.includeos.CommandString() + require.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestIncludeOSInit(t *testing.T) { + t.Parallel() + + params := types.UnikernelParams{ + CmdLine: []string{"hello", "world"}, + Monitor: "qemu", + Net: types.NetDevParams{ + IP: "192.168.1.20", + Gateway: "192.168.1.1", + Mask: "255.255.255.0", + }, + Block: []types.BlockDevParams{ + { + ID: "disk0", + Source: "/dev/loop0", + }, + }, + } + + inc := newIncludeos() + err := inc.Init(params) + require.NoError(t, err) + + assert.Equal(t, "hello world", inc.Command) + assert.Equal(t, "qemu", inc.Monitor) + assert.Equal(t, "192.168.1.20", inc.Net.Address) + assert.Equal(t, "192.168.1.1", inc.Net.Gateway) + assert.Equal(t, "255.255.255.0", inc.Net.Mask) + require.Len(t, inc.Block, 1) + assert.Equal(t, "disk0", inc.Block[0].ID) + assert.Equal(t, "/dev/loop0", inc.Block[0].HostPath) + + // Test Init with empty network mask + emptyNetParams := types.UnikernelParams{ + CmdLine: []string{"test"}, + Monitor: "hvt", + Net: types.NetDevParams{ + IP: "10.0.0.1", + Mask: "", + }, + } + inc2 := newIncludeos() + err = inc2.Init(emptyNetParams) + require.NoError(t, err) + assert.Empty(t, inc2.Net.Address) + assert.Empty(t, inc2.Net.Mask) +} + +func TestIncludeOSSupports(t *testing.T) { + t.Parallel() + + inc := newIncludeos() + assert.True(t, inc.SupportsBlock()) + assert.False(t, inc.SupportsFS("ext4")) + assert.False(t, inc.SupportsFS("9pfs")) + assert.False(t, inc.SupportsFS("virtiofs")) +} + +func TestIncludeOSMonitorNetCli(t *testing.T) { + t.Parallel() + + tests := []struct { + monitor string + expected string + }{ + { + monitor: "hvt", + expected: "--net:service=tap0_urunc --net-mac:service=52:54:00:12:34:56", + }, + { + monitor: "spt", + expected: "--net:service=tap0_urunc --net-mac:service=52:54:00:12:34:56", + }, + { + monitor: "qemu", + expected: "", + }, + { + monitor: "unknown", + expected: "", + }, + } + + for _, tt := range tests { + inc := &IncludeOS{Monitor: tt.monitor} + res := inc.MonitorNetCli("tap0_urunc", "52:54:00:12:34:56") + assert.Equal(t, tt.expected, res) + } +} + +func TestIncludeOSMonitorBlockCli(t *testing.T) { + t.Parallel() + + // Empty block devices + incEmpty := &IncludeOS{Monitor: "qemu"} + assert.Nil(t, incEmpty.MonitorBlockCli()) + + // Configured block devices + inc := &IncludeOS{ + Block: []IncludeOSBlock{ + { + ID: "disk0", + HostPath: "/tmp/rootfs.raw", + }, + }, + } + + for _, mon := range []string{"hvt", "spt", "qemu"} { + inc.Monitor = mon + blockCli := inc.MonitorBlockCli() + require.Len(t, blockCli, 1) + assert.Equal(t, "rootfs", blockCli[0].ID) + assert.Equal(t, "/tmp/rootfs.raw", blockCli[0].Path) + } + + inc.Monitor = "other" + assert.Nil(t, inc.MonitorBlockCli()) +} + +func TestIncludeOSNewFactory(t *testing.T) { + t.Parallel() + + uk, err := New(IncludeosUnikernel) + require.NoError(t, err) + assert.NotNil(t, uk) + _, ok := uk.(*IncludeOS) + assert.True(t, ok) +} diff --git a/pkg/unikontainers/unikernels/unikernel.go b/pkg/unikontainers/unikernels/unikernel.go index 103c20856..5e2378e36 100644 --- a/pkg/unikontainers/unikernels/unikernel.go +++ b/pkg/unikontainers/unikernels/unikernel.go @@ -42,6 +42,9 @@ func New(unikernelType string) (types.Unikernel, error) { case HermitUnikernel: unikernel := newHermit() return unikernel, nil + case IncludeosUnikernel: + unikernel := newIncludeos() + return unikernel, nil default: return nil, ErrNotSupportedUnikernel } diff --git a/tests/e2e/test_cases.go b/tests/e2e/test_cases.go index 3b90a381e..728053d83 100644 --- a/tests/e2e/test_cases.go +++ b/tests/e2e/test_cases.go @@ -472,6 +472,57 @@ func nerdctlTestCases() []containerTestArgs { Skippable: false, TestFunc: pingTest, }, + { + Image: "harbor.nbfc.io/nubificus/urunc/hello-qemu-includeos:latest", + Name: "Qemu-includeos-hello-world", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, + ExpectOut: "Hello, world!", + TestFunc: matchTest, + }, + { + Image: "harbor.nbfc.io/nubificus/urunc/hello-hvt-includeos:latest", + Name: "Hvt-includeos-hello-world", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, + ExpectOut: "Hello, world!", + TestFunc: matchTest, + }, + { + Image: "harbor.nbfc.io/nubificus/urunc/hello-spt-includeos:latest", + Name: "Spt-includeos-hello-world", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, + ExpectOut: "Hello, world!", + TestFunc: matchTest, + }, } } @@ -1241,5 +1292,56 @@ func dockerTestCases() []containerTestArgs { Skippable: false, TestFunc: namespaceTest, }, + { + Image: "harbor.nbfc.io/nubificus/urunc/hello-qemu-includeos:latest", + Name: "Qemu-includeos-hello-world", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, + ExpectOut: "Hello, world!", + TestFunc: matchTest, + }, + { + Image: "harbor.nbfc.io/nubificus/urunc/hello-hvt-includeos:latest", + Name: "Hvt-includeos-hello-world", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, + ExpectOut: "Hello, world!", + TestFunc: matchTest, + }, + { + Image: "harbor.nbfc.io/nubificus/urunc/hello-spt-includeos:latest", + Name: "Spt-includeos-hello-world", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, + ExpectOut: "Hello, world!", + TestFunc: matchTest, + }, } }