Skip to content
Draft

KVM #143

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
41 changes: 41 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Run integration tests

on:
pull_request:
types: [labeled, synchronize, reopened]
paths-ignore:
- 'docs/**'

jobs:
integration-tests:
runs-on: ubuntu-22.04

if: contains(github.event.pull_request.labels.*.name, 'integration-tests')

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'


- name: Install dependencies
run: |
set -x

sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
ca-certificates libc-bin gcc cpu-checker
sudo update-ca-certificates
sudo rm -rf /var/lib/apt/lists
sudo chmod +x ./dev-vm/install-chp.sh
./dev-vm/install-chp.sh


- name: Run integration tests
run: |
sudo kvm-ok
sudo /usr/local/cloud-hypervisor/50.0/bin/cloud-hypervisor --version
77 changes: 77 additions & 0 deletions dev-vm/install-chp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail

VERSION="${VERSION:-50.0}"
ARCH="${ARCH:-x86_64}" # x86_64 or aarch64
BASE_DIR="/usr/local/cloud-hypervisor"


normalize_arch() {
case "$1" in
amd64|x86_64) echo "x86_64" ;;
arm64|aarch64) echo "aarch64" ;;
*)
echo "ERROR: Unsupported architecture '$1'" >&2
exit 1
;;
esac
}

ARCH="$(normalize_arch "$ARCH")"
HOST_ARCH="$(normalize_arch "$(uname -m)")"

echo "Install settings:"
echo " VERSION = ${VERSION}"
echo " ARCH = ${ARCH}"
echo " HOST_ARCH = ${HOST_ARCH}"

if [[ "$ARCH" != "$HOST_ARCH" ]]; then
echo "ERROR: ARCH=${ARCH} does not match host architecture (${HOST_ARCH})." >&2
echo "Refusing to install wrong binary (would cause 'Exec format error')." >&2
exit 2
fi

case "$ARCH" in
x86_64)
CHP_ASSET="cloud-hypervisor-static"
CHR_ASSET="ch-remote"
;;
aarch64)
CHP_ASSET="cloud-hypervisor-static-aarch64"
CHR_ASSET="ch-remote-static-aarch64"
;;
esac

BASE_URL="https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v${VERSION}"
DEST_DIR="${BASE_DIR}/${VERSION}/bin"

install_bin() {
local name="$1"
local asset="$2"

local url="${BASE_URL}/${asset}"
local dest="${DEST_DIR}/${name}"

echo "Installing ${name}"
echo " url: ${url}"
echo " dest: ${dest}"

local tmp
tmp="$(mktemp)"
trap 'rm -f "$tmp"' RETURN

curl -fL "$url" -o "$tmp"
chmod 0755 "$tmp"
sudo mkdir -p "$DEST_DIR"
sudo mv "$tmp" "$dest"
}

install_bin "cloud-hypervisor" "$CHP_ASSET"
install_bin "ch-remote" "$CHR_ASSET"

# Optional sanity checks
"${DEST_DIR}/cloud-hypervisor" --version || true
"${DEST_DIR}/ch-remote" --help >/dev/null 2>&1 || true


echo "All done."
154 changes: 154 additions & 0 deletions internal/controllers/controllers_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package controllers_test

