package gitsync import ( "context" "os" "os/exec" "path/filepath" "strings" "testing" "time" "git.unkin.net/unkin/bootapi/internal/model" "git.unkin.net/unkin/bootapi/internal/render" "git.unkin.net/unkin/bootapi/templates" ) // gitRepo creates a real git repo at dir with an initial almalinux9 override. func gitRepo(t *testing.T, dir string) { t.Helper() gitCmd(t, "", "git", "init", "-b", "main", dir) gitCmd(t, dir, "git", "config", "user.email", "t@example.net") gitCmd(t, dir, "git", "config", "user.name", "test") writeKS(t, dir, "GITSYNC-V1 {{ .Hostname }}\n") gitCmd(t, dir, "git", "add", "-A") gitCmd(t, dir, "git", "commit", "-m", "v1") } func writeKS(t *testing.T, dir, body string) { t.Helper() if err := os.WriteFile(filepath.Join(dir, "almalinux9.ks.tmpl"), []byte(body), 0o600); err != nil { t.Fatal(err) } } func gitCmd(t *testing.T, dir, name string, args ...string) { t.Helper() cmd := exec.Command(name, args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s %v: %v: %s", name, args, err, out) } } func renderKS(t *testing.T, e *render.Engine) string { t.Helper() h := &model.Host{Hostname: "web01", Platform: "almalinux9", OSFamily: "almalinux", OSVersion: "9", Arch: "x86_64"} out, _, err := e.RenderKickstart(h) if err != nil { t.Fatalf("RenderKickstart: %v", err) } return string(out) } func TestBootstrapAndReload(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not available") } src := t.TempDir() gitRepo(t, src) s := New(Options{URL: src, Branch: "main", Interval: time.Hour, WorkDir: filepath.Join(t.TempDir(), "co")}, templates.FS) set, err := s.Bootstrap(context.Background()) if err != nil { t.Fatalf("Bootstrap: %v", err) } eng := render.NewEngine(render.RenderConfig{DefaultTemplate: "almalinux9", ArtifactBase: "https://af"}, set) s.SetEngine(eng) if got := renderKS(t, eng); !contains(got, "GITSYNC-V1 web01") { t.Fatalf("initial render missing v1 override:\n%s", got) } gen1 := s.Generation() // Commit v2 upstream, then poll: the engine must swap to the new content. writeKS(t, src, "GITSYNC-V2 {{ .Hostname }}\n") gitCmd(t, src, "git", "add", "-A") gitCmd(t, src, "git", "commit", "-m", "v2") s.pollOnce(context.Background()) if got := renderKS(t, eng); !contains(got, "GITSYNC-V2 web01") { t.Fatalf("after reload, render missing v2:\n%s", got) } if s.Generation() <= gen1 { t.Errorf("generation did not advance: %d <= %d", s.Generation(), gen1) } if s.Syncs() != 1 { t.Errorf("syncs = %d, want 1", s.Syncs()) } } func TestReloadKeepsLastGoodOnParseError(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not available") } src := t.TempDir() gitRepo(t, src) s := New(Options{URL: src, Branch: "main", Interval: time.Hour, WorkDir: filepath.Join(t.TempDir(), "co")}, templates.FS) set, err := s.Bootstrap(context.Background()) if err != nil { t.Fatal(err) } eng := render.NewEngine(render.RenderConfig{DefaultTemplate: "almalinux9", ArtifactBase: "https://af"}, set) s.SetEngine(eng) // Push a template that fails to parse. writeKS(t, src, "BROKEN {{ .Hostname \n") gitCmd(t, src, "git", "add", "-A") gitCmd(t, src, "git", "commit", "-m", "broken") s.pollOnce(context.Background()) // The last-good v1 set must still be served, and a failure recorded. if got := renderKS(t, eng); !contains(got, "GITSYNC-V1 web01") { t.Fatalf("last-good not kept after parse failure:\n%s", got) } if s.Failures() != 1 { t.Errorf("failures = %d, want 1", s.Failures()) } if s.Syncs() != 0 { t.Errorf("syncs = %d, want 0 (bad push must not count as a sync)", s.Syncs()) } } func TestBootstrapDegradesToEmbedded(t *testing.T) { // A bogus URL must not fail startup: Bootstrap returns the embedded set. s := New(Options{URL: "/nonexistent/repo", Branch: "main", Interval: time.Hour, WorkDir: filepath.Join(t.TempDir(), "co")}, templates.FS) set, err := s.Bootstrap(context.Background()) if err == nil { t.Error("expected a non-nil (non-fatal) error describing the degrade") } if set == nil { t.Fatal("expected the embedded fallback Set, got nil") } eng := render.NewEngine(render.RenderConfig{DefaultTemplate: "almalinux9", ArtifactBase: "https://af"}, set) // Embedded almalinux9 template still renders. if got := renderKS(t, eng); !contains(got, "rootpw") { t.Errorf("embedded fallback did not render a real kickstart:\n%s", got) } } func contains(s, sub string) bool { return strings.Contains(s, sub) }