Skip to content

Commit 2ba7d0f

Browse files
authored
Split server and controller tests (#670)
1 parent 8f0771c commit 2ba7d0f

20 files changed

+1303
-467
lines changed

cmd/libvirt-provider/app/app.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
147147
// Volume cache policy option
148148
fs.StringVar(&o.VolumeCachePolicy, "volume-cache-policy", "none",
149149
`Policy to use when creating a remote disk. (one of 'none', 'writeback', 'writethrough', 'directsync', 'unsafe').
150-
Note: The available options may depend on the hypervisor and libvirt version in use.
150+
Note: The available options may depend on the hypervisor and libvirt version in use.
151151
Please refer to the official documentation for more details: https://libvirt.org/formatdomain.html#hard-drives-floppy-disks-cdroms.`)
152152

153153
o.NicPlugin = networkinterfaceplugin.NewDefaultOptions()
@@ -410,7 +410,7 @@ func Run(ctx context.Context, opts Options) error {
410410

411411
g.Go(func() error {
412412
setupLog.Info("Starting grpc server")
413-
if err := runGRPCServer(ctx, setupLog, log, srv, opts); err != nil {
413+
if err := RunGRPCServer(ctx, setupLog, log, srv, opts.Address); err != nil {
414414
setupLog.Error(err, "failed to start grpc server")
415415
return err
416416
}
@@ -438,9 +438,9 @@ func Run(ctx context.Context, opts Options) error {
438438
return g.Wait()
439439
}
440440

441-
func runGRPCServer(ctx context.Context, setupLog logr.Logger, log logr.Logger, srv *server.Server, opts Options) error {
441+
func RunGRPCServer(ctx context.Context, setupLog logr.Logger, log logr.Logger, srv *server.Server, address string) error {
442442
setupLog.V(1).Info("Cleaning up any previous socket")
443-
if err := common.CleanupSocketIfExists(opts.Address); err != nil {
443+
if err := common.CleanupSocketIfExists(address); err != nil {
444444
return fmt.Errorf("error cleaning up socket: %w", err)
445445
}
446446

@@ -452,8 +452,8 @@ func runGRPCServer(ctx context.Context, setupLog logr.Logger, log logr.Logger, s
452452
)
453453
iri.RegisterMachineRuntimeServer(grpcSrv, srv)
454454

455-
setupLog.V(1).Info("Start listening on unix socket", "Address", opts.Address)
456-
l, err := net.Listen("unix", opts.Address)
455+
setupLog.V(1).Info("Start listening on unix socket", "Address", address)
456+
l, err := net.Listen("unix", address)
457457
if err != nil {
458458
return fmt.Errorf("failed to listen: %w", err)
459459
}
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package controllers_test
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
"time"
13+
14+
"github.com/digitalocean/go-libvirt"
15+
"github.com/google/uuid"
16+
"github.com/ironcore-dev/ironcore-image/oci/remote"
17+
ocistore "github.com/ironcore-dev/ironcore-image/oci/store"
18+
"github.com/ironcore-dev/libvirt-provider/api"
19+
"github.com/ironcore-dev/libvirt-provider/cmd/libvirt-provider/app"
20+
"github.com/ironcore-dev/libvirt-provider/internal/controllers"
21+
"github.com/ironcore-dev/libvirt-provider/internal/host"
22+
"github.com/ironcore-dev/libvirt-provider/internal/libvirt/guest"
23+
libvirtutils "github.com/ironcore-dev/libvirt-provider/internal/libvirt/utils"
24+
"github.com/ironcore-dev/libvirt-provider/internal/networkinterfaceplugin"
25+
providernetworkinterface "github.com/ironcore-dev/libvirt-provider/internal/plugins/networkinterface"
26+
volumeplugin "github.com/ironcore-dev/libvirt-provider/internal/plugins/volume"
27+
"github.com/ironcore-dev/libvirt-provider/internal/plugins/volume/localdisk"
28+
"github.com/ironcore-dev/libvirt-provider/internal/raw"
29+
"github.com/ironcore-dev/libvirt-provider/internal/strategy"
30+
apiutils "github.com/ironcore-dev/provider-utils/apiutils/api"
31+
"github.com/ironcore-dev/provider-utils/eventutils/event"
32+
"github.com/ironcore-dev/provider-utils/eventutils/recorder"
33+
ocihostutils "github.com/ironcore-dev/provider-utils/ociutils/host"
34+
ociutils "github.com/ironcore-dev/provider-utils/ociutils/oci"
35+
hostutils "github.com/ironcore-dev/provider-utils/storeutils/host"
36+
. "github.com/onsi/ginkgo/v2"
37+
. "github.com/onsi/gomega"
38+
logf "sigs.k8s.io/controller-runtime/pkg/log"
39+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
40+
)
41+
42+
const (
43+
eventuallyTimeout = 80 * time.Second
44+
pollingInterval = 50 * time.Millisecond
45+
gracefulShutdownTimeout = 0 * time.Second
46+
resyncGarbageCollectorInterval = 5 * time.Second
47+
consistentlyDuration = 1 * time.Second
48+
machineClassx3xlarge = "x3-xlarge"
49+
machineClassx2medium = "x2-medium"
50+
osImage = "ghcr.io/ironcore-dev/os-images/virtualization/gardenlinux:latest"
51+
emptyDiskSize = 1024 * 1024 * 1024
52+
machineEventMaxEvents = 1000
53+
machineEventTTL = 10 * time.Second
54+
machineEventResyncInterval = 2 * time.Second
55+
)
56+
57+
var (
58+
machineController *controllers.MachineReconciler
59+
machineStore *hostutils.Store[*api.Machine]
60+
machineEvents *event.ListWatchSource[*api.Machine]
61+
eventRecorder recorder.EventRecorder
62+
volumePlugins *volumeplugin.PluginManager
63+
networkPlugin providernetworkinterface.Plugin
64+
providerHost host.LibvirtHost
65+
libvirtConn *libvirt.Libvirt
66+
tempDir string
67+
controllerCtx context.Context
68+
controllerCancel context.CancelFunc
69+
)
70+
71+
func TestControllers(t *testing.T) {
72+
SetDefaultConsistentlyPollingInterval(pollingInterval)
73+
SetDefaultEventuallyPollingInterval(pollingInterval)
74+
SetDefaultEventuallyTimeout(eventuallyTimeout)
75+
SetDefaultConsistentlyDuration(consistentlyDuration)
76+
77+
RegisterFailHandler(Fail)
78+
RunSpecs(t, "Machine Controller Suite", Label("integration"))
79+
}
80+
81+
var _ = BeforeSuite(func() {
82+
log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))
83+
logf.SetLogger(log)
84+
85+
By("setting up test environment")
86+
87+
// Use a very short temp directory path to avoid Unix socket path length limits (108 chars)
88+
// Socket paths can be like: /tmp/t123/machines/uuid/qemu-guest-agent.sock (~85 chars)
89+
tempDir = filepath.Join("/tmp", fmt.Sprintf("t%d%v", time.Now().Unix(), GinkgoParallelProcess()))
90+
Expect(os.MkdirAll(tempDir, 0730)).Should(Succeed())
91+
DeferCleanup(func() {
92+
_ = os.RemoveAll(tempDir)
93+
})
94+
95+
rootDir := tempDir
96+
97+
By("setting up libvirt connection")
98+
libvirtOpts := app.LibvirtOptions{
99+
PreferredDomainTypes: []string{"kvm", "qemu"},
100+
PreferredMachineTypes: []string{"pc-q35", "pc-i440fx", "virt"},
101+
Qcow2Type: "exec",
102+
}
103+
libvirt, err := libvirtutils.GetLibvirt(libvirtOpts.Socket, libvirtOpts.Address, libvirtOpts.URI)
104+
Expect(err).NotTo(HaveOccurred())
105+
libvirtConn = libvirt
106+
DeferCleanup(libvirt.ConnectClose)
107+
108+
By("setting up provider host")
109+
providerHost, err = host.NewLibvirtAt(rootDir, libvirt)
110+
Expect(err).NotTo(HaveOccurred())
111+
112+
By("setting up machine store")
113+
machineStore, err = hostutils.NewStore[*api.Machine](hostutils.Options[*api.Machine]{
114+
NewFunc: func() *api.Machine { return &api.Machine{} },
115+
CreateStrategy: strategy.MachineStrategy,
116+
Dir: providerHost.MachineStoreDir(),
117+
})
118+
Expect(err).NotTo(HaveOccurred())
119+
120+
By("setting up machine events")
121+
machineEvents, err = event.NewListWatchSource[*api.Machine](
122+
machineStore.List,
123+
machineStore.Watch,
124+
event.ListWatchSourceOptions{
125+
ResyncDuration: 30 * time.Second,
126+
},
127+
)
128+
Expect(err).NotTo(HaveOccurred())
129+
130+
By("setting up event store")
131+
eventRecorder = recorder.NewEventStore(log, recorder.EventStoreOptions{
132+
MaxEvents: machineEventMaxEvents,
133+
TTL: machineEventTTL,
134+
ResyncInterval: machineEventResyncInterval,
135+
})
136+
137+
By("detecting guest capabilities")
138+
caps, err := guest.DetectCapabilities(libvirt, guest.CapabilitiesOptions{
139+
PreferredDomainTypes: libvirtOpts.PreferredDomainTypes,
140+
PreferredMachineTypes: libvirtOpts.PreferredMachineTypes,
141+
})
142+
Expect(err).NotTo(HaveOccurred())
143+
GinkgoWriter.Printf("Detected guest capabilities\n")
144+
145+
By("setting up OCI registry and image cache")
146+
platform, err := ocihostutils.Platform()
147+
Expect(err).NotTo(HaveOccurred())
148+
GinkgoWriter.Printf("Platform: %s\n", platform.Architecture)
149+
150+
reg, err := remote.DockerRegistryWithPlatform(nil, platform)
151+
Expect(err).NotTo(HaveOccurred())
152+
153+
ociStore, err := ocistore.New(providerHost.ImagesDir())
154+
Expect(err).NotTo(HaveOccurred())
155+
156+
imgCache, err := ociutils.NewLocalCache(log, reg, ociStore, nil)
157+
Expect(err).NotTo(HaveOccurred())
158+
159+
By("setting up raw instance")
160+
rawInst, err := raw.Instance(raw.Default())
161+
Expect(err).NotTo(HaveOccurred())
162+
163+
By("setting up volume plugins")
164+
volumePlugins = volumeplugin.NewPluginManager()
165+
err = volumePlugins.InitPlugins(providerHost, []volumeplugin.Plugin{
166+
localdisk.NewPlugin(rawInst, imgCache),
167+
})
168+
Expect(err).NotTo(HaveOccurred())
169+
170+
By("setting up network interface plugin")
171+
pluginOpts := networkinterfaceplugin.NewDefaultOptions()
172+
pluginOpts.PluginName = "isolated"
173+
var cleanup func()
174+
networkPlugin, cleanup, err = pluginOpts.NetworkInterfacePlugin()
175+
Expect(err).NotTo(HaveOccurred())
176+
if cleanup != nil {
177+
DeferCleanup(cleanup)
178+
}
179+
Expect(networkPlugin.Init(providerHost)).To(Succeed())
180+
181+
By("creating machine controller")
182+
machineController, err = controllers.NewMachineReconciler(
183+
log.WithName("machine-controller"),
184+
providerHost,
185+
machineStore,
186+
machineEvents,
187+
eventRecorder,
188+
controllers.MachineReconcilerOptions{
189+
GuestCapabilities: caps,
190+
ImageCache: imgCache,
191+
Raw: rawInst,
192+
VolumePluginManager: volumePlugins,
193+
NetworkInterfacePlugin: networkPlugin,
194+
ResyncIntervalGarbageCollector: resyncGarbageCollectorInterval,
195+
EnableHugepages: false,
196+
GCVMGracefulShutdownTimeout: gracefulShutdownTimeout,
197+
VolumeCachePolicy: "writethrough",
198+
},
199+
)
200+
Expect(err).NotTo(HaveOccurred())
201+
202+
By("starting machine events")
203+
controllerCtx, controllerCancel = context.WithCancel(context.Background())
204+
DeferCleanup(controllerCancel)
205+
206+
go func() {
207+
defer GinkgoRecover()
208+
err := machineEvents.Start(controllerCtx)
209+
if err != nil && controllerCtx.Err() == nil {
210+
// Only fail if not cancelled
211+
Expect(err).NotTo(HaveOccurred())
212+
}
213+
}()
214+
215+
By("starting machine controller")
216+
go func() {
217+
defer GinkgoRecover()
218+
err := machineController.Start(controllerCtx)
219+
if err != nil && controllerCtx.Err() == nil {
220+
// Only fail if not cancelled
221+
Expect(err).NotTo(HaveOccurred())
222+
}
223+
}()
224+
225+
By("starting image cache")
226+
go func() {
227+
defer GinkgoRecover()
228+
err := imgCache.Start(controllerCtx)
229+
if err != nil && controllerCtx.Err() == nil {
230+
// Only fail if not cancelled
231+
Expect(err).NotTo(HaveOccurred())
232+
}
233+
}()
234+
235+
// Wait a bit for controller and events to start
236+
time.Sleep(500 * time.Millisecond)
237+
238+
By("controller setup complete")
239+
})
240+
241+
func createMachine(spec api.MachineSpec) (*api.Machine, error) {
242+
machine := &api.Machine{
243+
Metadata: apiutils.Metadata{
244+
ID: uuid.NewString(),
245+
},
246+
Spec: spec,
247+
}
248+
return machineStore.Create(context.Background(), machine)
249+
}
250+
251+
func getMachine(id string) (*api.Machine, error) {
252+
return machineStore.Get(context.Background(), id)
253+
}
254+
255+
func deleteMachine(id string) error {
256+
return machineStore.Delete(context.Background(), id)
257+
}
258+
259+
func updateMachine(machine *api.Machine) (*api.Machine, error) {
260+
m, err := getMachine(machine.ID)
261+
if err != nil {
262+
return nil, err
263+
}
264+
GinkgoWriter.Printf("ResourceVersion: ID=%s\n", m.ResourceVersion)
265+
machine.ResourceVersion = m.ResourceVersion
266+
return machineStore.Update(context.Background(), machine)
267+
}
268+
269+
func cleanupMachine(machineID string) func(SpecContext) {
270+
return func(ctx SpecContext) {
271+
By(fmt.Sprintf("cleaning up machine ID=%s", machineID))
272+
err := deleteMachine(machineID)
273+
Expect(err).To(SatisfyAny(
274+
BeNil(),
275+
MatchError(ContainSubstring("NotFound")),
276+
))
277+
Eventually(func(g Gomega) bool {
278+
_, err = libvirtConn.DomainLookupByUUID(libvirtutils.UUIDStringToBytes(machineID))
279+
if err != nil {
280+
GinkgoWriter.Printf("Checking if domain still exists for machine ID=%s: err=%v\n", machineID, err)
281+
}
282+
return libvirt.IsNotFound(err)
283+
}).Should(BeTrue())
284+
}
285+
}

0 commit comments

Comments
 (0)