import (
"context"
"os"
"testing"
"time"

"github.com/ironcore-dev/cloud-hypervisor-provider/api"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/controllers"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/host"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/plugins/networkinterface/isolated"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/plugins/volume"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/plugins/volume/emptydisk"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/raw"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/strategy"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/vmm"
"github.com/ironcore-dev/ironcore-image/oci/remote"
ocistore "github.com/ironcore-dev/ironcore-image/oci/store"
"github.com/ironcore-dev/provider-utils/eventutils/event"
"github.com/ironcore-dev/provider-utils/eventutils/recorder"
ocihostutils "github.com/ironcore-dev/provider-utils/ociutils/host"
ociutils "github.com/ironcore-dev/provider-utils/ociutils/oci"
hostutils "github.com/ironcore-dev/provider-utils/storeutils/host"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

const (
eventuallyTimeout = 80 * time.Second
pollingInterval = 50 * time.Millisecond
consistentlyDuration = 1 * time.Second
osImage = "ghcr.io/ironcore-dev/os-images/virtualization/gardenlinux:latest"
)

var (
tempDir string
machineStore *hostutils.Store[*api.Machine]
)

func TestControllers(t *testing.T) {
SetDefaultConsistentlyPollingInterval(pollingInterval)
SetDefaultEventuallyPollingInterval(pollingInterval)
SetDefaultEventuallyTimeout(eventuallyTimeout)
SetDefaultConsistentlyDuration(consistentlyDuration)

RegisterFailHandler(Fail)
RunSpecs(t, "Machine Controller Suite", Label("integration"))
}

var _ = BeforeSuite(func(ctx context.Context) {
log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))
logf.SetLogger(log)

By("setting up test environment")
rootDir := GinkgoT().TempDir()

hostPaths, err := host.PathsAt(rootDir)
Expect(err).NotTo(HaveOccurred())

platform, err := ocihostutils.Platform()
Expect(err).NotTo(HaveOccurred())

reg, err := remote.DockerRegistryWithPlatform(nil, platform)
Expect(err).NotTo(HaveOccurred())

ociStore, err := ocistore.New(hostPaths.ImagesDir())
Expect(err).NotTo(HaveOccurred())

rawInst, err := raw.Instance(raw.Default())
Expect(err).NotTo(HaveOccurred())

imgCache, err := ociutils.NewLocalCache(log, reg, ociStore, nil)
Expect(err).NotTo(HaveOccurred())

volumePlugins := volume.NewPluginManager()
Expect(volumePlugins.InitPlugins(hostPaths, []volume.Plugin{
emptydisk.NewPlugin(rawInst),
})).NotTo(HaveOccurred())

nicPlugin := isolated.NewPlugin()
Expect(nicPlugin.Init(hostPaths)).NotTo(HaveOccurred())

machineStore, err = hostutils.NewStore[*api.Machine](hostutils.Options[*api.Machine]{
Dir: tempDir,
NewFunc: func() *api.Machine { return &api.Machine{} },
CreateStrategy: strategy.MachineStrategy,
})
Expect(err).NotTo(HaveOccurred())

machineEvents, err := event.NewListWatchSource[*api.Machine](
machineStore.List,
machineStore.Watch,
event.ListWatchSourceOptions{},
)
Expect(err).NotTo(HaveOccurred())

chSocketDir := os.Getenv("CH_SOCKET_DIR")
chFirmwarePath := os.Getenv("CH_FIRMWARE_PATH")

virtualMachineManager, err := vmm.NewManager(
log.WithName("virtual-machine-manager"),
hostPaths,
vmm.ManagerOptions{
CHSocketsPath: chSocketDir,
FirmwarePath: chFirmwarePath,
ReservedInstances: nil,
},
)
Expect(err).NotTo(HaveOccurred())

eventRecorder := recorder.NewEventStore(log, recorder.EventStoreOptions{})
machineReconciler, err := controllers.NewMachineReconciler(
log.WithName("machine-reconciler"),
machineStore,
machineEvents,
eventRecorder,
virtualMachineManager,
volumePlugins,
nicPlugin,
controllers.MachineReconcilerOptions{
ImageCache: imgCache,
Raw: rawInst,
Paths: hostPaths,
},
)
Expect(err).NotTo(HaveOccurred())

go func() {
defer GinkgoRecover()
Expect(imgCache.Start(ctx)).To(Succeed())
}()

go func() {
defer GinkgoRecover()
Expect(machineReconciler.Start(ctx)).To(Succeed())
}()

go func() {
defer GinkgoRecover()
Expect(machineEvents.Start(ctx)).To(Succeed())
}()

go func() {
defer GinkgoRecover()
eventRecorder.Start(ctx)
}()

})
56 changes: 56 additions & 0 deletions internal/controllers/machine_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package controllers_test

import (
"context"
"net/http"

"github.com/ironcore-dev/cloud-hypervisor-provider/api"
"github.com/ironcore-dev/cloud-hypervisor-provider/internal/vmm"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/utils/ptr"
)

var _ = Describe("MachineController", func() {
Context("Machine Lifecycle", func(ctx context.Context) {
var machineID string

It("should create and reconcile a machine", func(ctx SpecContext) {
By("creating a machine in the store")
machine, err := machineStore.Create(ctx, &api.Machine{
Spec: api.MachineSpec{
Power: api.PowerStatePowerOn,
Cpu: 4,
MemoryBytes: 4294967296, // 4GB
Image: ptr.To(osImage),
//Volumes: []*api.VolumeSpec{
// {
// Name: "root",
// Device: "oda",
// EmptyDisk: &api.EmptyDiskSpec{
// },
// },
//},
},
})
Expect(err).NotTo(HaveOccurred())
Expect(machine).NotTo(BeNil())
Expect(machine.ID).NotTo(BeEmpty())

GinkgoWriter.Printf("Created machine: ID=%s\n", machineID)

Eventually(machine.Spec.ApiSocketPath).ShouldNot(BeEmpty())

chClient, err := vmm.NewUnixSocketClient(ptr.Deref(machine.Spec.ApiSocketPath, ""))
Expect(err).NotTo(HaveOccurred())

resp, err := chClient.GetVmmPingWithResponse(ctx)
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
})

})
})
9 changes: 2 additions & 7 deletions internal/vmm/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewManager(log logr.Logger, paths host.Paths, opts ManagerOptions) (*Manage

socketPath := filepath.Join(opts.CHSocketsPath, v.Name())

apiClient, err := newUnixSocketClient(socketPath)
apiClient, err := NewUnixSocketClient(socketPath)
if err != nil {
initLog.V(1).Info("Failed to init cloud-hypervisor client", "path", socketPath)
continue
Expand Down Expand Up @@ -93,13 +93,8 @@ type Manager struct {
}

var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrResourceVersionNotLatest = errors.New("resourceVersion is not latest")
ErrVmInitialized = errors.New("vm already initialized")

ErrBrokenSocket = errors.New("broken socket")

ErrNotFound = errors.New("not found")
ErrVmNotCreated = errors.New("vm is not created")
)

Expand Down
2 changes: 1 addition & 1 deletion internal/vmm/socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/ironcore-dev/cloud-hypervisor-provider/cloud-hypervisor/client"
)

func newUnixSocketClient(socketPath string) (*client.ClientWithResponses, error) {
func NewUnixSocketClient(socketPath string) (*client.ClientWithResponses, error) {
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", socketPath)
Expand Down
Loading