Files
egress-proxies-operator/internal/provider/config_kvm_test.go
T
kacerrandClaude Fable 5.1 bd633c0288 Add KVM provider config: KVMConfig structs and validation
Adds the kvm block to the providers config (SSH access, images map,
machine-type profiles, hosts list) with validation and table tests.
validate() now rejects mismatched type-specific blocks generically via
setBlocks() instead of pairwise checks.

Step 1 of docs/plans/2026-09-04-1404-kvm-provider.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 14:25:06 +00:00

209 lines
6.8 KiB
Go

package provider
import (
"strings"
"testing"
)
const validKVMYAML = `
providers:
- name: kvm-home
type: kvm
kvm:
sshKeyFile: /etc/proxy-operator/kvm/id_ed25519
knownHostsFile: /etc/proxy-operator/kvm/known_hosts
diskSizeGb: 20
images:
debian-13:
url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
ubuntu:
url: http://mirror.local/noble.img
format: raw
machineTypes:
small: {vcpus: 1, memoryMiB: 1024}
medium: {vcpus: 2, memoryMiB: 2048}
hosts:
- name: nas1
address: nas1.home.lan:22
user: virt
bridge: br0
- name: lab2
address: 10.0.0.2:2222
user: root
socket: /run/libvirt/virtqemud-sock
pool: fast
bridge: br1
`
func TestLoadConfig_kvmValid(t *testing.T) {
t.Parallel()
cfg, err := LoadConfig([]byte(validKVMYAML))
if err != nil {
t.Fatalf("LoadConfig() error = %v, want nil", err)
}
k := cfg.Providers[0].KVM
if k == nil {
t.Fatal("providers[0].kvm = nil")
}
if k.SSHKeyFile != "/etc/proxy-operator/kvm/id_ed25519" || k.DiskSizeGB != 20 {
t.Errorf("kvm = %+v, want sshKeyFile and diskSizeGb parsed", k)
}
if got := k.Images["debian-13"].SHA256; !strings.HasPrefix(got, "0123") {
t.Errorf("images[debian-13].sha256 = %q", got)
}
if k.Images["ubuntu"].Format != "raw" {
t.Errorf("images[ubuntu].format = %q, want raw", k.Images["ubuntu"].Format)
}
if mt := k.MachineTypes["medium"]; mt.VCPUs != 2 || mt.MemoryMiB != 2048 {
t.Errorf("machineTypes[medium] = %+v, want {2 2048}", mt)
}
if len(k.Hosts) != 2 || k.Hosts[1].Socket != "/run/libvirt/virtqemud-sock" || k.Hosts[1].Pool != "fast" {
t.Errorf("hosts = %+v, want two hosts with lab2's socket and pool parsed", k.Hosts)
}
}
func TestLoadConfig_kvmInsecureHostKeyNeedsNoKnownHosts(t *testing.T) {
t.Parallel()
yaml := strings.Replace(validKVMYAML,
"knownHostsFile: /etc/proxy-operator/kvm/known_hosts",
"insecureIgnoreHostKey: true", 1)
if _, err := LoadConfig([]byte(yaml)); err != nil {
t.Fatalf("LoadConfig() error = %v, want nil", err)
}
}
func TestLoadConfig_kvmInvalid(t *testing.T) {
t.Parallel()
minimal := func(kvmBody string) string {
return "providers:\n - name: kvm-home\n type: kvm\n kvm:\n" + indent(kvmBody, 6)
}
hostsAndTypes := `
machineTypes:
small: {vcpus: 1, memoryMiB: 1024}
hosts:
- {name: nas1, address: nas1:22, user: virt, bridge: br0}
`
tests := []struct {
name string
yaml string
wantErrSub string
}{
{
name: "missing kvm block",
yaml: "providers:\n - name: kvm-home\n type: kvm\n",
wantErrSub: "kvm block is required",
},
{
name: "missing sshKeyFile",
yaml: minimal("knownHostsFile: /kh\n" + hostsAndTypes),
wantErrSub: "kvm.sshKeyFile is required",
},
{
name: "missing knownHostsFile without insecure flag",
yaml: minimal("sshKeyFile: /key\n" + hostsAndTypes),
wantErrSub: "kvm.knownHostsFile is required unless",
},
{
name: "negative disk size",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\ndiskSizeGb: -1\n" + hostsAndTypes),
wantErrSub: "diskSizeGb must not be negative",
},
{
name: "no machine types",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nhosts:\n - {name: nas1, address: nas1:22, user: virt, bridge: br0}\n"),
wantErrSub: "machineTypes must define at least one",
},
{
name: "machine type with zero memory",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nmachineTypes:\n small: {vcpus: 1}\nhosts:\n - {name: nas1, address: nas1:22, user: virt, bridge: br0}\n"),
wantErrSub: `machineTypes["small"] needs vcpus > 0 and memoryMiB > 0`,
},
{
name: "no hosts",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nmachineTypes:\n small: {vcpus: 1, memoryMiB: 1024}\n"),
wantErrSub: "kvm.hosts must list at least one host",
},
{
name: "duplicate host name",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nmachineTypes:\n small: {vcpus: 1, memoryMiB: 1024}\nhosts:\n - {name: nas1, address: a:22, user: virt, bridge: br0}\n - {name: nas1, address: b:22, user: virt, bridge: br0}\n"),
wantErrSub: `duplicate host name "nas1"`,
},
{
name: "host missing address",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nmachineTypes:\n small: {vcpus: 1, memoryMiB: 1024}\nhosts:\n - {name: nas1, user: virt, bridge: br0}\n"),
wantErrSub: "address is required",
},
{
name: "host missing user",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nmachineTypes:\n small: {vcpus: 1, memoryMiB: 1024}\nhosts:\n - {name: nas1, address: a:22, bridge: br0}\n"),
wantErrSub: "user is required",
},
{
name: "host missing bridge",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nmachineTypes:\n small: {vcpus: 1, memoryMiB: 1024}\nhosts:\n - {name: nas1, address: a:22, user: virt}\n"),
wantErrSub: "bridge is required",
},
{
name: "image with non-http url",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nimages:\n d: {url: file:///tmp/x.qcow2}\n" + hostsAndTypes),
wantErrSub: `images["d"].url must be an http(s) URL`,
},
{
name: "image with bad sha256",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nimages:\n d: {url: https://x/y.qcow2, sha256: abc}\n" + hostsAndTypes),
wantErrSub: `images["d"].sha256 must be 64 hex characters`,
},
{
name: "image with unknown format",
yaml: minimal("sshKeyFile: /key\nknownHostsFile: /kh\nimages:\n d: {url: https://x/y.vmdk, format: vmdk}\n" + hostsAndTypes),
wantErrSub: `images["d"].format must be qcow2 or raw`,
},
{
name: "kvm type with gcp block",
yaml: `
providers:
- name: p1
type: kvm
gcp:
project: my-project`,
wantErrSub: "type is kvm but a gcp block is set",
},
{
name: "gcp type with kvm block",
yaml: `
providers:
- name: p1
type: gcp
gcp:
project: my-project
kvm:
sshKeyFile: /key`,
wantErrSub: "type is gcp but a kvm block is set",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := LoadConfig([]byte(tc.yaml))
if err == nil {
t.Fatalf("LoadConfig() error = nil, want error containing %q", tc.wantErrSub)
}
if !strings.Contains(err.Error(), tc.wantErrSub) {
t.Errorf("LoadConfig() error = %q, want substring %q", err.Error(), tc.wantErrSub)
}
})
}
}
func indent(s string, n int) string {
pad := strings.Repeat(" ", n)
lines := strings.Split(strings.TrimLeft(s, "\n"), "\n")
for i, l := range lines {
if l != "" {
lines[i] = pad + l
}
}
return strings.Join(lines, "\n")
}