Files
egress-proxies-operator/internal/provider/kvm2/kvm2_test.go
T
kacerrandClaude Fable 5.1 95768281be kvm2: config block, constructor registration, IDs, state mapping and classify
The kvm2 block reuses KVMConfig; validateKVM gains a block-name variant so
messages say kvm2. The package skeleton has a non-dialing New, strict
providerID parsing, the state table and a 34-row classify test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:38:48 +00:00

286 lines
8.7 KiB
Go

package kvm2
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/go-logr/logr/funcr"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
func captureContext() (context.Context, *[]string) {
lines := &[]string{}
log := funcr.New(func(prefix, args string) {
*lines = append(*lines, prefix+" "+args)
}, funcr.Options{Verbosity: 2})
return logf.IntoContext(context.Background(), log), lines
}
func twoHosts() provider.KVMConfig {
return provider.KVMConfig{
SSHKeyFile: "/nonexistent/kvm2/id_ed25519",
KnownHostsFile: "/nonexistent/kvm2/known_hosts",
MachineTypes: map[string]provider.KVMMachineType{"small": {VCPUs: 1, MemoryMiB: 1024}},
Hosts: []provider.KVMHost{
{Name: "beelink", Address: "beelink:22", User: "virt", Bridge: "br0"},
{Name: "lab2", Address: "lab2:22", User: "virt", Bridge: "br1", Socket: "/run/s", Pool: "fast"},
},
}
}
func TestNew_doesNotDialAndWarnsOnMissingFiles(t *testing.T) {
t.Parallel()
ctx, lines := captureContext()
cfg := twoHosts()
cfg.Hosts[0].Address = "192.0.2.1:22"
start := time.Now()
p, err := New(ctx, provider.ProviderConfig{Name: "kvm2-home", Type: "kvm2", KVM2: &cfg})
elapsed := time.Since(start)
if err != nil {
t.Fatalf("New() error = %v, want nil", err)
}
if p == nil {
t.Fatal("New() = nil provider")
}
if elapsed > 100*time.Millisecond {
t.Errorf("New() took %v, want under 100ms (it must not dial)", elapsed)
}
var warnings int
for _, l := range *lines {
if strings.Contains(l, "WARNING:") {
warnings++
}
if !strings.Contains(l, `"provider"="kvm2-home"`) {
t.Errorf("log line lacks the provider value: %s", l)
}
}
if warnings != 2 {
t.Errorf("got %d WARNING: lines, want 2 (key and known_hosts):\n%s", warnings, strings.Join(*lines, "\n"))
}
}
func TestNew_insecureIgnoreHostKeyWarns(t *testing.T) {
t.Parallel()
ctx, lines := captureContext()
cfg := twoHosts()
cfg.KnownHostsFile = ""
cfg.InsecureIgnoreHostKey = true
if _, err := New(ctx, provider.ProviderConfig{Name: "kvm2-home", Type: "kvm2", KVM2: &cfg}); err != nil {
t.Fatalf("New() error = %v, want nil", err)
}
found := false
for _, l := range *lines {
if strings.Contains(l, "WARNING:") && strings.Contains(l, "host keys are not verified") {
found = true
}
if strings.Contains(l, "known_hosts") {
t.Errorf("known_hosts warning emitted although host keys are ignored: %s", l)
}
}
if !found {
t.Errorf("no insecureIgnoreHostKey warning in:\n%s", strings.Join(*lines, "\n"))
}
}
func TestNew_nilBlockDoesNotPanic(t *testing.T) {
t.Parallel()
ctx, _ := captureContext()
p, err := New(ctx, provider.ProviderConfig{Name: "kvm2-empty", Type: "kvm2"})
if err != nil {
t.Fatalf("New() error = %v, want nil", err)
}
kp, ok := p.(*Provider)
if !ok {
t.Fatalf("New() = %T, want *Provider", p)
}
if kp.name != "kvm2-empty" || kp.cfg.DiskSizeGB != defaultDiskSizeGB || kp.now == nil {
t.Errorf("provider = %+v, want name kvm2-empty, default disk size and a clock", kp)
}
}
func TestHost_table(t *testing.T) {
t.Parallel()
tests := []struct {
name string
hosts []provider.KVMHost
zone string
wantName string
wantErr string
}{
{
name: "one host and empty zone selects it",
hosts: twoHosts().Hosts[:1],
zone: "",
wantName: "beelink",
},
{
name: "several hosts and empty zone is permanent",
hosts: twoHosts().Hosts,
zone: "",
wantErr: "placement.zone is required when kvm2.hosts lists more than one host",
},
{
name: "known name",
hosts: twoHosts().Hosts,
zone: "lab2",
wantName: "lab2",
},
{
name: "unknown name is permanent",
hosts: twoHosts().Hosts,
zone: "nas9",
wantErr: `host "nas9" is not in kvm2.hosts`,
},
{
name: "no hosts at all is permanent",
hosts: nil,
zone: "",
wantErr: "placement.zone is required",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := newProvider("kvm2-home", provider.KVMConfig{Hosts: tc.hosts})
h, err := p.host(tc.zone)
if tc.wantErr != "" {
if err == nil {
t.Fatalf("host(%q) = %+v, want error containing %q", tc.zone, h, tc.wantErr)
}
if !errors.Is(err, provider.ErrPermanent) {
t.Errorf("host(%q) error = %v, want ErrPermanent", tc.zone, err)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("host(%q) error = %q, want substring %q", tc.zone, err, tc.wantErr)
}
return
}
if err != nil {
t.Fatalf("host(%q) error = %v, want nil", tc.zone, err)
}
if h.Name != tc.wantName {
t.Errorf("host(%q).Name = %q, want %q", tc.zone, h.Name, tc.wantName)
}
})
}
}
func TestWithDefaults_table(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in provider.KVMConfig
check func(t *testing.T, got provider.KVMConfig)
}{
{
name: "empty config gets disk size and empty maps",
in: provider.KVMConfig{},
check: func(t *testing.T, got provider.KVMConfig) {
if got.DiskSizeGB != 10 {
t.Errorf("DiskSizeGB = %d, want 10", got.DiskSizeGB)
}
if got.Hosts == nil || got.Images == nil {
t.Errorf("Hosts = %v, Images = %v, want non-nil", got.Hosts, got.Images)
}
},
},
{
name: "explicit disk size kept",
in: provider.KVMConfig{DiskSizeGB: 40},
check: func(t *testing.T, got provider.KVMConfig) {
if got.DiskSizeGB != 40 {
t.Errorf("DiskSizeGB = %d, want 40", got.DiskSizeGB)
}
},
},
{
name: "host socket and pool default",
in: provider.KVMConfig{Hosts: []provider.KVMHost{{Name: "a"}, {Name: "b", Socket: "/run/s", Pool: "fast"}}},
check: func(t *testing.T, got provider.KVMConfig) {
if got.Hosts[0].Socket != "/var/run/libvirt/libvirt-sock" || got.Hosts[0].Pool != "default" {
t.Errorf("hosts[0] = %+v, want default socket and pool", got.Hosts[0])
}
if got.Hosts[1].Socket != "/run/s" || got.Hosts[1].Pool != "fast" {
t.Errorf("hosts[1] = %+v, want explicit socket and pool kept", got.Hosts[1])
}
},
},
{
name: "image format and firmware default",
in: provider.KVMConfig{Images: map[string]provider.KVMImage{
"d": {URL: "https://x/d.qcow2"},
"u": {URL: "https://x/u.img", Format: provider.KVMFormatRaw, Firmware: provider.KVMFirmwareBIOS},
}},
check: func(t *testing.T, got provider.KVMConfig) {
if got.Images["d"].Format != provider.KVMFormatQcow2 || got.Images["d"].Firmware != provider.KVMFirmwareUEFI {
t.Errorf("images[d] = %+v, want qcow2 and uefi", got.Images["d"])
}
if got.Images["u"].Format != provider.KVMFormatRaw || got.Images["u"].Firmware != provider.KVMFirmwareBIOS {
t.Errorf("images[u] = %+v, want raw and bios kept", got.Images["u"])
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.check(t, withDefaults(tc.in))
})
}
}
func TestWithDefaults_doesNotAliasInput(t *testing.T) {
t.Parallel()
in := provider.KVMConfig{
Hosts: []provider.KVMHost{{Name: "a"}},
Images: map[string]provider.KVMImage{"d": {URL: "https://x/d.qcow2"}},
}
got := withDefaults(in)
got.Hosts[0].Name = "changed"
got.Images["d"] = provider.KVMImage{URL: "changed"}
if in.Hosts[0].Name != "a" || in.Images["d"].URL != "https://x/d.qcow2" {
t.Errorf("input mutated through the defaulted copy: %+v", in)
}
}
func TestWrapErr_carriesProviderNameOpAndID(t *testing.T) {
t.Parallel()
p := newProvider("kvm2-home", twoHosts())
err := p.wrapErr("get", "hosts/beelink/domains/proxy-x", permanentf("boom"))
var pe *provider.Error
if !errors.As(err, &pe) {
t.Fatalf("wrapErr() = %T, want *provider.Error", err)
}
if pe.Provider != "kvm2-home" || pe.Op != "get" || pe.ID != "hosts/beelink/domains/proxy-x" || pe.Class != provider.ErrPermanent {
t.Errorf("wrapErr() = %+v, want provider kvm2-home, op get, the id and ErrPermanent", pe)
}
}
func TestStubs_returnPermanentWithProviderName(t *testing.T) {
t.Parallel()
ctx := context.Background()
p := newProvider("kvm2-home", twoHosts())
_, createErr := p.Create(ctx, provider.CreateRequest{Name: "proxy-x"})
inst, getErr := p.Get(ctx, "hosts/beelink/domains/proxy-x")
deleteErr := p.Delete(ctx, "hosts/beelink/domains/proxy-x")
list, listErr := p.ListByTag(ctx)
if inst != nil || list != nil {
t.Errorf("Get = %+v, ListByTag = %+v, want nil results from stubs", inst, list)
}
for op, err := range map[string]error{"create": createErr, "get": getErr, "delete": deleteErr, "list": listErr} {
var pe *provider.Error
if !errors.As(err, &pe) {
t.Errorf("%s: error = %T, want *provider.Error", op, err)
continue
}
if pe.Provider != "kvm2-home" || pe.Op != op || !errors.Is(err, provider.ErrPermanent) {
t.Errorf("%s: error = %+v, want provider kvm2-home, op %s, ErrPermanent", op, pe, op)
}
}
}