Files
egress-proxies-operator/internal/provider/proxmox/seed_test.go
T
kacerrandClaude Opus 5.5 0a6c8624f3 provider: extract nocloud seed builder and keyedlock from kvm2
kvm2's NoCloud seed builder moves to internal/provider/nocloud (exported,
with an ErrEmptyName sentinel instead of permanentf) and its keyed locks
to internal/provider/keyedlock, tests moved under their existing names.
kvm2 behaviour is unchanged. proxmox gains the per-name lock and the pure
seed helpers (seedISOName, seedVolID, buildSeed); unlike kvm2 (review
finding S5.1), a seed staging failure there is Transient, not Permanent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 14:01:27 +00:00

140 lines
4.5 KiB
Go

package proxmox
import (
"bytes"
"errors"
"io"
"strings"
"testing"
"github.com/kdomanski/iso9660"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/nocloud"
)
// seedFiles opens a built seed and returns its label and files by name.
func seedFiles(t *testing.T, iso *bytes.Buffer) (string, map[string]string) {
t.Helper()
img, err := iso9660.OpenImage(bytes.NewReader(iso.Bytes()))
if err != nil {
t.Fatalf("OpenImage: %v", err)
}
label, err := img.Label()
if err != nil {
t.Fatalf("Label: %v", err)
}
root, err := img.RootDir()
if err != nil {
t.Fatalf("RootDir: %v", err)
}
children, err := root.GetChildren()
if err != nil {
t.Fatalf("GetChildren: %v", err)
}
files := make(map[string]string, len(children))
for _, c := range children {
data, err := io.ReadAll(c.Reader())
if err != nil {
t.Fatalf("reading %s: %v", c.Name(), err)
}
files[c.Name()] = string(data)
}
return label, files
}
func TestSeedNames_table(t *testing.T) {
t.Parallel()
if got, want := seedISOName("proxy-abc"), "proxy-abc-seed.iso"; got != want {
t.Errorf("seedISOName = %q, want %q", got, want)
}
if got, want := seedVolID("proxy-seeds", "proxy-abc"), "proxy-seeds:iso/proxy-abc-seed.iso"; got != want {
t.Errorf("seedVolID = %q, want %q", got, want)
}
}
func TestBuildSeed_table(t *testing.T) {
t.Parallel()
off := false
userData := "#cloud-config\r\npackages: [squid]\r\nwrite_files:\n - content: \"caf\xc3\xa9\"\n"
configured := "#cloud-config\nntp:\n enabled: true\n"
tests := []struct {
name string
vendorData string
install *bool
want func(t *testing.T, got string)
}{
{name: "agent script by default", want: func(t *testing.T, got string) {
if got != nocloud.GuestAgentScript {
t.Errorf("vendor-data is not the guest agent script: %q", got)
}
}},
{name: "configured joined with the agent script", vendorData: configured, want: func(t *testing.T, got string) {
if !strings.HasPrefix(got, "MIME-Version: 1.0\r\n") || !strings.Contains(got, configured) || !strings.Contains(got, nocloud.GuestAgentScript) {
t.Errorf("vendor-data is not a multipart of both documents: %q", got)
}
}},
{name: "agent off passes configured through", vendorData: configured, install: &off, want: func(t *testing.T, got string) {
if got != configured {
t.Errorf("vendor-data = %q, want the configured document", got)
}
}},
{name: "agent off with nothing configured is empty", install: &off, want: func(t *testing.T, got string) {
if got != "" {
t.Errorf("vendor-data = %q, want empty", got)
}
}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
cfg := testConfig()
cfg.VendorData = tc.vendorData
cfg.InstallGuestAgent = tc.install
p := newProvider("proxmox-home", cfg, nil)
iso, err := p.buildSeed(provider.CreateRequest{Name: "proxy-abc", CloudInit: userData})
if err != nil {
t.Fatalf("buildSeed error = %v", err)
}
label, files := seedFiles(t, iso)
if label != nocloud.SeedLabel {
t.Errorf("label = %q, want %q", label, nocloud.SeedLabel)
}
if files["user-data"] != userData {
t.Errorf("user-data changed:\n got %q\nwant %q", files["user-data"], userData)
}
if want := "instance-id: proxy-abc\nlocal-hostname: proxy-abc\n"; files["meta-data"] != want {
t.Errorf("meta-data = %q, want %q", files["meta-data"], want)
}
tc.want(t, files["vendor-data"])
})
}
}
func TestBuildSeed_emptyNameIsPermanentWithoutPayload(t *testing.T) {
t.Parallel()
const sentinel = "SENTINEL-cloud-init-payload"
p := newProvider("proxmox-home", testConfig(), nil)
_, err := p.buildSeed(provider.CreateRequest{CloudInit: sentinel})
if !errors.Is(err, provider.ErrPermanent) {
t.Fatalf("buildSeed error = %v, want ErrPermanent", err)
}
if strings.Contains(err.Error(), sentinel) {
t.Errorf("error carries the cloud-init payload: %v", err)
}
}
// Not parallel: it points TMPDIR at a path that cannot exist for the
// process.
func TestBuildSeed_stagingFailureIsTransient(t *testing.T) {
t.Setenv("TMPDIR", t.TempDir()+"/missing")
p := newProvider("proxmox-home", testConfig(), nil)
_, err := p.buildSeed(provider.CreateRequest{Name: "proxy-abc", CloudInit: "#cloud-config\n"})
if err == nil {
t.Fatal("buildSeed with no staging dir succeeded")
}
if got := classify("create", err); got != provider.ErrTransient {
t.Errorf("classify(buildSeed error) = %v, want ErrTransient: %v", got, err)
}
}