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") }